44 Commits

Author SHA1 Message Date
Wanjohi
f92150cb9e fix: WIP 2025-05-05 07:04:30 +03:00
Wanjohi
c0c14d8c3c feat: Change the pricing 2025-05-05 05:34:16 +03:00
Wanjohi
47e61599bb feat(api): Add payments with Polar.sh (#264)
## Description
<!-- Briefly describe the purpose and scope of your changes -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new subscription API endpoint for managing subscriptions
and products.
- Enhanced subscription management with new entities and
functionalities.
- Added functionality to retrieve current timestamps in both local and
UTC formats.
- Added Polar.sh integration with customer portal and checkout session
creation APIs.

- **Refactor**
- Redesigned team details to now present members and subscription
information instead of a plan type.
  - Enhanced member management by incorporating role assignments.
- Streamlined user data handling and removed legacy subscription event
logic.
  - Simplified error handling in actor functions for better clarity.
  - Updated plan types and UI labels to reflect new subscription tiers.
  - Improved database indexing for Steam user data.

- **Chores**
- Updated the database schema with new tables and fields to support
subscription, team, and member enhancements.
  - Extended identifier prefixes to broaden system integration.
- Added new secrets related to pricing plans in infrastructure
configuration.
  - Configured API and auth routing with new domain and routing rules.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-04-18 14:24:19 +03:00
dependabot[bot]
76d27e4708 build(deps): bump golang.org/x/net from 0.34.0 to 0.38.0 in /packages/maitred in the go_modules group across 1 directory (#266)
Bumps the go_modules group with 1 update in the /packages/maitred
directory: [golang.org/x/net](https://github.com/golang/net).

Updates `golang.org/x/net` from 0.34.0 to 0.38.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e1fcd82abb"><code>e1fcd82</code></a>
html: properly handle trailing solidus in unquoted attribute value in
foreign...</li>
<li><a
href="ebed060e8f"><code>ebed060</code></a>
internal/http3: fix build of tests with GOEXPERIMENT=nosynctest</li>
<li><a
href="1f1fa29e0a"><code>1f1fa29</code></a>
publicsuffix: regenerate table</li>
<li><a
href="12150816f7"><code>1215081</code></a>
http2: improve error when server sends HTTP/1</li>
<li><a
href="312450e473"><code>312450e</code></a>
html: ensure &lt;search&gt; tag closes &lt;p&gt; and update tests</li>
<li><a
href="09731f9bf9"><code>09731f9</code></a>
http2: improve handling of lost PING in Server</li>
<li><a
href="55989e24b9"><code>55989e2</code></a>
http2/h2c: use ResponseController for hijacking connections</li>
<li><a
href="2914f46773"><code>2914f46</code></a>
websocket: re-recommend gorilla/websocket</li>
<li><a
href="99b3ae0643"><code>99b3ae0</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="85d1d54551"><code>85d1d54</code></a>
go.mod: update golang.org/x dependencies</li>
<li>Additional commits viewable in <a
href="https://github.com/golang/net/compare/v0.34.0...v0.38.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/net&package-manager=go_modules&previous-version=0.34.0&new-version=0.38.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/nestrilabs/nestri/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:53:52 +03:00
Wanjohi
896832b89c 🐜 fix: Remove Steam account info repetition (#263)
## Description
<!-- Briefly describe the purpose and scope of your changes -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- User profiles now display integrated Steam account information for a
more consolidated view.
- Accounts can now include associated teams and Steam account
information.

- **Refactor**
- Streamlined the underlying data structures for user, machine, and
Steam information to improve consistency and performance.

- **Chores**
- Updated database schemas and upgraded core dependencies, including the
`remeda` and `vite` packages, while refining authentication settings for
smoother operation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 10:51:40 +03:00
dependabot[bot]
492013d610 build(deps): bump the npm_and_yarn group across 3 directories with 1 update (#260)
Bumps the npm_and_yarn group with 1 update in the /apps/docs directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /apps/www directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /packages/www
directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

Updates `vite` from 6.2.5 to 6.2.6
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v6.2.6</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->6.2.6 (2025-04-10)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: reject requests with <code>#</code> in request-target (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19830">#19830</a>)
(<a
href="3bb0883d22">3bb0883</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19830">#19830</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d3dbf25fd5"><code>d3dbf25</code></a>
release: v6.2.6</li>
<li><a
href="3bb0883d22"><code>3bb0883</code></a>
fix: reject requests with <code>#</code> in request-target (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19830">#19830</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v6.2.6/packages/vite">compare
view</a></li>
</ul>
</details>
<br />

Updates `vite` from 6.0.14 to 6.0.15
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v6.2.6</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->6.2.6 (2025-04-10)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: reject requests with <code>#</code> in request-target (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19830">#19830</a>)
(<a
href="3bb0883d22">3bb0883</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19830">#19830</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d3dbf25fd5"><code>d3dbf25</code></a>
release: v6.2.6</li>
<li><a
href="3bb0883d22"><code>3bb0883</code></a>
fix: reject requests with <code>#</code> in request-target (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19830">#19830</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v6.2.6/packages/vite">compare
view</a></li>
</ul>
</details>
<br />

Updates `vite` from 6.0.14 to 6.0.15
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v6.2.6</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->6.2.6 (2025-04-10)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: reject requests with <code>#</code> in request-target (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19830">#19830</a>)
(<a
href="3bb0883d22">3bb0883</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19830">#19830</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d3dbf25fd5"><code>d3dbf25</code></a>
release: v6.2.6</li>
<li><a
href="3bb0883d22"><code>3bb0883</code></a>
fix: reject requests with <code>#</code> in request-target (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19830">#19830</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v6.2.6/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/nestrilabs/nestri/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 10:33:56 +03:00
Wanjohi
e93099784c feat(api): Connect Steam to main user account (#262)
## Description
This attempts to connect the Steam account to user account... for easier
management

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Enhanced user profiles and account views now display integrated Steam
account details and enriched team associations for a more comprehensive
experience.
- **Chores**
- Backend and database refinements have been implemented to improve
system stability, data integrity, and overall performance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-14 10:32:21 +03:00
Kristian Ollikainen
9a6826b069 feat(runner): Fixes and improvements (#259)
## Description

- Improves latency for runner
- Fixes bugs in entrypoint bash scripts
- Package updates, gstreamer 1.26 and workaround for it

Modified runner workflow to hopefully pull latest cachyos base image on
nightlies. This will cause a full build but for nightlies should be
fine?

Also removed the duplicate key-down workaround as we've enabled ordered
datachannels now. Increased retransmit to 2 from 0 to see if it'll help
with some network issues.

Marked as draft as I need to do bug testing still, I'll do it after
fever calms down 😅



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Enhanced deployment workflows with optimized container image
management.
- Improved audio and video processing for lower latency and better
synchronization.
  - Consolidated debugging options to ease command-line monitoring.

- **Refactor**
- Streamlined internal script flow and process handling for smoother
performance.
- Updated dependency management and communication protocols to boost
overall stability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
2025-04-13 23:13:09 +03:00
Wanjohi
f408ec56cb feat(www): Add logic to the homepage and Steam integration (#258)
## Description
<!-- Briefly describe the purpose and scope of your changes -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Upgraded API and authentication services with dynamic scaling,
enhanced load balancing, and real-time interaction endpoints.
- Introduced new commands to streamline local development and container
builds.
- Added new endpoints for retrieving Steam account information and
managing connections.
- Implemented a QR code authentication interface for Steam, enhancing
user login experiences.

- **Database Updates**
- Rolled out comprehensive schema migrations that improve data integrity
and indexing.
- Introduced new tables for managing Steam user credentials and machine
information.

- **UI Enhancements**
- Added refreshed animated assets and an improved QR code login flow for
a more engaging experience.
	- Introduced new styled components for displaying friends and games.

- **Maintenance**
- Completed extensive refactoring and configuration updates to optimize
performance and development workflows.
- Updated logging configurations and improved error handling mechanisms.
	- Streamlined resource definitions in the configuration files.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-04-13 14:30:45 +03:00
Wanjohi
8394bb4259 🐜 fix(dependabot): Remove the goddamn file (#257)
## Description
<!-- Briefly describe the purpose and scope of your changes -->
2025-04-12 13:50:35 +03:00
dependabot[bot]
0305a14fdd build(deps-dev): bump @nuxt/devtools from 1.7.0 to 2.3.2 in /apps/docs (#256)
Bumps
[@nuxt/devtools](https://github.com/nuxt/devtools/tree/HEAD/packages/devtools)
from 1.7.0 to 2.3.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nuxt/devtools/releases"><code>@​nuxt/devtools</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v2.3.2</h2>
<h3>   🐞 Bug Fixes</h3>
<ul>
<li>Prioritize vue-devtools plugin registation, fix <a
href="https://github.com/nuxt/devtools/tree/HEAD/packages/devtools/issues/823">#823</a>,
fix <a
href="https://github.com/nuxt/devtools/tree/HEAD/packages/devtools/issues/822">#822</a>
 -  by <a href="https://github.com/antfu"><code>@​antfu</code></a> in <a
href="https://redirect.github.com/nuxt/devtools/issues/823">nuxt/devtools#823</a>
and <a
href="https://redirect.github.com/nuxt/devtools/issues/822">nuxt/devtools#822</a>
<a href="https://github.com/nuxt/devtools/commit/259853b9"><!-- raw HTML
omitted -->(25985)<!-- raw HTML omitted --></a></li>
<li>Update <code>vite-plugin-vue-tracer</code>  -  by <a
href="https://github.com/antfu"><code>@​antfu</code></a> <a
href="https://github.com/nuxt/devtools/commit/0c1740cc"><!-- raw HTML
omitted -->(0c174)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a
href="https://github.com/nuxt/devtools/compare/v2.3.1...v2.3.2">View
changes on GitHub</a></h5>
<h2>v2.3.1</h2>
<h3>   🐞 Bug Fixes</h3>
<ul>
<li>Downgrade <code>execa</code> to be compatible with Node v18, fix <a
href="https://github.com/nuxt/devtools/tree/HEAD/packages/devtools/issues/821">#821</a>
 -  by <a href="https://github.com/antfu"><code>@​antfu</code></a> in <a
href="https://redirect.github.com/nuxt/devtools/issues/821">nuxt/devtools#821</a>
<a href="https://github.com/nuxt/devtools/commit/f15c7dca"><!-- raw HTML
omitted -->(f15c7)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a
href="https://github.com/nuxt/devtools/compare/v2.3.0...v2.3.1">View
changes on GitHub</a></h5>
<h2>v2.3.0</h2>
<h3>   🚀 Features</h3>
<ul>
<li>Debug page of module mutation  -  by <a
href="https://github.com/antfu"><code>@​antfu</code></a> in <a
href="https://redirect.github.com/nuxt/devtools/issues/770">nuxt/devtools#770</a>
<a href="https://github.com/nuxt/devtools/commit/f7e9ab55"><!-- raw HTML
omitted -->(f7e9a)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a
href="https://github.com/nuxt/devtools/compare/v2.2.1...v2.3.0">View
changes on GitHub</a></h5>
<h2>v2.2.1</h2>
<h3>   🐞 Bug Fixes</h3>
<ul>
<li><strong>inspector</strong>: Do not register instapector events if
there is already any  -  by <a
href="https://github.com/antfu"><code>@​antfu</code></a> <a
href="https://github.com/nuxt/devtools/commit/db01e1b5"><!-- raw HTML
omitted -->(db01e)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a
href="https://github.com/nuxt/devtools/compare/v2.2.0...v2.2.1">View
changes on GitHub</a></h5>
<h2>v2.2.0</h2>
<h3>   🚀 Features</h3>
<ul>
<li>Migrate to <code>vite-plugin-vue-tracer</code>  -  by <a
href="https://github.com/antfu"><code>@​antfu</code></a> in <a
href="https://redirect.github.com/nuxt/devtools/issues/803">nuxt/devtools#803</a>
<a href="https://github.com/nuxt/devtools/commit/faa08d39"><!-- raw HTML
omitted -->(faa08)<!-- raw HTML omitted --></a></li>
<li>Component graph node name toogle  -  by <a
href="https://github.com/runyasak"><code>@​runyasak</code></a> and <a
href="https://github.com/antfu"><code>@​antfu</code></a> in <a
href="https://redirect.github.com/nuxt/devtools/issues/797">nuxt/devtools#797</a>
<a href="https://github.com/nuxt/devtools/commit/2eb2a37e"><!-- raw HTML
omitted -->(2eb2a)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a
href="https://github.com/nuxt/devtools/compare/v2.1.3...v2.2.0">View
changes on GitHub</a></h5>
<h2>v2.1.3</h2>
<p><em>No significant changes</em></p>
<h5>    <a
href="https://github.com/nuxt/devtools/compare/v2.1.2...v2.1.3">View
changes on GitHub</a></h5>
<h2>v2.1.2</h2>
<p><em>No significant changes</em></p>
<h5>    <a
href="https://github.com/nuxt/devtools/compare/v2.1.1...v2.1.2">View
changes on GitHub</a></h5>
<h2>v2.1.1</h2>
<h3>   🚀 Features</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nuxt/devtools/blob/main/CHANGELOG.md"><code>@​nuxt/devtools</code>'s
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/nuxt/devtools/compare/v2.3.1...v2.3.2">2.3.2</a>
(2025-03-26)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>prioritize vue-devtools plugin registation, fix <a
href="https://redirect.github.com/nuxt/devtools/issues/823">#823</a>,
fix <a
href="https://redirect.github.com/nuxt/devtools/issues/822">#822</a> (<a
href="259853b94c">259853b</a>)</li>
<li>update <code>vite-plugin-vue-tracer</code> (<a
href="0c1740cc1d">0c1740c</a>)</li>
</ul>
<h2><a
href="https://github.com/nuxt/devtools/compare/v2.3.0...v2.3.1">2.3.1</a>
(2025-03-20)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>downgrade <code>execa</code> to be compatible with Node v18, fix <a
href="https://redirect.github.com/nuxt/devtools/issues/821">#821</a> (<a
href="f15c7dca3a">f15c7dc</a>)</li>
</ul>
<h1><a
href="https://github.com/nuxt/devtools/compare/v2.2.1...v2.3.0">2.3.0</a>
(2025-03-13)</h1>
<h3>Features</h3>
<ul>
<li>debug page of module mutation (<a
href="https://redirect.github.com/nuxt/devtools/issues/770">#770</a>)
(<a
href="f7e9ab555a">f7e9ab5</a>)</li>
</ul>
<h2><a
href="https://github.com/nuxt/devtools/compare/v2.2.0...v2.2.1">2.2.1</a>
(2025-03-05)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>inspector:</strong> do not register instapector events if
there is already any (<a
href="db01e1b561">db01e1b</a>)</li>
</ul>
<h1><a
href="https://github.com/nuxt/devtools/compare/v2.1.3...v2.2.0">2.2.0</a>
(2025-03-05)</h1>
<h3>Features</h3>
<ul>
<li>component graph node name toogle (<a
href="https://redirect.github.com/nuxt/devtools/issues/797">#797</a>)
(<a
href="2eb2a37e79">2eb2a37</a>)</li>
<li>migrate to <code>vite-plugin-vue-tracer</code> (<a
href="https://redirect.github.com/nuxt/devtools/issues/803">#803</a>)
(<a
href="faa08d3949">faa08d3</a>)</li>
</ul>
<h2><a
href="https://github.com/nuxt/devtools/compare/v2.1.2...v2.1.3">2.1.3</a>
(2025-03-03)</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f919dbc5a7"><code>f919dbc</code></a>
chore: release v2.3.2</li>
<li><a
href="068173338c"><code>0681733</code></a>
Add Windsurf to the list of editors (<a
href="https://github.com/nuxt/devtools/tree/HEAD/packages/devtools/issues/820">#820</a>)</li>
<li><a
href="259853b94c"><code>259853b</code></a>
fix: prioritize vue-devtools plugin registation, fix <a
href="https://github.com/nuxt/devtools/tree/HEAD/packages/devtools/issues/823">#823</a>,
fix <a
href="https://github.com/nuxt/devtools/tree/HEAD/packages/devtools/issues/822">#822</a></li>
<li><a
href="11812abcc0"><code>11812ab</code></a>
chore: release v2.3.1</li>
<li><a
href="4e9da7fd36"><code>4e9da7f</code></a>
chore: release v2.3.0</li>
<li><a
href="f7e9ab555a"><code>f7e9ab5</code></a>
feat: debug page of module mutation (<a
href="https://github.com/nuxt/devtools/tree/HEAD/packages/devtools/issues/770">#770</a>)</li>
<li><a
href="68df38ed21"><code>68df38e</code></a>
build: only apply build output customisation to client build (<a
href="https://github.com/nuxt/devtools/tree/HEAD/packages/devtools/issues/806">#806</a>)</li>
<li><a
href="f0d804f945"><code>f0d804f</code></a>
chore: add missing type deps</li>
<li><a
href="a6448d95d6"><code>a6448d9</code></a>
chore: use named catalogs</li>
<li><a
href="eda0e673b4"><code>eda0e67</code></a>
chore: release v2.2.1</li>
<li>Additional commits viewable in <a
href="https://github.com/nuxt/devtools/commits/v2.3.2/packages/devtools">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@nuxt/devtools&package-manager=npm_and_yarn&previous-version=1.7.0&new-version=2.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-12 13:47:07 +03:00
Wanjohi
6b1521d7d4 feat(dependabot): Put a leash on it (#231)
## Description
This attempts to limit the dependabot alerts to a week, plus make sure
it works on all the projects

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Introduced an automated dependency update configuration to help keep
all package ecosystems current.
- **Bug Fixes**
- Adjusted the email sender address configuration to ensure that
outgoing communications display the intended sender details.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-12 13:41:42 +03:00
dependabot[bot]
39e187832a build(deps): bump the npm_and_yarn group across 3 directories with 2 updates (#229)
Bumps the npm_and_yarn group with 1 update in the /apps/docs directory:
[koa](https://github.com/koajs/koa).
Bumps the npm_and_yarn group with 1 update in the /apps/www directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /packages/www
directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

Updates `koa` from 2.15.4 to 2.16.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/koajs/koa/releases">koa's
releases</a>.</em></p>
<blockquote>
<h2>v2.16.1</h2>
<p>fix: don't render redirect values in anchor ref</p>
<h2>2.16.0</h2>
<p>This is a backported release to fix core underlying issue with
<code>HEAD</code> requests when using
<code>http2.createSecureServer</code>. See discussion at <a
href="https://redirect.github.com/koajs/koa/pull/1593">koajs/koa#1593</a>
and <a
href="https://redirect.github.com/koajs/koa/issues/1547">koajs/koa#1547</a>.</p>
<ul>
<li>fix missing cleanup, if response socket is no longer writeable
(issue 1547) (<a
href="https://redirect.github.com/koajs/koa/pull/1593">koajs/koa#1593</a>)
399cb6b0dd2104224c0ef0ce8e92f84e4f7faf42</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ba14822069"><code>ba14822</code></a>
2.16.1</li>
<li><a
href="2ff6c3fb80"><code>2ff6c3f</code></a>
2.16.0</li>
<li><a
href="3d51d034af"><code>3d51d03</code></a>
ci: allow codecov to fail</li>
<li><a
href="eb84d890b8"><code>eb84d89</code></a>
fix: don't render redirect values in anchor ref</li>
<li>See full diff in <a
href="https://github.com/koajs/koa/compare/2.15.4...v2.16.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `vite` from 5.4.16 to 6.0.14
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.14</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.14/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.13</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.13/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.12</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.12/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.11</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.11/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.10</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.10/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.9</h2>
<p>This version contains a breaking change due to security fixes. See <a
href="https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6">https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6</a>
for more details.</p>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.9/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.8</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.8/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.7</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.7/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.6</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.6/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.5</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.5/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.4</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.4/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.3</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.3/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.2</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.2/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>plugin-legacy@6.0.2</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/plugin-legacy@6.0.2/packages/plugin-legacy/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>create-vite@6.0.1</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/create-vite@6.0.1/packages/create-vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.1</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.1/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v6.0.14/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->6.0.14 (2025-04-03)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths (<a
href="48ee91df38">48ee91d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19782">#19782</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.13 (2025-03-31)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)
(<a
href="1487f393f3">1487f39</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19761">#19761</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.12 (2025-03-24)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs raw query with query separators (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19702">#19702</a>)
(<a
href="92ca12dc79">92ca12d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19702">#19702</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.11 (2025-01-21)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: <code>preview.allowedHosts</code> with specific values was not
respected (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19246">#19246</a>)
(<a
href="aeb3ec84a2">aeb3ec8</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19246">#19246</a></li>
<li>fix: allow CORS from loopback addresses by default (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19249">#19249</a>)
(<a
href="3d03899737">3d03899</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19249">#19249</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.10 (2025-01-20)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: try parse <code>server.origin</code> URL (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19241">#19241</a>)
(<a
href="2495022420">2495022</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19241">#19241</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.9 (2025-01-20)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix!: check host header to prevent DNS rebinding attacks and
introduce <code>server.allowedHosts</code> (<a
href="bd896fb5f3">bd896fb</a>)</li>
<li>fix!: default <code>server.cors: false</code> to disallow fetching
from untrusted origins (<a
href="b09572acc9">b09572a</a>)</li>
<li>fix: verify token for HMR WebSocket connection (<a
href="029dcd6d77">029dcd6</a>)</li>
</ul>
<h2><!-- raw HTML omitted -->6.0.8 (2025-01-20)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: avoid SSR HMR for HTML files (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19193">#19193</a>)
(<a
href="3bd55bcb7e">3bd55bc</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19193">#19193</a></li>
<li>fix: build time display 7m 60s (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19108">#19108</a>)
(<a
href="cf0d2c8e23">cf0d2c8</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19108">#19108</a></li>
<li>fix: don't resolve URL starting with double slash (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19059">#19059</a>)
(<a
href="35942cde11">35942cd</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19059">#19059</a></li>
<li>fix: ensure <code>server.close()</code> only called once (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19204">#19204</a>)
(<a
href="db81c2dada">db81c2d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19204">#19204</a></li>
<li>fix: resolve.conditions in ResolvedConfig was
<code>defaultServerConditions</code> (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19174">#19174</a>)
(<a
href="ad75c56dce">ad75c56</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19174">#19174</a></li>
<li>fix: tree shake stringified JSON imports (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19189">#19189</a>)
(<a
href="f2aed62d0b">f2aed62</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19189">#19189</a></li>
<li>fix: use shared sigterm callback (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19203">#19203</a>)
(<a
href="47039f4643">47039f4</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19203">#19203</a></li>
<li>fix(deps): update all non-major dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19098">#19098</a>)
(<a
href="8639538e64">8639538</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19098">#19098</a></li>
<li>fix(optimizer): use correct default install state path for yarn PnP
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19119">#19119</a>)
(<a
href="e690d8bb1e">e690d8b</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19119">#19119</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f678baacaf"><code>f678baa</code></a>
release: v6.0.14</li>
<li><a
href="48ee91df38"><code>48ee91d</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths</li>
<li><a
href="0eaadcf952"><code>0eaadcf</code></a>
release: v6.0.13</li>
<li><a
href="1487f393f3"><code>1487f39</code></a>
fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)</li>
<li><a
href="9d981f9d38"><code>9d981f9</code></a>
release: v6.0.12</li>
<li><a
href="92ca12dc79"><code>92ca12d</code></a>
fix: fs raw query with query separators (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19702">#19702</a>)</li>
<li><a
href="a0ed4057c9"><code>a0ed405</code></a>
release: v6.0.11</li>
<li><a
href="3d03899737"><code>3d03899</code></a>
fix: allow CORS from loopback addresses by default (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19249">#19249</a>)</li>
<li><a
href="aeb3ec84a2"><code>aeb3ec8</code></a>
fix: <code>preview.allowedHosts</code> with specific values was not
respected (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19246">#19246</a>)</li>
<li><a
href="9654348258"><code>9654348</code></a>
release: v6.0.10</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v6.0.14/packages/vite">compare
view</a></li>
</ul>
</details>
<br />

Updates `vite` from 5.4.16 to 6.0.14
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.14</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.14/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.13</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.13/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.12</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.12/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.11</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.11/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.10</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.10/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.9</h2>
<p>This version contains a breaking change due to security fixes. See <a
href="https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6">https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6</a>
for more details.</p>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.9/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.8</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.8/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.7</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.7/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.6</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.6/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.5</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.5/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.4</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.4/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.3</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.3/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.2</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.2/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>plugin-legacy@6.0.2</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/plugin-legacy@6.0.2/packages/plugin-legacy/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>create-vite@6.0.1</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/create-vite@6.0.1/packages/create-vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.0.1</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.0.1/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v6.0.14/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->6.0.14 (2025-04-03)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths (<a
href="48ee91df38">48ee91d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19782">#19782</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.13 (2025-03-31)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)
(<a
href="1487f393f3">1487f39</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19761">#19761</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.12 (2025-03-24)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs raw query with query separators (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19702">#19702</a>)
(<a
href="92ca12dc79">92ca12d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19702">#19702</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.11 (2025-01-21)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: <code>preview.allowedHosts</code> with specific values was not
respected (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19246">#19246</a>)
(<a
href="aeb3ec84a2">aeb3ec8</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19246">#19246</a></li>
<li>fix: allow CORS from loopback addresses by default (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19249">#19249</a>)
(<a
href="3d03899737">3d03899</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19249">#19249</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.10 (2025-01-20)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: try parse <code>server.origin</code> URL (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19241">#19241</a>)
(<a
href="2495022420">2495022</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19241">#19241</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.0.9 (2025-01-20)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix!: check host header to prevent DNS rebinding attacks and
introduce <code>server.allowedHosts</code> (<a
href="bd896fb5f3">bd896fb</a>)</li>
<li>fix!: default <code>server.cors: false</code> to disallow fetching
from untrusted origins (<a
href="b09572acc9">b09572a</a>)</li>
<li>fix: verify token for HMR WebSocket connection (<a
href="029dcd6d77">029dcd6</a>)</li>
</ul>
<h2><!-- raw HTML omitted -->6.0.8 (2025-01-20)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: avoid SSR HMR for HTML files (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19193">#19193</a>)
(<a
href="3bd55bcb7e">3bd55bc</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19193">#19193</a></li>
<li>fix: build time display 7m 60s (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19108">#19108</a>)
(<a
href="cf0d2c8e23">cf0d2c8</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19108">#19108</a></li>
<li>fix: don't resolve URL starting with double slash (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19059">#19059</a>)
(<a
href="35942cde11">35942cd</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19059">#19059</a></li>
<li>fix: ensure <code>server.close()</code> only called once (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19204">#19204</a>)
(<a
href="db81c2dada">db81c2d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19204">#19204</a></li>
<li>fix: resolve.conditions in ResolvedConfig was
<code>defaultServerConditions</code> (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19174">#19174</a>)
(<a
href="ad75c56dce">ad75c56</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19174">#19174</a></li>
<li>fix: tree shake stringified JSON imports (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19189">#19189</a>)
(<a
href="f2aed62d0b">f2aed62</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19189">#19189</a></li>
<li>fix: use shared sigterm callback (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19203">#19203</a>)
(<a
href="47039f4643">47039f4</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19203">#19203</a></li>
<li>fix(deps): update all non-major dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19098">#19098</a>)
(<a
href="8639538e64">8639538</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19098">#19098</a></li>
<li>fix(optimizer): use correct default install state path for yarn PnP
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19119">#19119</a>)
(<a
href="e690d8bb1e">e690d8b</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19119">#19119</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f678baacaf"><code>f678baa</code></a>
release: v6.0.14</li>
<li><a
href="48ee91df38"><code>48ee91d</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths</li>
<li><a
href="0eaadcf952"><code>0eaadcf</code></a>
release: v6.0.13</li>
<li><a
href="1487f393f3"><code>1487f39</code></a>
fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)</li>
<li><a
href="9d981f9d38"><code>9d981f9</code></a>
release: v6.0.12</li>
<li><a
href="92ca12dc79"><code>92ca12d</code></a>
fix: fs raw query with query separators (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19702">#19702</a>)</li>
<li><a
href="a0ed4057c9"><code>a0ed405</code></a>
release: v6.0.11</li>
<li><a
href="3d03899737"><code>3d03899</code></a>
fix: allow CORS from loopback addresses by default (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19249">#19249</a>)</li>
<li><a
href="aeb3ec84a2"><code>aeb3ec8</code></a>
fix: <code>preview.allowedHosts</code> with specific values was not
respected (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19246">#19246</a>)</li>
<li><a
href="9654348258"><code>9654348</code></a>
release: v6.0.10</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v6.0.14/packages/vite">compare
view</a></li>
</ul>
</details>
<br />

<details>
<summary>Most Recent Ignore Conditions Applied to This Pull
Request</summary>

| Dependency Name | Ignore Conditions |
| --- | --- |
| vite | [< 5.5, > 5.4.16] |
</details>


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/nestrilabs/nestri/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Wanjohi <elviswanjohi47@gmail.com>
2025-04-12 13:03:10 +03:00
Wanjohi
de80f3e6ab feat(maitred): Update maitred - hookup to the API (#198)
## Description
We are attempting to hookup maitred to the API
Maitred duties will be:
- [ ] Hookup to the API
- [ ]  Wait for signal (from the API) to start Steam
- [ ] Stop signal to stop the gaming session, clean up Steam... and
maybe do the backup

## Summary by CodeRabbit

- **New Features**
- Introduced Docker-based deployment configurations for both the main
and relay applications.
- Added new API endpoints enabling real-time machine messaging and
enhanced IoT operations.
- Expanded database schema and actor types to support improved machine
tracking.

- **Improvements**
- Enhanced real-time communication and relay management with streamlined
room handling.
- Upgraded dependencies, logging, and error handling for greater
stability and performance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Kristian Ollikainen <14197772+DatCaptainHorse@users.noreply.github.com>
2025-04-07 23:23:53 +03:00
Wanjohi
6990494b34 🐜 fix(infra): Fix the Web path 2025-04-07 23:14:22 +03:00
Wanjohi
5a3fdf25ff 🧹 chore(infra): Upgrade to sst v3.11.21 2025-04-07 22:45:58 +03:00
dependabot[bot]
18b14a4261 build(deps): bump the npm_and_yarn group across 3 directories with 1 update (#224)
Bumps the npm_and_yarn group with 1 update in the /apps/docs directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /apps/www directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /packages/www
directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

Updates `vite` from 6.2.3 to 6.2.5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v6.2.5</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.2.4</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.4/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->6.2.5 (2025-04-03)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths (<a
href="fdb196e9f8">fdb196e</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19782">#19782</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.2.4 (2025-03-31)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)
(<a
href="7a4fabab6a">7a4faba</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19761">#19761</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c176acf70a"><code>c176acf</code></a>
release: v6.2.5</li>
<li><a
href="fdb196e9f8"><code>fdb196e</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths</li>
<li><a
href="037f801075"><code>037f801</code></a>
release: v6.2.4</li>
<li><a
href="7a4fabab6a"><code>7a4faba</code></a>
fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v6.2.5/packages/vite">compare
view</a></li>
</ul>
</details>
<br />

Updates `vite` from 5.4.12 to 5.4.16
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v6.2.5</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.2.4</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.4/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->6.2.5 (2025-04-03)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths (<a
href="fdb196e9f8">fdb196e</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19782">#19782</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.2.4 (2025-03-31)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)
(<a
href="7a4fabab6a">7a4faba</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19761">#19761</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c176acf70a"><code>c176acf</code></a>
release: v6.2.5</li>
<li><a
href="fdb196e9f8"><code>fdb196e</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths</li>
<li><a
href="037f801075"><code>037f801</code></a>
release: v6.2.4</li>
<li><a
href="7a4fabab6a"><code>7a4faba</code></a>
fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v6.2.5/packages/vite">compare
view</a></li>
</ul>
</details>
<br />

Updates `vite` from 5.4.12 to 5.4.16
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v6.2.5</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v6.2.4</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.4/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->6.2.5 (2025-04-03)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths (<a
href="fdb196e9f8">fdb196e</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19782">#19782</a></li>
</ul>
<h2><!-- raw HTML omitted -->6.2.4 (2025-03-31)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)
(<a
href="7a4fabab6a">7a4faba</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19761">#19761</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c176acf70a"><code>c176acf</code></a>
release: v6.2.5</li>
<li><a
href="fdb196e9f8"><code>fdb196e</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>,
fs check with svg and relative paths</li>
<li><a
href="037f801075"><code>037f801</code></a>
release: v6.2.4</li>
<li><a
href="7a4fabab6a"><code>7a4faba</code></a>
fix: fs check in transform middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v6.2.5/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/nestrilabs/nestri/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 18:50:19 +03:00
dependabot[bot]
f4aa2ca4a4 build(deps): bump vite from 6.2.2 to 6.2.3 in /apps/docs in the npm_and_yarn group across 1 directory (#213)
Bumps the npm_and_yarn group with 1 update in the /apps/docs directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

Updates `vite` from 6.2.2 to 6.2.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v6.2.3</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v6.2.3/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v6.2.3/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->6.2.3 (2025-03-24)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs raw query with query separators (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19702">#19702</a>)
(<a
href="f234b5744d">f234b57</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19702">#19702</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="16869d7c99"><code>16869d7</code></a>
release: v6.2.3</li>
<li><a
href="f234b5744d"><code>f234b57</code></a>
fix: fs raw query with query separators (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19702">#19702</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v6.2.3/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=6.2.2&new-version=6.2.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/nestrilabs/nestri/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Wanjohi <71614375+wanjohiryan@users.noreply.github.com>
2025-04-03 18:44:46 +03:00
Wanjohi
6092c4e4f8 🐜 fix(www): Change CTA for now (#223)
## Description
Change the CTA for the landing page
2025-04-03 18:29:21 +03:00
Wanjohi
f3d7ea2663 🧹 chore: Use simpler PR template (#220)
## Description
Use a simpler template
2025-03-26 08:01:50 +03:00
Wanjohi
7ff4ff8c90 🐜 fix(infra): Make sure all stages are in sync (#219)
## Description
Syncs all branches to the current changes
2025-03-26 07:59:40 +03:00
Wanjohi
7ecc068466 feat(infra): Use a shared VPC (#218)
## Description
The scope of this PR is to add a shared VPC for everyone on the team.
2025-03-26 06:29:25 +03:00
Wanjohi
633b332700 🏇🏾ci(docs): Deploy docs to cloudflare (#217)
## Description
This deploys docs to cloudflare pages

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [ ] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [ ] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [ ] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->

## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
2025-03-26 04:03:17 +03:00
Wanjohi
cacdae79c0 🐜 fix(ci): Remove unnecessary code 2025-03-26 03:35:16 +03:00
Wanjohi
ca4432bcde 🐜 fix(ci): Fix issue where main does not deploy to prod (#216)
## Description
Fix an issue where the `apps/www` CI does not push to main

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [x] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [ ] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [ ] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->

## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
2025-03-26 03:31:09 +03:00
Wanjohi
261a1276f5 🏇 ci(www): Test whether it is working (#215)
## Description
This fixes issues with the `apps/www` ci
## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [x] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [ ] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [ ] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->

## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
2025-03-26 03:20:40 +03:00
Wanjohi
a45b2bf9b7 feat(ci): Deploy nestri landing page (#214)
## Description
This attempts to deploy `apps/www` to the new CF account using
cloudflare pages project

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [ ] Bug fix (non-breaking change)
- [x] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [ ] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [ ] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->

## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
2025-03-26 03:04:28 +03:00
Wanjohi
f62fc1fb4b feat(www): Finish up on the onboarding (#210)
Merging this prematurely to make sure the team is on the same boat... like dang! We need to find a better way to do this. 

Plus it has become too big
2025-03-26 02:21:53 +03:00
dependabot[bot]
957eca7794 build(deps-dev): bump nuxt from 3.15.4 to 3.16.1 in /apps/docs in the npm_and_yarn group across 1 directory (#211)
Bumps the npm_and_yarn group with 1 update in the /apps/docs directory:
[nuxt](https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt).

Updates `nuxt` from 3.15.4 to 3.16.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nuxt/nuxt/releases">nuxt's
releases</a>.</em></p>
<blockquote>
<h2>v3.16.1</h2>
<p><a
href="https://github.com/nuxt/nuxt/compare/v3.16.0...v3.16.1">compare
changes</a></p>
<h3>🔥 Performance</h3>
<ul>
<li><strong>nuxt:</strong> Use browser cache for payloads (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31379">#31379</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>nuxt:</strong> Restore nuxt aliases to nitro
compilerOptions.paths (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31278">#31278</a>)</li>
<li><strong>nuxt:</strong> Use new <code>mocked-exports</code> (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31295">#31295</a>)</li>
<li><strong>nuxt:</strong> Check resolved options for polyfills (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31307">#31307</a>)</li>
<li><strong>nuxt:</strong> Render style component html (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31337">#31337</a>)</li>
<li><strong>nuxt:</strong> Add missing lazy hydration prop in regex (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31359">#31359</a>)</li>
<li><strong>nuxt:</strong> Fully resolve nuxt dependencies (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31436">#31436</a>)</li>
<li><strong>vite:</strong> Don't show interim vite build output files
(<a
href="https://redirect.github.com/nuxt/nuxt/pull/31439">#31439</a>)</li>
<li><strong>nuxt:</strong> Ignore prerendering unprefixed public assets
(<a
href="https://github.com/nuxt/nuxt/commit/151912ec3">151912ec3</a>)</li>
<li><strong>nuxt:</strong> Use more performant router catchall pattern
(<a
href="https://redirect.github.com/nuxt/nuxt/pull/31450">#31450</a>)</li>
<li><strong>nuxt:</strong> Prevent param duplication in
<code>typedPages</code> implementation (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31331">#31331</a>)</li>
<li><strong>nuxt:</strong> Sort route paths before creating route tree
(<a
href="https://redirect.github.com/nuxt/nuxt/pull/31454">#31454</a>)</li>
</ul>
<h3>📖 Documentation</h3>
<ul>
<li>Update link to vercel edge network (<a
href="https://github.com/nuxt/nuxt/commit/ec20802a5">ec20802a5</a>)</li>
<li>Improve HMR performance note for Windows users (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31301">#31301</a>)</li>
<li>Correct WSL note phrasing (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31322">#31322</a>)</li>
<li>Fix typo (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31341">#31341</a>)</li>
<li>Adjust <code>app.head</code> example (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31350">#31350</a>)</li>
<li>Include package manager options in update script (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31346">#31346</a>)</li>
<li>Add missing comma (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31362">#31362</a>)</li>
<li>Add mention of <code>addServerTemplate</code> to modules guide (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31369">#31369</a>)</li>
<li>Add <code>rspack</code> and remove <code>test-utils</code> for
monorepo guide (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31371">#31371</a>)</li>
<li>Adjust example (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31372">#31372</a>)</li>
<li>Update experimental docs (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31388">#31388</a>)</li>
<li>Use <code>ini</code> syntax block highlighting for <code>.env</code>
files (<a
href="https://github.com/nuxt/nuxt/commit/f79fabe46">f79fabe46</a>)</li>
<li>Improve <code>useHydration</code> docs (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31427">#31427</a>)</li>
<li>Update broken docs links (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31430">#31430</a>)</li>
<li>Mention possibility of prerendering api routes (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31234">#31234</a>)</li>
</ul>
<h3>🏡 Chore</h3>
<ul>
<li>Fix gitignore (<a
href="https://github.com/nuxt/nuxt/commit/6fe9dff7e">6fe9dff7e</a>)</li>
<li>Bump axios dependency in lockfile (<a
href="https://github.com/nuxt/nuxt/commit/c3352e80b">c3352e80b</a>)</li>
<li>Lint repo (<a
href="https://github.com/nuxt/nuxt/commit/2ab20bfdc">2ab20bfdc</a>)</li>
<li>Add scorecard badge (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31302">#31302</a>)</li>
<li>Dedupe (<a
href="https://github.com/nuxt/nuxt/commit/be5d85f2b">be5d85f2b</a>)</li>
</ul>
<h3> Tests</h3>
<ul>
<li>Migrate runtime compiler test to playwright (+ add test cases) (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31405">#31405</a>)</li>
<li>Migrate spa preloader tests to playwright (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31273">#31273</a>)</li>
<li>Use <code>srvx</code> and random port for remote provider (<a
href="https://redirect.github.com/nuxt/nuxt/pull/31432">#31432</a>)</li>
</ul>
<h3>🤖 CI</h3>
<ul>
<li>Automate release on merge of of v3/v4 (<a
href="https://github.com/nuxt/nuxt/commit/6ae5b5fdb">6ae5b5fdb</a>)</li>
<li>Fix workflow quoting (<a
href="https://github.com/nuxt/nuxt/commit/fef39cf3c">fef39cf3c</a>)</li>
</ul>
<h3>❤️ Contributors</h3>
<ul>
<li>Daniel Roe (<a
href="https://github.com/danielroe"><code>@​danielroe</code></a>)</li>
<li>Anoesj Sadraee (<a
href="https://github.com/Anoesj"><code>@​Anoesj</code></a>)</li>
<li>Peter Radko (<a
href="https://github.com/Gwynerva"><code>@​Gwynerva</code></a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="cd38de24c1"><code>cd38de2</code></a>
v3.16.1</li>
<li><a
href="ea9025ea01"><code>ea9025e</code></a>
fix(nuxt): sort route paths before creating route tree (<a
href="https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt/issues/31454">#31454</a>)</li>
<li><a
href="7bbc794b19"><code>7bbc794</code></a>
fix(nuxt): prevent param duplication in <code>typedPages</code>
implementation (<a
href="https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt/issues/31331">#31331</a>)</li>
<li><a
href="10a4f8ee6b"><code>10a4f8e</code></a>
fix(nuxt): use more performant router catchall pattern (<a
href="https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt/issues/31450">#31450</a>)</li>
<li><a
href="502720f4c8"><code>502720f</code></a>
chore(deps): update all non-major dependencies (3.x) (<a
href="https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt/issues/31452">#31452</a>)</li>
<li><a
href="151912ec38"><code>151912e</code></a>
fix(nuxt): ignore prerendering unprefixed public assets</li>
<li><a
href="a752be862a"><code>a752be8</code></a>
docs: update broken docs links (<a
href="https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt/issues/31430">#31430</a>)</li>
<li><a
href="19ac2e76ff"><code>19ac2e7</code></a>
fix(nuxt): fully resolve nuxt dependencies (<a
href="https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt/issues/31436">#31436</a>)</li>
<li><a
href="9374ceb420"><code>9374ceb</code></a>
chore(deps): update devdependency nitropack to v2.11.7 (3.x) (<a
href="https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt/issues/31441">#31441</a>)</li>
<li><a
href="e26dd61e2e"><code>e26dd61</code></a>
chore(deps): update all non-major dependencies (3.x) (<a
href="https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt/issues/31421">#31421</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/nuxt/nuxt/commits/v3.16.1/packages/nuxt">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nuxt&package-manager=npm_and_yarn&previous-version=3.15.4&new-version=3.16.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/nestrilabs/nestri/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 10:05:48 +03:00
Wanjohi
74f8208fa4 🐜 fix: Fix dynamodb errors when trying to authenticate (#208)
## Description
This fixes an issue where `openauthjs` throws timeout errors while
attempting to access DynamoDB
> I am so glad this was not a major issue 😅
## Related Issues
[OpenAuth Error TypeError: fetch failed while accessing DynamoDB on AWS
#5543
](https://github.com/sst/sst/issues/5543)
[Error TypeError: fetch failed while accessing DynamoDB on AWS
#220](https://github.com/toolbeam/openauth/issues/220)
## Type of Change

- [x] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [x] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [ ] My changes generate no new warnings/errors

## Screenshots/Demo

![image](https://github.com/user-attachments/assets/03656741-4e74-4947-8a15-333f5cf19dc8)
2025-03-10 04:43:14 +03:00
Wanjohi
b251584ccb 🧹 chore(www): Update the onboarding (#207)
## Description
<!-- Briefly describe the purpose and scope of your changes -->

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [x] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [ ] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [ ] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->

## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
2025-03-10 04:06:03 +03:00
dependabot[bot]
15825c70e6 build(deps): bump ring from 0.17.11 to 0.17.13 in /packages/server in the cargo group across 1 directory (#205)
Bumps the cargo group with 1 update in the /packages/server directory:
[ring](https://github.com/briansmith/ring).

Updates `ring` from 0.17.11 to 0.17.13
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/briansmith/ring/blob/main/RELEASES.md">ring's
changelog</a>.</em></p>
<blockquote>
<h1>Version 0.17.13 (2025-03-06)</h1>
<p>Increased MSRV to 1.66.0 to avoid bugs in earlier versions so that we
can
safely use <code>core::arch::x86_64::__cpuid</code> and
<code>core::arch::x86::__cpuid</code> from
Rust in future releases.</p>
<p>AVX2-based VAES-CLMUL implementation. This will be a notable
performance
improvement for most newish x86-64 systems. This will likely raise the
minimum
binutils version supported for very old Linux distros.</p>
<h1>Version 0.17.12 (2025-03-05)</h1>
<p>Bug fix: <a
href="https://redirect.github.com/briansmith/ring/pull/2447">briansmith/ring#2447</a>
for denial of service (DoS).</p>
<ul>
<li>
<p>Fixes a panic in
<code>ring::aead::quic::HeaderProtectionKey::new_mask()</code> when
integer overflow checking is enabled. In the QUIC protocol, an attacker
can
induce this panic by sending a specially-crafted packet. Even
unintentionally
it is likely to occur in 1 out of every 2**32 packets sent and/or
received.</p>
</li>
<li>
<p>Fixes a panic on 64-bit targets in <code>ring::aead::{AES_128_GCM,
AES_256_GCM}</code>
when overflow checking is enabled, when encrypting/decrypting
approximately
68,719,476,700 bytes (about 64 gigabytes) of data in a single chunk.
Protocols
like TLS and SSH are not affected by this because those protocols break
large
amounts of data into small chunks. Similarly, most applications will not
attempt to encrypt/decrypt 64GB of data in one chunk.</p>
</li>
</ul>
<p>Overflow checking is not enabled in release mode by default, but
<code>RUSTFLAGS=&quot;-C overflow-checks&quot;</code> or
<code>overflow-checks = true</code> in the Cargo.toml
profile can override this. Overflow checking is usually enabled by
default in
debug mode.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/briansmith/ring/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ring&package-manager=cargo&previous-version=0.17.11&new-version=0.17.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/nestrilabs/nestri/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-08 10:42:21 +02:00
Wanjohi
117503081b feat(www): Finish up on the onboarding (#202)
## Description
This is an attempt to finish up on the onboarding and creating a team

## Type of Change

- [ ] Bug fix (non-breaking change)
- [x] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [ ] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors
2025-03-05 22:44:00 +03:00
Wanjohi
1aeafec40b 🐜 fix(db): Migrate to Neon org account (#204)
## Description
Migrates to a neon account shared by the Nestrilabs organisation

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [ ] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [ ] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [ ] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->

## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
2025-03-03 14:59:48 +03:00
Wanjohi
9ab4b6580c 🐜 fix(db): Fix database state issues (#203)
## Description
This attempts to fix all database issues for all teammates

## Related Issues
1. Getting funny errors while trying to run `sst dev`
2. The neon pulumi stuff trying to create a db each time on push

## Type of Change

- [x] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [ ] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [x] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->

## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
2025-03-03 13:36:13 +03:00
Kristian Ollikainen
49853807a1 feat(relay): Port muxing and TLS (#197)
## Description
This PR will work on adding port muxing (share single port for HTTP/WS +
WebRTC connections), along with API communication.

## Type of Change

- [x] Bug fix (non-breaking change)
- [x] New feature (non-breaking change)

## Checklist

- [ ] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
2025-03-02 18:47:25 +03:00
Wanjohi
321dda60d9 🐜 fix(www): Error Missing "./400.css" specifier in "@fontsource/geist-mono" (#201)
## Description
Attempts to fix the build issue (again) where the non-variable fonts
from font-source are throwing `Missing "./*00.css" specifier in
"@fontsource/geist-*` while building or developing locally... like damn!

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [x] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [x] I have updated relevant documentation
- [x] My code follows the project's coding style
- [ ] My changes generate no new warnings/errors
2025-03-02 14:54:20 +03:00
Wanjohi
fb47bb6699 🐜 fix: Remove old.sst.config.ts file 2025-03-02 01:47:00 +03:00
Wanjohi
849a470073 🐜 fix(infra): Share the same Neon project 2025-03-02 01:30:57 +03:00
Wanjohi
178c612f0f 🐜 fix: Make sure the db uses the same name (#199)
## Description
This fixes the issue where Cloudflare fails

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [x] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [x] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors
2025-03-02 00:01:25 +03:00
Kristian Ollikainen
b18b08b822 feat(runner): Rust updates and improvements (#196)
## Description
- Updates to latest Rust 2024 🎉
- Make DataChannel messages ordered on nestri-server side
- Bugfixes and code improvements + formatting

## Type of Change

- [x] Bug fix (non-breaking change)
- [x] New feature (non-breaking change)

## Checklist

- [x] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Wanjohi <elviswanjohi47@gmail.com>
2025-03-01 21:57:54 +02:00
dependabot[bot]
ea96fed4f6 build(deps-dev): bump vite from 5.4.10 to 5.4.12 in /packages/www in the npm_and_yarn group across 1 directory (#195)
Bumps the npm_and_yarn group with 1 update in the /packages/www
directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

Updates `vite` from 5.4.10 to 5.4.12
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v5.4.12</h2>
<p>This version contains a breaking change due to security fixes. See <a
href="https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6">https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6</a>
for more details.</p>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v5.4.12/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v5.4.11</h2>
<p>Please refer to <a
href="ecd2375460/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v5.4.12/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.4.12 (2025-01-20)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix!: check host header to prevent DNS rebinding attacks and
introduce <code>server.allowedHosts</code> (<a
href="9da4abc8dd">9da4abc</a>)</li>
<li>fix!: default <code>server.cors: false</code> to disallow fetching
from untrusted origins (<a
href="dfea38f1ff">dfea38f</a>)</li>
<li>fix: verify token for HMR WebSocket connection (<a
href="b71a5c89a1">b71a5c8</a>)</li>
<li>chore: add deps update changelog (<a
href="ecd2375460">ecd2375</a>)</li>
</ul>
<h2><!-- raw HTML omitted -->5.4.11 (2024-11-11)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix(deps): update dependencies of postcss-modules (<a
href="ceb15db613">ceb15db</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/18617">#18617</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f428aa9af8"><code>f428aa9</code></a>
release: v5.4.12</li>
<li><a
href="9da4abc8dd"><code>9da4abc</code></a>
fix!: check host header to prevent DNS rebinding attacks and introduce
`serve...</li>
<li><a
href="b71a5c89a1"><code>b71a5c8</code></a>
fix: verify token for HMR WebSocket connection</li>
<li><a
href="dfea38f1ff"><code>dfea38f</code></a>
fix!: default <code>server.cors: false</code> to disallow fetching from
untrusted origins</li>
<li><a
href="ecd2375460"><code>ecd2375</code></a>
chore: add deps update changelog</li>
<li><a
href="c54c860f9d"><code>c54c860</code></a>
release: v5.4.11</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v5.4.12/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.4.10&new-version=5.4.12)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/nestrilabs/nestri/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-27 21:41:21 +03:00
Wanjohi
457aac2258 feat(infra): Update infra and add support for teams to SST (#186)
## Description
- [x] Adds support for AWS SSO, which makes us (the team) able to use
SST and update the components independently
- [x] Splits the webpage into the landing page (Qwik), and Astro (the
console) in charge of playing. This allows us to pass in Environment
Variables to the console
- ~Migrates the docs from Nuxt to Nextjs, and connects them to SST. This
allows us to use Fumadocs _citation needed_ that's much more beautiful,
and supports OpenApi~
- Cloudflare pages with github integration is not working on our new CF
account. So we will have to push the pages deployment manually with
Github actions
- [x] Moves the current set up from my personal CF and AWS accounts to
dedicated Nestri accounts -

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [ ] Bug fix (non-breaking change)
- [x] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [x] Documentation update
- [ ] Other (please describe):

## Checklist

- [x] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->
Please approve my PR 🥹


## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
2025-02-27 18:52:05 +03:00
Kristian Ollikainen
237e016b2d 🐜 fix(runner): Workarounds for NVIDIA drivers (#188)
#### Description

This PR will be fixing issue of runner not working under Ubuntu and
other Debian-based distros with NVIDIA GPUs.
Another fix will be arriving is allowing Steam to run without namespace
requirements, removing the need for `--privileged` flag in certain
situations.

#### Type of Change

- [x] Bug fix (non-breaking change)

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
2025-02-27 17:37:23 +02:00
318 changed files with 41184 additions and 9076 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
CLOUDFLARE_API_TOKEN=
NEON_API_KEY=

View File

@@ -1,28 +1,2 @@
## Description
<!-- Briefly describe the purpose and scope of your changes -->
## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->
## Type of Change
- [ ] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing functionality)
- [ ] Documentation update
- [ ] Other (please describe):
## Checklist
- [ ] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [ ] My changes generate no new warnings/errors
## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you have, or decisions that need discussion -->
## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes (especially for UI changes) -->
## Additional Context
<!-- Add any other context about the pull request here -->

40
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Build docs
on:
pull_request:
paths:
- "apps/docs/**"
- ".github/workflows/docs.yml"
push:
branches: [main]
paths:
- "apps/docs/**"
- ".github/workflows/docs.yml"
jobs:
deploy-docs:
name: Build and deploy docs
runs-on: ubuntu-latest
defaults:
run:
working-directory: "apps/docs"
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Build Project Artifacts
run: bun run build
- name: Deploy Project Artifacts to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
packageManager: bun
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "3.93.0"
workingDirectory: "apps/docs"
command: pages deploy ./dist --project-name=${{ vars.CF_DOCS_PAGES_PROJECT_NAME }} --commit-dirty=true
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

View File

@@ -7,6 +7,7 @@ on:
paths:
- "containers/runner.Containerfile"
- "packages/scripts/**"
- "packages/server/**"
- ".github/workflows/runner.yml"
schedule:
- cron: 7 0 * * 1,3,6 # Regularly to keep that build cache warm
@@ -16,6 +17,7 @@ on:
- "containers/runner.Containerfile"
- ".github/workflows/runner.yml"
- "packages/scripts/**"
- "packages/server/**"
tags:
- v*.*.*
release:
@@ -25,6 +27,7 @@ env:
REGISTRY: ghcr.io
IMAGE_NAME: nestrilabs/nestri
BASE_TAG_PREFIX: runner
BASE_IMAGE: docker.io/cachyos/cachyos:latest
# This makes our release ci quit prematurely
# concurrency:
@@ -53,7 +56,7 @@ jobs:
swap-size-gb: 20
-
name: Build Docker image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
file: containers/runner.Containerfile
context: ./
@@ -105,7 +108,7 @@ jobs:
swap-size-gb: 20
-
name: Build Docker image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
file: containers/runner.Containerfile
context: ./
@@ -114,3 +117,4 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
pull: ${{ github.event_name == 'schedule' }} # Pull base image for scheduled builds

40
.github/workflows/www.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Build www
on:
pull_request:
paths:
- "apps/www/**"
- ".github/workflows/www.yml"
push:
branches: [main]
paths:
- "apps/www/**"
- ".github/workflows/www.yml"
jobs:
deploy-www:
name: Build and deploy www
runs-on: ubuntu-latest
defaults:
run:
working-directory: "apps/www"
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Build Project Artifacts
run: bun run build
- name: Deploy Project Artifacts to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
packageManager: bun
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "3.93.0"
workingDirectory: "apps/www"
command: pages deploy ./dist --project-name=${{ vars.CF_WWW_PAGES_PROJECT_NAME }} --commit-dirty=true
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ node_modules
# Local env files
.env
.env.local
.env.sst
.env.development.local
.env.test.local
.env.production.local

View File

@@ -1,4 +0,0 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "unifiedjs.vscode-mdx"],
"unwantedRecommendations": []
}

24
.vscode/launch.json vendored
View File

@@ -1,24 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Chrome",
"request": "launch",
"type": "chrome",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}"
},
{
"type": "node",
"name": "dev.debug",
"request": "launch",
"skipFiles": ["<node_internals>/**"],
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/node_modules/vite/bin/vite.js",
"args": ["--mode", "ssr", "--force"]
}
]
}

View File

@@ -1,36 +0,0 @@
{
"onRequest": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qonRequest",
"description": "onRequest function for a route index",
"body": [
"export const onRequest: RequestHandler = (request) => {",
" $0",
"};",
],
},
"loader$": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qloader$",
"description": "loader$()",
"body": ["export const $1 = routeLoader$(() => {", " $0", "});"],
},
"action$": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qaction$",
"description": "action$()",
"body": ["export const $1 = routeAction$((data) => {", " $0", "});"],
},
"Full Page": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qpage",
"description": "Simple page component",
"body": [
"import { component$ } from '@builder.io/qwik';",
"",
"export default component$(() => {",
" $0",
"});",
],
},
}

View File

@@ -1,78 +0,0 @@
{
"Qwik component (simple)": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qcomponent$",
"description": "Simple Qwik component",
"body": [
"export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = component$(() => {",
" return <${2:div}>$4</$2>",
"});",
],
},
"Qwik component (props)": {
"scope": "typescriptreact",
"prefix": "qcomponent$ + props",
"description": "Qwik component w/ props",
"body": [
"export interface ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}Props {",
" $2",
"}",
"",
"export const $1 = component$<$1Props>((props) => {",
" const ${2:count} = useSignal(0);",
" return (",
" <${3:div} on${4:Click}$={(ev) => {$5}}>",
" $6",
" </${3}>",
" );",
"});",
],
},
"Qwik signal": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseSignal",
"description": "useSignal() declaration",
"body": ["const ${1:foo} = useSignal($2);", "$0"],
},
"Qwik store": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseStore",
"description": "useStore() declaration",
"body": ["const ${1:state} = useStore({", " $2", "});", "$0"],
},
"$ hook": {
"scope": "javascriptreact,typescriptreact",
"prefix": "q$",
"description": "$() function hook",
"body": ["$(() => {", " $0", "});", ""],
},
"useVisibleTask": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseVisibleTask",
"description": "useVisibleTask$() function hook",
"body": ["useVisibleTask$(({ track }) => {", " $0", "});", ""],
},
"useTask": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseTask$",
"description": "useTask$() function hook",
"body": [
"useTask$(({ track }) => {",
" track(() => $1);",
" $0",
"});",
"",
],
},
"useResource": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseResource$",
"description": "useResource$() declaration",
"body": [
"const $1 = useResource$(({ track, cleanup }) => {",
" $0",
"});",
"",
],
},
}

12
.vscode/settings.json vendored
View File

@@ -1,11 +1,3 @@
{
"material-icon-theme.activeIconPack": "qwik",
"emmet.includeLanguages": {
"typescriptreact": "html"
},
"emmet.preferences": {
// to ensure closing tags are used (e.g. <img/> not just <img> like in HTML)
// https://github.com/microsoft/vscode/commit/083bf9020407ea5a91199eb1f0b373859df8d600#diff-88456bc9b7caa2f8126aea0107b4671db0f094961aaf39a7c689f890e23aaaba
"output.selfClosingStyle": "xhtml"
}
}
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@@ -0,0 +1,3 @@
# What is this?
This is the part of the docs dedicated for the team working on Nestri

View File

@@ -0,0 +1,27 @@
# Setup
- Install bun [https://bun.sh/](https://bun.sh/)
- Generate your Cloudflare token from [here](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22memberships%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22user_details%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_kv_storage%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_r2%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_routes%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_scripts%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_tail%22%2C%22type%22%3A%22read%22%7D%5D&name=sst&accountId=*&zoneId=all)
- save it to a `.env` file like this
```
CLOUDFLARE_API_TOKEN=xxx
```
- Copy this to your `~/.aws/config` file
```
[sso-session nestri]
sso_start_url = https://nestri.awsapps.com/start
sso_region = us-east-1
[profile nestri-dev]
sso_session = nestri
sso_account_id = 535002871375
sso_role_name = AdministratorAccess
region = us-east-1
[profile nestri-production]
sso_session = nestri
sso_account_id = 209479283398
sso_role_name = AdministratorAccess
region = us-east-1
```
- You need to login once a day with `bun sso` in root

View File

@@ -0,0 +1,2 @@
title: 'Nestri Internals'
icon: heroicons-outline:bookmark-alt

File diff suppressed because it is too large Load Diff

View File

@@ -4,19 +4,19 @@
"private": true,
"scripts": {
"nestri.dev": "nuxi dev",
"build": "nuxi build",
"build": "nuxi build --preset=cloudflare_pages",
"generate": "nuxi generate",
"preview": "nuxi preview",
"lint": "eslint ."
},
"devDependencies": {
"@nuxt-themes/docus": "latest",
"@nuxt/devtools": "^1.4.1",
"@nuxt/devtools": "^2.3.2",
"@nuxt/eslint-config": "^0.5.6",
"@nuxt/ui": "^2.19.2",
"@nuxtjs/plausible": "^1.0.2",
"@types/node": "^20.16.5",
"eslint": "^9.10.0",
"nuxt": "^3.15.4"
"nuxt": "^3.16.1"
}
}

View File

@@ -35,14 +35,16 @@
"@builder.io/qwik": "^1.8.0",
"@builder.io/qwik-city": "^1.8.0",
"@builder.io/qwik-react": "0.5.0",
"@fontsource-variable/bricolage-grotesque": "^5.1.1",
"@fontsource-variable/bricolage-grotesque": "^5.0.1",
"@fontsource/geist-mono": "^5.1.0",
"@fontsource/geist-sans": "^5.1.0",
"@fontsource-variable/mona-sans": "^5.0.1",
"@modular-forms/qwik": "^0.29.0",
"@nestri/input": "*",
"@nestri/libmoq": "*",
"@nestri/sdk": "0.1.0-alpha.14",
"@nestri/ui": "*",
"@openauthjs/openauth": "^0.2.6",
"@openauthjs/openauth": "*",
"@polar-sh/checkout": "^0.1.8",
"@polar-sh/sdk": "^0.21.1",
"@qwik-ui/headless": "^0.6.4",
@@ -61,10 +63,11 @@
"prettier": "3.3.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"semver": "^7.7.1",
"typescript": "5.4.5",
"undici": "*",
"valibot": "^0.42.1",
"vite": "5.4.12",
"vite": "6.0.15",
"vite-tsconfig-paths": "^4.2.1",
"wrangler": "^3.0.0"
},

View File

@@ -16,8 +16,8 @@ export default component$(() => {
<div class="w-screen relative">
<HeroSection client:load>
<div class="sm:w-full flex gap-3 justify-center pt-4 sm:flex-row flex-col w-auto items-center">
<Link href="/auth/login" prefetch={false} class="flex ring-2 ring-primary-500 font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Get early access
<Link href="https://discord.gg/6um5K6jrYj" prefetch={false} class="flex ring-2 ring-primary-500 font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Join our Discord
</Link>
<Link href="/links/github" prefetch={false} class="sm:flex text-sm sm:text-base hidden font-bricolage items-center gap-2 rounded-full font-semibold text-gray-900/70 dark:text-gray-100/70 bg-white dark:bg-black px-5 py-4 ring-2 ring-gray-300 dark:ring-gray-700 transition-all hover:scale-105 active:scale-95 sm:px-6" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-5 w-5 fill-content3-light"><path fill-rule="evenodd" d="M4.25 5.5a.75.75 0 00-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0112.75 17h-8.5A2.25 2.25 0 012 14.75v-8.5A2.25 2.25 0 014.25 4h5a.75.75 0 010 1.5h-5z" clip-rule="evenodd"></path><path fill-rule="evenodd" d="M6.194 12.753a.75.75 0 001.06.053L16.5 4.44v2.81a.75.75 0 001.5 0v-4.5a.75.75 0 00-.75-.75h-4.5a.75.75 0 000 1.5h2.553l-9.056 8.194a.75.75 0 00-.053 1.06z" clip-rule="evenodd"></path></svg>
@@ -570,8 +570,8 @@ export default component$(() => {
</section>
<Footer client:load>
<div class="w-full flex justify-center flex-col items-center gap-3">
<Link href="/auth/login" prefetch={false} class="ring-2 ring-primary-500 flex font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Get early access
<Link href="https://discord.gg/6um5K6jrYj" prefetch={false} class="ring-2 ring-primary-500 flex font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Join our Discord
</Link>
<div class="mt-6 flex w-full items-center justify-center gap-2 text-xs sm:text-sm font-medium text-neutral-600 dark:text-neutral-400">
<span class="hover:text-primary-500 transition-colors duration-200">

View File

@@ -116,8 +116,30 @@ export default component$(() => {
return (
<div class="w-screen relative">
<TitleSection client:load title="Pricing" description={"We're growing at the speed of trust. Choose a price that feels right for you and help support Nestri"} />
<MotionComponent
<TitleSection client:load title="Pricing" description={"The biggest bang, binge, and blast for your buck"} />
<Footer client:load>
<div class="w-full flex justify-center flex-col items-center gap-3">
<Link href="https://discord.gg/6um5K6jrYj" prefetch={false} class="flex font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Join our Discord
</Link>
<div class="mt-6 flex w-full items-center justify-center gap-2 text-xs sm:text-sm font-medium text-neutral-600 dark:text-neutral-400">
<span class="hover:text-primary-500 transition-colors duration-200">
<Link rel="noreferrer" href="/terms" >Terms of Service</Link></span>
<span class="text-gray-400 dark:text-gray-600"></span>
<span class="hover:text-primary-500 transition-colors duration-200" >
<Link href="/privacy">Privacy Policy</Link>
</span>
</div>
</div>
</Footer>
</div>
)
})
/**
* <MotionComponent
initial={{ opacity: 0, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
@@ -144,7 +166,6 @@ export default component$(() => {
</div>
<div class="flex flex-col w-full">
<p class="text-[4rem] leading-[1] font-medium font-title"> Free </p>
{/**FIXME: Add the link to the docs here */}
<a href={CONSTANTS.githubLink} ref={v => bookRef.value = v} class="h-[154px] w-full flex items-start pt-4 justify-center overflow-hidden">
<Book textColor="#FFF"
bgColor="#FF4F01"
@@ -519,21 +540,4 @@ export default component$(() => {
</section>
</div>
</MotionComponent>
<Footer client:load>
<div class="w-full flex justify-center flex-col items-center gap-3">
<Link href="/auth/login" prefetch={false} class="flex font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Get early access
</Link>
<div class="mt-6 flex w-full items-center justify-center gap-2 text-xs sm:text-sm font-medium text-neutral-600 dark:text-neutral-400">
<span class="hover:text-primary-500 transition-colors duration-200">
<Link rel="noreferrer" href="/terms" >Terms of Service</Link></span>
<span class="text-gray-400 dark:text-gray-600"></span>
<span class="hover:text-primary-500 transition-colors duration-200" >
<Link href="/privacy">Privacy Policy</Link>
</span>
</div>
</div>
</Footer>
</div>
)
})
*/

7203
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
FROM docker.io/golang:1.24-bookworm AS go-build
WORKDIR /builder
COPY packages/maitred/ /builder/
RUN go build
FROM docker.io/golang:1.24-bookworm
COPY --from=go-build /builder/maitred /maitred/maitred
WORKDIR /maitred
RUN apt update && apt install -y --no-install-recommends pciutils
ENTRYPOINT ["/maitred/maitred"]

View File

@@ -1,20 +1,31 @@
FROM docker.io/golang:1.23-alpine AS go-build
FROM docker.io/golang:1.24-alpine AS go-build
WORKDIR /builder
COPY packages/relay/ /builder/
RUN go build
FROM docker.io/golang:1.23-alpine
FROM docker.io/golang:1.24-alpine
COPY --from=go-build /builder/relay /relay/relay
WORKDIR /relay
# TODO: Switch running layer to just alpine (doesn't need golang dev stack)
# ENV flags
ENV VERBOSE=false
ENV DEBUG=false
ENV ENDPOINT_PORT=8088
ENV MESH_PORT=8089
ENV WEBRTC_UDP_START=10000
ENV WEBRTC_UDP_END=20000
ENV STUN_SERVER="stun.l.google.com:19302"
ENV WEBRTC_UDP_MUX=8088
ENV WEBRTC_NAT_IPS=""
ENV AUTO_ADD_LOCAL_IP=true
ENV TLS_CERT=""
ENV TLS_KEY=""
EXPOSE $ENDPOINT_PORT
EXPOSE $MESH_PORT
EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp
EXPOSE $WEBRTC_UDP_MUX/udp
ENTRYPOINT ["/relay/relay"]

View File

@@ -1,10 +1,18 @@
# Container build arguments #
ARG BASE_IMAGE=docker.io/cachyos/cachyos:latest
#******************************************************************************
# Base Stage - Updates system packages
#******************************************************************************
FROM ${BASE_IMAGE} AS base
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman --noconfirm -Syu
#******************************************************************************
# Base Builder Stage - Prepares core build environment
#******************************************************************************
FROM ${BASE_IMAGE} AS base-builder
FROM base AS base-builder
# Environment setup for Rust and Cargo
ENV CARGO_HOME=/usr/local/cargo \
@@ -14,9 +22,12 @@ ENV CARGO_HOME=/usr/local/cargo \
# Install build essentials and caching tools
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm mold rust && \
pacman -Sy --noconfirm mold rustup && \
mkdir -p "${ARTIFACTS}"
# Install latest Rust using rustup
RUN rustup default stable
# Install cargo-chef with proper caching
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo install -j $(nproc) cargo-chef cargo-c --locked
@@ -28,7 +39,8 @@ FROM base-builder AS nestri-server-deps
WORKDIR /builder
# Install build dependencies
RUN pacman -Sy --noconfirm meson pkgconf cmake git gcc make \
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm meson pkgconf cmake git gcc make \
gstreamer gst-plugins-base gst-plugins-good gst-plugin-rswebrtc
#--------------------------------------------------------------------
@@ -73,8 +85,8 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm meson pkgconf cmake git gcc make \
libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput
# Clone repository with proper directory structure
RUN git clone -b dev-dmabuf https://github.com/games-on-whales/gst-wayland-display.git
# Clone repository
RUN git clone -b dev-dmabuf https://github.com/DatCaptainHorse/gst-wayland-display.git
#--------------------------------------------------------------------
FROM gst-wayland-deps AS gst-wayland-planner
@@ -107,7 +119,7 @@ RUN --mount=type=cache,target=${CARGO_HOME}/registry \
#******************************************************************************
# Final Runtime Stage
#******************************************************************************
FROM ${BASE_IMAGE} AS runtime
FROM base AS runtime
### System Configuration ###
RUN sed -i \
@@ -117,27 +129,31 @@ RUN sed -i \
dirmngr </dev/null > /dev/null 2>&1
### Package Installation ###
RUN pacman --noconfirm -Sy && \
# Core system components
pacman -S --needed --noconfirm \
archlinux-keyring vulkan-intel lib32-vulkan-intel mesa \
steam steam-native-runtime \
sudo xorg-xwayland labwc wlr-randr mangohud \
# Core system components
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --needed --noconfirm \
vulkan-intel lib32-vulkan-intel vpl-gpu-rt mesa \
steam steam-native-runtime gtk3 lib32-gtk3 \
sudo xorg-xwayland seatd libinput labwc wlr-randr gamescope mangohud \
libssh2 curl wget \
pipewire pipewire-pulse pipewire-alsa wireplumber \
noto-fonts-cjk supervisor jq chwd lshw pacman-contrib && \
# GStreamer stack
pacman -S --needed --noconfirm \
pacman -Sy --needed --noconfirm \
gstreamer gst-plugins-base gst-plugins-good \
gst-plugins-bad gst-plugin-pipewire \
gst-plugin-rswebrtc gst-plugin-rsrtp && \
gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \
gst-plugin-va gst-plugin-qsv && \
# lib32 GStreamer stack to fix some games with videos
pacman -Sy --needed --noconfirm \
lib32-gstreamer lib32-gst-plugins-base lib32-gst-plugins-good && \
# Cleanup
paccache -rk1 && \
rm -rf /usr/share/{info,man,doc}/*
### Application Installation ###
ARG LUDUSAVI_VERSION="0.28.0"
RUN pacman -Sy --noconfirm --needed curl && \
curl -fsSL -o ludusavi.tar.gz \
RUN curl -fsSL -o ludusavi.tar.gz \
"https://github.com/mtkennerly/ludusavi/releases/download/v${LUDUSAVI_VERSION}/ludusavi-v${LUDUSAVI_VERSION}-linux.tar.gz" && \
tar -xzvf ludusavi.tar.gz && \
mv ludusavi /usr/bin/ && \
@@ -172,6 +188,30 @@ RUN mkdir -p /run/dbus && \
-e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \
/usr/share/wireplumber/wireplumber.conf
### PipeWire Latency Optimizations (1-5ms instead of 20ms) ###
RUN mkdir -p /etc/pipewire/pipewire.conf.d && \
echo "[audio]\
\n default.clock.rate = 48000\
\n default.clock.quantum = 128\
\n default.clock.min-quantum = 128\
\n default.clock.max-quantum = 256" > /etc/pipewire/pipewire.conf.d/low-latency.conf && \
mkdir -p /etc/wireplumber/main.lua.d && \
echo 'table.insert(default_nodes.rules, {\
\n matches = { { { "node.name", "matches", ".*" } } },\
\n apply_properties = {\
\n ["audio.format"] = "S16LE",\
\n ["audio.rate"] = 48000,\
\n ["audio.channels"] = 2,\
\n ["api.alsa.period-size"] = 128,\
\n ["api.alsa.headroom"] = 0,\
\n ["session.suspend-timeout-seconds"] = 0\
\n }\
\n})' > /etc/wireplumber/main.lua.d/50-low-latency.lua && \
echo "default-fragments = 2\
\ndefault-fragment-size-msec = 2" >> /etc/pulse/daemon.conf && \
echo "load-module module-loopback latency_msec=1" >> /etc/pipewire/pipewire.conf.d/loopback.conf
### Artifacts and Verification ###
COPY --from=nestri-server-cached-builder /artifacts/nestri-server /usr/bin/
COPY --from=gst-wayland-cached-builder /artifacts/lib/ /usr/lib/

View File

@@ -1,54 +1,63 @@
import { authFingerprintKey } from "./auth";
import { bus } from "./bus";
import { auth } from "./auth";
import { domain } from "./dns";
import { secret } from "./secrets"
// import { party } from "./party"
import { gpuTaskDefinition, ecsCluster } from "./cluster";
import { secret } from "./secret";
import { cluster } from "./cluster";
import { postgres } from "./postgres";
export const urls = new sst.Linkable("Urls", {
properties: {
api: "https://api." + domain,
auth: "https://auth." + domain,
export const api = new sst.aws.Service("Api", {
cluster,
cpu: $app.stage === "production" ? "2 vCPU" : undefined,
memory: $app.stage === "production" ? "4 GB" : undefined,
command: ["bun", "run", "./src/api/index.ts"],
link: [
bus,
auth,
postgres,
secret.PolarSecret,
secret.PolarWebhookSecret,
secret.NestriFamilyMonthly,
secret.NestriFamilyYearly,
secret.NestriFreeMonthly,
secret.NestriProMonthly,
secret.NestriProYearly,
],
image: {
dockerfile: "packages/functions/Containerfile",
},
environment: {
NO_COLOR: "1",
},
loadBalancer: {
rules: [
{
listen: "80/http",
forward: "3001/http",
},
],
},
dev: {
command: "bun dev:api",
directory: "packages/functions",
url: "http://localhost:3001",
},
scaling:
$app.stage === "production"
? {
min: 2,
max: 10,
}
: undefined,
});
export const kv = new sst.cloudflare.Kv("CloudflareAuthKV")
export const auth = new sst.cloudflare.Worker("Auth", {
link: [
kv,
urls,
authFingerprintKey,
secret.InstantAdminToken,
secret.InstantAppId,
secret.LoopsApiKey,
secret.GithubClientID,
secret.GithubClientSecret,
secret.DiscordClientID,
secret.DiscordClientSecret,
],
handler: "./packages/functions/src/auth.ts",
url: true,
domain: "auth." + domain
});
export const api = new sst.cloudflare.Worker("Api", {
link: [
urls,
ecsCluster,
gpuTaskDefinition,
authFingerprintKey,
secret.LoopsApiKey,
secret.InstantAppId,
secret.AwsAccessKey,
secret.AwsSecretKey,
secret.InstantAdminToken,
],
url: true,
handler: "./packages/functions/src/api/index.ts",
domain: "api." + domain
})
export const outputs = {
auth: auth.url,
api: api.url
}
export const apiRoute = new sst.aws.Router("ApiRoute", {
routes: {
// I think api.url should work all the same
"/*": api.nodes.loadBalancer.dnsName,
},
domain: {
name: "api." + domain,
dns: sst.cloudflare.dns(),
},
})

View File

@@ -1,12 +1,66 @@
export const authFingerprintKey = new random.RandomString(
"AuthFingerprintKey",
{
length: 32,
},
);
import { bus } from "./bus";
import { domain } from "./dns";
import { secret } from "./secret";
import { cluster } from "./cluster";
import { postgres } from "./postgres";
sst.Linkable.wrap(random.RandomString, (resource) => ({
properties: {
value: resource.result,
//FIXME: Use a shared /tmp folder
export const auth = new sst.aws.Service("Auth", {
cluster,
cpu: $app.stage === "production" ? "1 vCPU" : undefined,
memory: $app.stage === "production" ? "2 GB" : undefined,
command: ["bun", "run", "./src/auth.ts"],
link: [
bus,
postgres,
secret.PolarSecret,
secret.GithubClientID,
secret.DiscordClientID,
secret.GithubClientSecret,
secret.DiscordClientSecret,
],
image: {
dockerfile: "packages/functions/Containerfile",
},
}));
environment: {
NO_COLOR: "1",
STORAGE: $dev ? "/tmp/persist.json" : "/mnt/efs/persist.json"
},
loadBalancer: {
rules: [
{
listen: "80/http",
forward: "3002/http",
},
],
},
permissions: [
{
actions: ["ses:SendEmail"],
resources: ["*"],
},
],
dev: {
command: "bun dev:auth",
directory: "packages/functions",
url: "http://localhost:3002",
},
scaling:
$app.stage === "production"
? {
min: 2,
max: 10,
}
: undefined,
});
export const authRoute = new sst.aws.Router("AuthRoute", {
routes: {
// I think auth.url should work all the same
"/*": auth.nodes.loadBalancer.dnsName,
},
domain: {
name: "auth." + domain,
dns: sst.cloudflare.dns(),
},
})

23
infra/bus.ts Normal file
View File

@@ -0,0 +1,23 @@
import { vpc } from "./vpc";
// import { email } from "./email";
import { allSecrets } from "./secret";
import { postgres } from "./postgres";
export const bus = new sst.aws.Bus("Bus");
bus.subscribe("Event", {
vpc,
handler: "./packages/functions/src/event/event.handler",
link: [
// email,
postgres,
...allSecrets
],
timeout: "5 minutes",
permissions: [
{
actions: ["ses:SendEmail"],
resources: ["*"],
},
],
});

View File

@@ -1,155 +1,6 @@
import { sshKey } from "./ssh";
import { authFingerprintKey } from "./auth";
import { vpc } from "./vpc";
export const ecsCluster = new aws.ecs.Cluster("NestriGPUCluster", {
name: "NestriGPUCluster",
});
const ecsInstanceRole = new aws.iam.Role("NestriGPUInstanceRole", {
name: "GPUAssumeRoleProd",
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Principal: {
Service: "ec2.amazonaws.com",
},
Effect: "Allow",
Sid: "",
}],
}),
});
new aws.iam.RolePolicyAttachment("NestriGPUInstancePolicyAttachment", {
role: ecsInstanceRole.name,
policyArn: "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role",
});
const ecsInstanceProfile = new aws.iam.InstanceProfile("NestriGPUInstanceProfile", {
role: ecsInstanceRole.name,
});
// const server = new aws.ec2.Instance("NestriGPU", {
// instanceType: aws.ec2.InstanceType.G4dn_XLarge,
// ami: "ami-046a6af96ef510bb6",//Fedora cloud
// keyName: sshKey.keyName,
// instanceMarketOptions: {
// marketType: "spot",
// spotOptions: {
// maxPrice: "0.2",
// spotInstanceType: "persistent",
// instanceInterruptionBehavior: "stop"
// },
// },
// iamInstanceProfile: ecsInstanceProfile,
// });
const logGroup = new aws.cloudwatch.LogGroup("NestriGPULogGroup", {
name: "/ecs/nestri-gpu-prod",
retentionInDays: 7,
});
// Create a Task Definition for the ECS service to test it
export const gpuTaskDefinition = new aws.ecs.TaskDefinition("NestriGPUTask", {
family: "NestriGPUTaskProd",
requiresCompatibilities: ["EC2"],
volumes: [
{
name: "host",
hostPath: "/mnt/"
// efsVolumeConfiguration: {
// fileSystemId: storage.id,
// authorizationConfig: { accessPointId: storage.accessPoint },
// transitEncryption: "ENABLED",
// }
}
],
containerDefinitions: authFingerprintKey.result.apply(v => JSON.stringify([{
"essential": true,
"name": "nestri",
"memory": 1024,
"cpu": 200,
"gpu": 1,
"image": "ghcr.io/nestrilabs/nestri/runner:nightly",
"environment": [
{
"name": "RESOLUTION",
"value": "1920x1080"
},
{
"name": "AUTH_FINGERPRINT",
"value": v
},
{
"name": "FRAMERATE",
"value": "60"
},
{
"name": "NESTRI_ROOM",
"value": "aws-testing"
},
{
"name": "RELAY_URL",
"value": "https://relay.dathorse.com"
},
{
"name": "NESTRI_PARAMS",
"value": "--verbose=true --video-codec=h264 --video-bitrate=4000 --video-bitrate-max=6000 --gpu-card-path=/dev/dri/card0"
},
],
"mountPoints": [{ "containerPath": "/home/nestri", "sourceVolume": "host" }],
"disableNetworking": false,
"linuxParameter": {
"sharedMemorySize": 5120
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/nestri-gpu-prod",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "nestri-gpu-task"
}
}
}]))
});
sst.Linkable.wrap(aws.ecs.TaskDefinition, (resource) => ({
properties: {
value: resource.arn,
},
}));
sst.Linkable.wrap(aws.ecs.Cluster, (resource) => ({
properties: {
value: resource.arn,
},
}));
// userData: $interpolate`#!/bin/bash
// sudo rm /etc/sysconfig/docker
// echo DAEMON_MAXFILES=1048576 | sudo tee -a /etc/sysconfig/docker
// echo DAEMON_PIDFILE_TIMEOUT=10 | sud o tee -a /etc/sysconfig/docker
// echo OPTIONS="--default-ulimit nofile=32768:65536" | sudo tee -a /etc/sysconfig/docker
// sudo tee "/etc/docker/daemon.json" > /dev/null <<EOF
// {
// "default-runtime": "nvidia",
// "runtimes": {
// "nvidia": {
// "path": "/usr/bin/nvidia-container-runtime",
// "runtimeArgs": []
// }
// }
// }
// EOF
// sudo systemctl restart docker
// echo ECS_CLUSTER='${ecsCluster.name}' | sudo tee -a /etc/ecs/ecs.config
// echo ECS_ENABLE_GPU_SUPPORT=true | sudo tee -a /etc/ecs/ecs.config
// echo ECS_CONTAINER_STOP_TIMEOUT=3h | sudo tee -a /etc/ecs/ecs.config
// echo ECS_ENABLE_SPOT_INSTANCE_DRAINING=true | sudo tee -a /etc/ecs/ecs.config
// `,
// This is used for requesting a container to be deployed on AWS
// const queue = new sst.aws.Queue("PartyQueue", { fifo: true });
// queue.subscribe({ handler: "packages/functions/src/party/subscriber.handler", permissions:{}, link:[taskF]})
// const authRes = $interpolate`${authFingerprintKey.result}`
export const cluster = new sst.aws.Cluster("Cluster", {
vpc,
forceUpgrade: "v2"
});

6
infra/email.ts Normal file
View File

@@ -0,0 +1,6 @@
import { domain } from "./dns";
export const email = new sst.aws.Email("Email",{
sender: domain,
dns: sst.cloudflare.dns(),
})

View File

@@ -1,33 +0,0 @@
// This is for the websocket/MQTT endpoint that helps the API communicate with the container
// [API] <-> party <-websocket-> container
// The container is it's own this, and can listen to Websocket connections to start or stop a Steam Game
// import { authFingerprintKey } from "./auth";
// import { ecsCluster, gpuTaskDefinition } from "./cluster";
// export const party = new sst.aws.Realtime("Party", {
// authorizer: "packages/functions/src/party/authorizer.handler"
// });
// export const partyFn = new sst.aws.Function("NestriPartyFn", {
// handler: "packages/functions/src/party/create.handler",
// // link: [queue],
// link: [authFingerprintKey],
// environment: {
// TASK_DEFINITION: gpuTaskDefinition.arn,
// // AUTH_FINGERPRINT: authFingerprintKey.result,
// ECS_CLUSTER: ecsCluster.arn,
// },
// permissions: [
// {
// effect: "allow",
// actions: ["ecs:RunTask"],
// resources: [gpuTaskDefinition.arn]
// }
// ],
// url: true,
// });
// export const outputs = {
// partyFunction: partyFn.url
// }

68
infra/postgres.ts Normal file
View File

@@ -0,0 +1,68 @@
import { vpc } from "./vpc";
import { isPermanentStage } from "./stage";
// TODO: Add a dev db to use, this will help with running zero locally... and testing it
export const postgres = new sst.aws.Aurora("Database", {
vpc,
engine: "postgres",
scaling: isPermanentStage
? undefined
: {
min: "0 ACU",
max: "1 ACU",
},
transform: {
clusterParameterGroup: {
parameters: [
{
name: "rds.logical_replication",
value: "1",
applyMethod: "pending-reboot",
},
{
name: "max_slot_wal_keep_size",
value: "10240",
applyMethod: "pending-reboot",
},
{
name: "rds.force_ssl",
value: "0",
applyMethod: "pending-reboot",
},
{
name: "max_connections",
value: "1000",
applyMethod: "pending-reboot",
},
],
},
},
});
new sst.x.DevCommand("Studio", {
link: [postgres],
dev: {
command: "bun db:dev studio",
directory: "packages/core",
autostart: true,
},
});
const migrator = new sst.aws.Function("DatabaseMigrator", {
handler: "packages/functions/src/migrator.handler",
link: [postgres],
copyFiles: [
{
from: "packages/core/migrations",
to: "./migrations",
},
],
});
if (!$dev) {
new aws.lambda.Invocation("DatabaseMigratorInvocation", {
input: Date.now().toString(),
functionName: migrator.name,
});
}

9
infra/realtime.ts Normal file
View File

@@ -0,0 +1,9 @@
import { auth } from "./auth";
import { postgres } from "./postgres";
export const device = new sst.aws.Realtime("Realtime", {
authorizer: {
link: [auth, postgres],
handler: "packages/functions/src/realtime/authorizer.handler"
}
})

View File

@@ -1,179 +0,0 @@
// const vpc = new sst.aws.Vpc("NestriRelayVpc", { az: 2 })
// import { subnet1, subnet2, securityGroup } from "./vpc"
// const taskExecutionRole = new aws.iam.Role('NestriRelayExecutionRole', {
// assumeRolePolicy: JSON.stringify({
// Version: '2012-10-17',
// Statement: [
// {
// Effect: 'Allow',
// Principal: {
// Service: 'ecs-tasks.amazonaws.com',
// },
// Action: 'sts:AssumeRole',
// },
// ],
// }),
// });
// const taskRole = new aws.iam.Role('NestriRelayTaskRole', {
// assumeRolePolicy: JSON.stringify({
// Version: '2012-10-17',
// Statement: [
// {
// Effect: 'Allow',
// Principal: {
// Service: 'ecs-tasks.amazonaws.com',
// },
// Action: 'sts:AssumeRole',
// },
// ],
// }),
// });
// new aws.cloudwatch.LogGroup('NestriRelayLogGroup', {
// name: '/ecs/nestri-relay',
// retentionInDays: 7,
// });
// new aws.iam.RolePolicyAttachment('NestriRelayExecutionRoleAttachment', {
// policyArn: 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy',
// role: taskRole,
// });
// const logPolicy = new aws.iam.Policy('NestriRelayLogPolicy', {
// policy: JSON.stringify({
// Version: '2012-10-17',
// Statement: [
// {
// Effect: 'Allow',
// Action: ['logs:CreateLogStream', 'logs:PutLogEvents'],
// Resource: 'arn:aws:logs:*:*:*',
// },
// ],
// }),
// });
// new aws.iam.RolePolicyAttachment('NestriRelayTaskRoleAttachment', {
// policyArn: logPolicy.arn,
// role: taskExecutionRole,
// });
// const taskDefinition = new aws.ecs.TaskDefinition("NestriRelayTask", {
// family: "NestriRelay",
// cpu: "1024",
// memory: "2048",
// networkMode: "awsvpc",
// taskRoleArn: taskRole.arn,
// requiresCompatibilities: ["FARGATE"],
// executionRoleArn: taskExecutionRole.arn,
// containerDefinitions: JSON.stringify([{
// name: "nestri-relay",
// essential: true,
// memory: 2048,
// image: "ghcr.io/nestrilabs/nestri/relay:nightly",
// portMappings: [
// // HTTP port
// {
// protocol: "tcp",
// hostPort: 80,
// containerPort: 80,
// },
// // UDP port range (1,000 ports)
// {
// containerPortRange: "10000-11000",
// protocol: "udp",
// },
// ],
// "environment": [
// {
// name: "ENDPOINT_PORT",
// value: "80"
// },
// ],
// logConfiguration: {
// logDriver: 'awslogs',
// options: {
// 'awslogs-group': '/ecs/nestri-relay',
// 'awslogs-region': 'us-east-1',
// 'awslogs-stream-prefix': 'ecs',
// },
// },
// }]),
// });
// const relayCluster = new aws.ecs.Cluster('NestriRelay');
// new aws.ecs.Service('NestriRelayService', {
// name: 'NestriRelayService',
// cluster: relayCluster.arn,
// desiredCount: 1,
// launchType: 'FARGATE',
// taskDefinition: taskDefinition.arn,
// deploymentCircuitBreaker: {
// enable: true,
// rollback: true,
// },
// enableExecuteCommand: true,
// networkConfiguration: {
// assignPublicIp: true,
// subnets: [subnet1.id, subnet2.id],
// securityGroups: [securityGroup.id],
// },
// });
//FIXME: I cannot create Global Accelerators (Something to do with Quotas - Yet my account is fine)
// const usWest2 = new aws.Provider("GlobalAccelerator", { region: aws.Region.USWest2 })
// const accelerator = new aws.globalaccelerator.Accelerator('Accelerator', {
// name: 'NestriRelayAccelerator',
// enabled: true,
// ipAddressType: 'IPV4',
// }, { provider: usWest2 });
// const httpListener = new aws.globalaccelerator.Listener('TcpListener', {
// acceleratorArn: accelerator.id,
// clientAffinity: 'SOURCE_IP',
// protocol: 'TCP',
// portRanges: [{
// fromPort: 80,
// toPort: 80,
// }],
// }, { provider: usWest2 });
// const udpListener = new aws.globalaccelerator.Listener('UdpListener', {
// acceleratorArn: accelerator.id,
// clientAffinity: 'SOURCE_IP',
// protocol: 'UDP',
// portRanges: [{
// fromPort: 10000,
// toPort: 11000,
// }],
// }, { provider: usWest2 });
// new aws.globalaccelerator.EndpointGroup('TcpRelay', {
// listenerArn: httpListener.id,
// // healthCheckPath: '/',
// endpointGroupRegion: aws.Region.USEast1,
// endpointConfigurations: [{
// clientIpPreservationEnabled: true,
// endpointId: subnet1.id, //vpc.publicSubnets[0].apply(i => i),
// weight: 100,
// }],
// }, { provider: usWest2 });
// new aws.globalaccelerator.EndpointGroup('UdpRelay', {
// listenerArn: udpListener.id,
// // healthCheckPort: 80,
// // healthCheckPath: "/",
// endpointGroupRegion: aws.Region.USEast1,
// endpointConfigurations: [{
// clientIpPreservationEnabled: true,
// endpointId: subnet1.id,//vpc.publicSubnets[0].apply(i => i),
// weight: 100,
// }],
// }, { provider: usWest2 });
// export const outputs = {
// relay: accelerator.dnsName
// }

17
infra/secret.ts Normal file
View File

@@ -0,0 +1,17 @@
export const secret = {
PolarSecret: new sst.Secret("PolarSecret", process.env.POLAR_API_KEY),
GithubClientID: new sst.Secret("GithubClientID"),
DiscordClientID: new sst.Secret("DiscordClientID"),
PolarWebhookSecret: new sst.Secret("PolarWebhookSecret"),
GithubClientSecret: new sst.Secret("GithubClientSecret"),
DiscordClientSecret: new sst.Secret("DiscordClientSecret"),
// Pricing
NestriFreeMonthly: new sst.Secret("NestriFreeMonthly"),
NestriProMonthly: new sst.Secret("NestriProMonthly"),
NestriProYearly: new sst.Secret("NestriProYearly"),
NestriFamilyMonthly: new sst.Secret("NestriFamilyMonthly"),
NestriFamilyYearly: new sst.Secret("NestriFamilyYearly"),
};
export const allSecrets = Object.values(secret);

View File

@@ -1,13 +0,0 @@
export const secret = {
LoopsApiKey: new sst.Secret("LoopsApiKey"),
InstantAppId: new sst.Secret("InstantAppId"),
AwsSecretKey: new sst.Secret("AwsSecretKey"),
AwsAccessKey: new sst.Secret("AwsAccessKey"),
GithubClientID: new sst.Secret("GithubClientID"),
DiscordClientID: new sst.Secret("DiscordClientID"),
GithubClientSecret: new sst.Secret("GithubClientSecret"),
InstantAdminToken: new sst.Secret("InstantAdminToken"),
DiscordClientSecret: new sst.Secret("DiscordClientSecret"),
};
export const allSecrets = Object.values(secret);

View File

@@ -1,19 +0,0 @@
import { resolve } from "path";
import { writeFileSync } from "fs";
export const privateKey = new tls.PrivateKey("NestriGPUPrivateKey", {
algorithm: "RSA",
rsaBits: 4096,
});
// Just in case you want to SSH
export const sshKey = new aws.ec2.KeyPair("NestriGPUKey", {
keyName: "NestriGPUKeyProd",
publicKey: privateKey.publicKeyOpenssh
})
export const keyPath = privateKey.privateKeyOpenssh.apply((key) => {
const path = "key_ssh";
writeFileSync(path, key, { mode: 0o600 });
return resolve(path);
});

7
infra/steam.ts Normal file
View File

@@ -0,0 +1,7 @@
new sst.x.DevCommand("Steam", {
dev: {
command: "bun dev",
directory: "packages/steam",
autostart: true,
},
});

View File

@@ -1,4 +1 @@
// export const vpc = new sst.aws.Vpc("Vpc")
// export const storage = new sst.aws.Efs("GameStorage",{ vpc })
// //
export const storage = new sst.aws.Bucket("Storage");

View File

@@ -1,103 +1,11 @@
// export const vpc = new aws.ec2.Vpc('NestriVpc', {
// cidrBlock: '172.16.0.0/16',
// });
import { isPermanentStage } from "./stage";
// export const subnet1 = new aws.ec2.Subnet('NestriSubnet1', {
// vpcId: vpc.id,
// cidrBlock: '172.16.1.0/24',
// // cidrBlock: '110.0.12.0/22',
// availabilityZone: 'us-east-1a',
// });
// export const subnet2 = new aws.ec2.Subnet('NestriSubnet2', {
// vpcId: vpc.id,
// cidrBlock: '172.16.2.0/24',
// // cidrBlock: '10.0.20.0/22',
// availabilityZone: 'us-east-1b',
// });
// const internetGateway = new aws.ec2.InternetGateway('NestriInternetGateway', {
// vpcId: vpc.id,
// });
// const routeTable = new aws.ec2.RouteTable('NestriRouteTable', {
// vpcId: vpc.id,
// routes: [
// {
// cidrBlock: '0.0.0.0/0',
// gatewayId: internetGateway.id,
// },
// ],
// });
// new aws.ec2.RouteTableAssociation('NestriSubnet1RouteTable', {
// subnetId: subnet1.id,
// routeTableId: routeTable.id,
// });
// new aws.ec2.RouteTableAssociation('NestriSubnet2RouteTable', {
// subnetId: subnet2.id,
// routeTableId: routeTable.id,
// });
// // const vpc = new sst.aws.Vpc("NestriRelayVpc")
// export const securityGroup = new aws.ec2.SecurityGroup("NestriSecurityGroup", {
// vpcId: vpc.id,
// description: "Managed thru SST",
// ingress: [
// {
// protocol: "tcp",
// fromPort: 80,
// toPort: 80,
// cidrBlocks: ["0.0.0.0/0"],
// },
// {
// protocol: "udp",
// fromPort: 10000,
// toPort: 20000,
// cidrBlocks: ["0.0.0.0/0"],
// },
// ],
// egress: [
// {
// protocol: "-1",
// cidrBlocks: ["0.0.0.0/0"],
// fromPort: 0,
// toPort: 0
// }
// ]
// });
// const loadBalancer = new aws.lb.LoadBalancer('NestriVpcLoadBalancer', {
// name: 'NestriVpcLoadBalancer',
// internal: false,
// securityGroups: [securityGroup.id],
// subnets: vpc.publicSubnets
// });
// const targetGroup = new aws.lb.TargetGroup('NestriVpcTargetGroup', {
// name: 'NestriVpcTargetGroup',
// port: 80,
// protocol: 'HTTP',
// targetType: 'ip',
// vpcId: vpc.id,
// healthCheck: {
// path: '/',
// protocol: 'HTTP',
// },
// });
// new aws.lb.Listener('NestriVpcLoadBalancerListener', {
// loadBalancerArn: loadBalancer.arn,
// port: 80,
// protocol: 'HTTP',
// defaultActions: [
// {
// type: 'forward',
// targetGroupArn: targetGroup.arn,
// },
// ],
// });
// // export const subnets = [subnet1, subnet2]
export const vpc = isPermanentStage
? new sst.aws.Vpc("VPC", {
az: 2,
// For lambdas to work in this VPC
nat: "ec2",
// For SST tunnel to work
bastion: true,
})
: sst.aws.Vpc.get("VPC", "vpc-0beb1cdc21a725748");

23
infra/www.ts Normal file
View File

@@ -0,0 +1,23 @@
// This is the website part where people play and connect
import { api } from "./api";
import { auth } from "./auth";
import { zero } from "./zero";
import { domain } from "./dns";
new sst.aws.StaticSite("Web", {
path: "packages/www",
build: {
output: "./dist",
command: "bun run build",
},
domain: {
dns: sst.cloudflare.dns(),
name: "console." + domain
},
environment: {
VITE_API_URL: api.url,
VITE_STAGE: $app.stage,
VITE_AUTH_URL: auth.url,
VITE_ZERO_URL: zero.url,
},
})

196
infra/zero.ts Normal file
View File

@@ -0,0 +1,196 @@
import { vpc } from "./vpc";
import { auth } from "./auth";
import { domain } from "./dns";
import { readFileSync } from "fs";
import { cluster } from "./cluster";
import { storage } from "./storage";
import { postgres } from "./postgres";
// const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}/${postgres.database}`
const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}:${postgres.port}/${postgres.database}`;
const tag = $dev
? `latest`
: JSON.parse(
readFileSync("./node_modules/@rocicorp/zero/package.json").toString(),
).version.replace("+", "-");
const zeroEnv = {
FORCE: "1",
NO_COLOR: "1",
ZERO_LOG_LEVEL: "info",
ZERO_LITESTREAM_LOG_LEVEL: "info",
ZERO_UPSTREAM_DB: connectionString,
ZERO_IMAGE_URL: `rocicorp/zero:${tag}`,
ZERO_CVR_DB: connectionString,
ZERO_CHANGE_DB: connectionString,
ZERO_REPLICA_FILE: "/tmp/nestri.db",
ZERO_LITESTREAM_RESTORE_PARALLELISM: "64",
ZERO_SHARD_ID: $app.stage,
ZERO_AUTH_JWKS_URL: $interpolate`${auth.url}/.well-known/jwks.json`,
...($dev
? {
}
: {
ZERO_LITESTREAM_BACKUP_URL: $interpolate`s3://${storage.name}/zero`,
}),
};
// Replication Manager Service
const replicationManager = !$dev
? new sst.aws.Service(`ZeroReplication`, {
cluster,
wait: true,
...($app.stage === "production"
? {
cpu: "2 vCPU",
memory: "4 GB",
}
: {}),
architecture: "arm64",
image: zeroEnv.ZERO_IMAGE_URL,
link: [storage, postgres],
health: {
command: ["CMD-SHELL", "curl -f http://localhost:4849/ || exit 1"],
interval: "5 seconds",
retries: 3,
startPeriod: "300 seconds",
},
environment: {
...zeroEnv,
ZERO_CHANGE_MAX_CONNS: "3",
ZERO_NUM_SYNC_WORKERS: "0",
},
logging: {
retention: "1 month",
},
loadBalancer: {
public: false,
ports: [
{
listen: "80/http",
forward: "4849/http",
},
],
},
transform: {
loadBalancer: {
idleTimeout: 3600,
},
service: {
healthCheckGracePeriodSeconds: 900,
},
},
}) : undefined;
// Permissions deployment
const permissions = new sst.aws.Function(
"ZeroPermissions",
{
vpc,
link: [postgres],
handler: "packages/functions/src/zero.handler",
// environment: { ["ZERO_UPSTREAM_DB"]: connectionString },
copyFiles: [{
from: "packages/zero/.permissions.sql",
to: "./.permissions.sql"
}],
}
);
if (replicationManager) {
new aws.lambda.Invocation(
"ZeroPermissionsInvocation",
{
input: Date.now().toString(),
functionName: permissions.name,
},
{ dependsOn: replicationManager }
);
// new command.local.Command(
// "ZeroPermission",
// {
// dir: process.cwd() + "/packages/zero",
// environment: {
// ZERO_UPSTREAM_DB: connectionString,
// },
// create: "bun run zero-deploy-permissions",
// triggers: [Date.now()],
// },
// {
// dependsOn: [replicationManager],
// },
// );
}
export const zero = new sst.aws.Service("Zero", {
cluster,
image: zeroEnv.ZERO_IMAGE_URL,
link: [storage, postgres],
architecture: "arm64",
...($app.stage === "production"
? {
cpu: "2 vCPU",
memory: "4 GB",
capacity: "spot"
}
: {
capacity: "spot"
}),
environment: {
...zeroEnv,
...($dev
? {
ZERO_NUM_SYNC_WORKERS: "1",
}
: {
ZERO_CHANGE_STREAMER_URI: replicationManager.url.apply((val) =>
val.replace("http://", "ws://"),
),
ZERO_UPSTREAM_MAX_CONNS: "15",
ZERO_CVR_MAX_CONNS: "160",
}),
},
wait: true,
health: {
retries: 3,
command: ["CMD-SHELL", "curl -f http://localhost:4848/ || exit 1"],
interval: "5 seconds",
startPeriod: "300 seconds",
},
loadBalancer: {
domain: {
name: "zero." + domain,
dns: sst.cloudflare.dns()
},
rules: [
{ listen: "443/https", forward: "4848/http" },
{ listen: "80/http", forward: "4848/http" },
],
},
scaling: {
min: 1,
max: 4,
},
logging: {
retention: "1 month",
},
transform: {
service: {
healthCheckGracePeriodSeconds: 900,
},
// taskDefinition: {
// ephemeralStorage: {
// sizeInGib: 200,
// },
// },
loadBalancer: {
idleTimeout: 3600,
},
},
dev: {
command: "bun dev",
directory: "packages/zero",
url: "http://localhost:4848",
},
});

29
nestri.sln Normal file
View File

@@ -0,0 +1,29 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "packages", "packages", "{809F86A1-1C4C-B159-0CD4-DF9D33D876CE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "steam", "packages\steam\steam.csproj", "{96118F95-BF02-0ED3-9042-36FA1B740D67}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{96118F95-BF02-0ED3-9042-36FA1B740D67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{96118F95-BF02-0ED3-9042-36FA1B740D67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{96118F95-BF02-0ED3-9042-36FA1B740D67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{96118F95-BF02-0ED3-9042-36FA1B740D67}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{96118F95-BF02-0ED3-9042-36FA1B740D67} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {526AD703-4D15-43CF-B7C0-83F10D3158DB}
EndGlobalSection
EndGlobal

View File

@@ -1,34 +1,41 @@
{
"name": "nestri",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"sst": "sst dev",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"lint": "turbo lint"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20240821.1",
"@pulumi/pulumi": "^3.134.0",
"@types/aws-lambda": "8.10.145",
"@types/aws-lambda": "8.10.147",
"prettier": "^3.2.5",
"typescript": "^5.4.5"
},
"engines": {
"node": ">=18"
},
"packageManager": "bun@1.1.18",
"packageManager": "bun@1.2.4",
"private": true,
"scripts": {
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"sso": "aws sso login --sso-session=nestri --no-browser --use-device-code"
},
"overrides": {
"@openauthjs/openauth": "0.4.3",
"@rocicorp/zero": "0.16.2025022000"
},
"patchedDependencies": {
"@macaron-css/solid@1.5.3": "patches/@macaron-css%2Fsolid@1.5.3.patch",
"drizzle-orm@0.36.1": "patches/drizzle-orm@0.36.1.patch"
},
"trustedDependencies": [
"core-js-pure",
"esbuild",
"protobufjs",
"@rocicorp/zero-sqlite3",
"workerd"
],
"workspaces": [
"apps/*",
"packages/*"
],
"trustedDependencies": [
"core-js-pure",
"esbuild",
"workerd"
],
"dependencies": {
"sst": "3.6.27"
"sst": "^3.11.21"
}
}
}

View File

@@ -0,0 +1,19 @@
import { Resource } from "sst";
import { defineConfig } from "drizzle-kit";
const connection = {
user: Resource.Database.username,
password: Resource.Database.password,
host: Resource.Database.host,
};
export default defineConfig({
verbose: true,
strict: true,
out: "./migrations",
dialect: "postgresql",
dbCredentials: {
url: `postgres://${connection.user}:${connection.password}@${connection.host}/nestri`,
},
schema: "./src/**/*.sql.ts",
});

View File

@@ -1,30 +0,0 @@
// Docs: https://www.instantdb.com/docs/permissions
import type { InstantRules } from "@instantdb/core";
const rules = {
/**
* Welcome to Instant's permission system!
* Right now your rules are empty. To start filling them in, check out the docs:
* https://www.instantdb.com/docs/permissions
*
* Here's an example to give you a feel:
* posts: {
* allow: {
* view: "true",
* create: "isOwner",
* update: "isOwner",
* delete: "isOwner",
* },
* bind: ["isOwner", "auth.id != null && auth.id == data.ownerId"],
* },
*/
// $default: {
// allow: {
// $default: "isOwner"
// },
// bind: ["isOwner", "auth.id != null && auth.id == data.ownerID"],
// }
} satisfies InstantRules;
export default rules;

View File

@@ -1,123 +0,0 @@
import { i } from "@instantdb/core";
const _schema = i.schema({
entities: {
$users: i.entity({
email: i.string().unique().indexed(),
}),
// machines: i.entity({
// hostname: i.string(),
// fingerprint: i.string().unique().indexed(),
// deletedAt: i.date().optional().indexed(),
// createdAt: i.date()
// }),
tasks: i.entity({
type: i.string(),
lastStatus: i.string(),
healthStatus: i.string(),
startedAt: i.string(),
lastUpdated: i.date(),
stoppedAt: i.string().optional(),
taskID: i.string().unique().indexed()
}),
instances: i.entity({
hostname: i.string(),
lastActive: i.date().optional(),
createdAt: i.date()
}),
profiles: i.entity({
avatarUrl: i.string().optional(),
username: i.string().indexed(),
status: i.string().indexed(),
updatedAt: i.date().indexed(),
createdAt: i.date(),
discriminator: i.string().indexed()
}),
teams: i.entity({
name: i.string(),
slug: i.string().unique().indexed(),
deletedAt: i.date().optional(),//.indexed(),
updatedAt: i.date(),
createdAt: i.date(),
}),
// games: i.entity({
// name: i.string(),
// steamID: i.number().unique().indexed(),
// }),
sessions: i.entity({
startedAt: i.date(),
endedAt: i.date().optional().indexed(),
public: i.boolean().indexed(),
}),
subscriptions: i.entity({
checkoutID: i.string(),
canceledAt: i.date(),
})
},
links: {
UserSubscriptions: {
forward: { on: "subscriptions", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "subscriptions" }
},
UserProfiles: {
forward: { on: "profiles", has: "one", label: "owner" },
reverse: { on: "$users", has: "one", label: "profile" }
},
UserTasks: {
forward: { on: "tasks", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "tasks" }
},
TaskSessions: {
forward: { on: "tasks", has: "many", label: "sessions" },
reverse: { on: "sessions", has: "one", label: "task" }
},
UserSession: {
forward: { on: "sessions", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "sessions" }
},
TeamsOwned: {
forward: { on: "teams", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "teamsOwned" },
},
TeamsJoined: {
forward: { on: "teams", has: "many", label: "members" },
reverse: { on: "$users", has: "many", label: "teamsJoined" },
},
// UserMachines: {
// forward: { on: "machines", has: "one", label: "owner" },
// reverse: { on: "$users", has: "many", label: "machines" }
// },
// UserGames: {
// forward: { on: "games", has: "many", label: "owners" },
// reverse: { on: "$users", has: "many", label: "games" }
// },
// TeamInstances: {
// forward: { on: "instances", has: "many", label: "owners" },
// reverse: { on: "teams", has: "many", label: "instances" }
// },
// MachineSessions: {
// forward: { on: "machines", has: "many", label: "sessions" },
// reverse: { on: "sessions", has: "one", label: "machine" }
// },
// GamesMachines: {
// forward: { on: "machines", has: "many", label: "games" },
// reverse: { on: "games", has: "many", label: "machines" }
// },
// GameSessions: {
// forward: { on: "games", has: "many", label: "sessions" },
// reverse: { on: "sessions", has: "one", label: "game" }
// },
// UserSessions: {
// forward: { on: "sessions", has: "one", label: "owner" },
// reverse: { on: "$users", has: "many", label: "sessions" }
// }
}
});
// This helps Typescript display nicer intellisense
type _AppSchema = typeof _schema;
interface AppSchema extends _AppSchema { }
const schema: AppSchema = _schema;
export type { AppSchema };
export default schema;

View File

@@ -0,0 +1,39 @@
CREATE TABLE "member" (
"id" char(30) NOT NULL,
"team_id" char(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"time_seen" timestamp with time zone,
"email" varchar(255) NOT NULL,
CONSTRAINT "member_team_id_id_pk" PRIMARY KEY("team_id","id")
);
--> statement-breakpoint
CREATE TABLE "team" (
"id" char(30) PRIMARY KEY NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"name" varchar(255) NOT NULL,
"slug" varchar(255) NOT NULL,
"plan_type" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" char(30) PRIMARY KEY NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"avatar_url" text,
"name" varchar(255) NOT NULL,
"discriminator" integer NOT NULL,
"email" varchar(255) NOT NULL,
"polar_customer_id" varchar(255),
"flags" json DEFAULT '{}'::json,
CONSTRAINT "user_polar_customer_id_unique" UNIQUE("polar_customer_id")
);
--> statement-breakpoint
CREATE INDEX "email_global" ON "member" USING btree ("email");--> statement-breakpoint
CREATE UNIQUE INDEX "member_email" ON "member" USING btree ("team_id","email");--> statement-breakpoint
CREATE UNIQUE INDEX "team_slug" ON "team" USING btree ("slug");--> statement-breakpoint
CREATE UNIQUE INDEX "user_email" ON "user" USING btree ("email");

View File

@@ -0,0 +1,2 @@
DROP INDEX "team_slug";--> statement-breakpoint
CREATE UNIQUE INDEX "slug" ON "team" USING btree ("slug");

View File

@@ -0,0 +1,17 @@
CREATE TABLE "steam" (
"id" char(30) NOT NULL,
"user_id" char(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"avatar_url" text NOT NULL,
"access_token" text NOT NULL,
"email" varchar(255) NOT NULL,
"country" varchar(255) NOT NULL,
"username" varchar(255) NOT NULL,
"persona_name" varchar(255) NOT NULL,
CONSTRAINT "steam_user_id_id_pk" PRIMARY KEY("user_id","id")
);
--> statement-breakpoint
CREATE INDEX "global_steam_email" ON "steam" USING btree ("email");--> statement-breakpoint
CREATE UNIQUE INDEX "steam_email" ON "steam" USING btree ("user_id","email");

View File

@@ -0,0 +1,13 @@
CREATE TABLE "machine" (
"id" char(30) PRIMARY KEY NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"country" text NOT NULL,
"timezone" text NOT NULL,
"location" "point" NOT NULL,
"fingerprint" varchar(32) NOT NULL,
"country_code" varchar(2) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "machine_fingerprint" ON "machine" USING btree ("fingerprint");

View File

@@ -0,0 +1,22 @@
CREATE TABLE "machine" (
"id" char(30) PRIMARY KEY NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"country" text NOT NULL,
"timezone" text NOT NULL,
"location" "point" NOT NULL,
"fingerprint" varchar(32) NOT NULL,
"country_code" varchar(2) NOT NULL
);
--> statement-breakpoint
ALTER TABLE "steam" RENAME COLUMN "country" TO "country_code";--> statement-breakpoint
DROP INDEX "global_steam_email";--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "time_seen" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "steam_id" integer NOT NULL;--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "last_game" json NOT NULL;--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "steam_email" varchar(255) NOT NULL;--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "limitation" json NOT NULL;--> statement-breakpoint
CREATE UNIQUE INDEX "machine_fingerprint" ON "machine" USING btree ("fingerprint");--> statement-breakpoint
ALTER TABLE "steam" DROP COLUMN "access_token";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "flags";

View File

@@ -0,0 +1,8 @@
ALTER TABLE "steam" RENAME COLUMN "time_seen" TO "last_seen";--> statement-breakpoint
DROP INDEX "steam_email";--> statement-breakpoint
ALTER TABLE "steam" DROP CONSTRAINT "steam_user_id_id_pk";--> statement-breakpoint
ALTER TABLE "steam" ADD PRIMARY KEY ("id");--> statement-breakpoint
ALTER TABLE "machine" ADD CONSTRAINT "machine_user_id_id_pk" PRIMARY KEY("user_id","id");--> statement-breakpoint
ALTER TABLE "machine" ADD COLUMN "user_id" char(30);--> statement-breakpoint
ALTER TABLE "steam" ADD CONSTRAINT "steam_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "steam" DROP COLUMN "email";

View File

@@ -0,0 +1,2 @@
ALTER TABLE "machine" DROP CONSTRAINT "machine_user_id_id_pk";--> statement-breakpoint
ALTER TABLE "machine" DROP COLUMN "user_id";

View File

@@ -0,0 +1,2 @@
ALTER TABLE "member" ADD COLUMN "role" text NOT NULL;--> statement-breakpoint
ALTER TABLE "team" DROP COLUMN "plan_type";

View File

@@ -0,0 +1,15 @@
CREATE TABLE "subscription" (
"id" char(30) NOT NULL,
"user_id" char(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"team_id" char(30) NOT NULL,
"standing" text NOT NULL,
"plan_type" text NOT NULL,
"tokens" integer NOT NULL,
"product_id" varchar(255),
"subscription_id" varchar(255)
);
--> statement-breakpoint
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_id_team_id_pk" PRIMARY KEY("id","team_id");--> statement-breakpoint
CREATE UNIQUE INDEX "subscription_id" ON "subscription" USING btree ("id");--> statement-breakpoint
CREATE INDEX "subscription_user_id" ON "subscription" USING btree ("user_id");

View File

@@ -0,0 +1,2 @@
CREATE UNIQUE INDEX "steam_id" ON "steam" USING btree ("steam_id");--> statement-breakpoint
CREATE INDEX "steam_user_id" ON "steam" USING btree ("user_id");

View File

@@ -0,0 +1,294 @@
{
"id": "f09034df-208a-42b3-b61f-f842921c6e24",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"team_slug": {
"name": "team_slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"flags": {
"name": "flags",
"type": "json",
"primaryKey": false,
"notNull": false,
"default": "'{}'::json"
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,294 @@
{
"id": "6f428226-b5d8-4182-a676-d04f842f9ded",
"prevId": "f09034df-208a-42b3-b61f-f842921c6e24",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"flags": {
"name": "flags",
"type": "json",
"primaryKey": false,
"notNull": false,
"default": "'{}'::json"
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,420 @@
{
"id": "227c54d2-b643-48d5-964b-af6fe004369a",
"prevId": "6f428226-b5d8-4182-a676-d04f842f9ded",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"global_steam_email": {
"name": "global_steam_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"steam_email": {
"name": "steam_email",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"steam_user_id_id_pk": {
"name": "steam_user_id_id_pk",
"columns": [
"user_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"flags": {
"name": "flags",
"type": "json",
"primaryKey": false,
"notNull": false,
"default": "'{}'::json"
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,507 @@
{
"id": "eb5d41aa-5f85-4b2d-8633-fc021b211241",
"prevId": "227c54d2-b643-48d5-964b-af6fe004369a",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"steam_email": {
"name": "steam_email",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"steam_user_id_id_pk": {
"name": "steam_user_id_id_pk",
"columns": [
"user_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,499 @@
{
"id": "65574f71-e0d3-4363-9449-394e7c376a30",
"prevId": "eb5d41aa-5f85-4b2d-8633-fc021b211241",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"machine_user_id_id_pk": {
"name": "machine_user_id_id_pk",
"columns": [
"user_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,485 @@
{
"id": "0b04858c-a7e3-43b6-98a4-1dc2f6f97488",
"prevId": "65574f71-e0d3-4363-9449-394e7c376a30",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,485 @@
{
"id": "69827225-1351-4709-a9b2-facb0f569215",
"prevId": "0b04858c-a7e3-43b6-98a4-1dc2f6f97488",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,580 @@
{
"id": "fff2b73d-85ab-48bc-86de-69d3caf317f0",
"prevId": "69827225-1351-4709-a9b2-facb0f569215",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription": {
"name": "subscription",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"standing": {
"name": "standing",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tokens": {
"name": "tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"subscription_id": {
"name": "subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"subscription_team_id_team_id_fk": {
"name": "subscription_team_id_team_id_fk",
"tableFrom": "subscription",
"tableTo": "team",
"columnsFrom": [
"team_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,619 @@
{
"id": "17b9c14f-ff15-44a5-9aaf-3f3b7dd7d294",
"prevId": "fff2b73d-85ab-48bc-86de-69d3caf317f0",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription": {
"name": "subscription",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"standing": {
"name": "standing",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tokens": {
"name": "tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"subscription_id": {
"name": "subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"subscription_id": {
"name": "subscription_id",
"columns": [
{
"expression": "id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"subscription_user_id": {
"name": "subscription_user_id",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"subscription_team_id_team_id_fk": {
"name": "subscription_team_id_team_id_fk",
"tableFrom": "subscription",
"tableTo": "team",
"columnsFrom": [
"team_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"subscription_id_team_id_pk": {
"name": "subscription_id_team_id_pk",
"columns": [
"id",
"team_id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,650 @@
{
"id": "1717c769-cee0-4242-bcbb-9538c80d985c",
"prevId": "17b9c14f-ff15-44a5-9aaf-3f3b7dd7d294",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"steam_id": {
"name": "steam_id",
"columns": [
{
"expression": "steam_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"steam_user_id": {
"name": "steam_user_id",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription": {
"name": "subscription",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"standing": {
"name": "standing",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tokens": {
"name": "tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"subscription_id": {
"name": "subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"subscription_id": {
"name": "subscription_id",
"columns": [
{
"expression": "id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"subscription_user_id": {
"name": "subscription_user_id",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"subscription_team_id_team_id_fk": {
"name": "subscription_team_id_team_id_fk",
"tableFrom": "subscription",
"tableTo": "team",
"columnsFrom": [
"team_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"subscription_id_team_id_pk": {
"name": "subscription_id_team_id_pk",
"columns": [
"id",
"team_id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,76 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1741759978256,
"tag": "0000_flaky_matthew_murdock",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1741955636085,
"tag": "0001_nifty_sauron",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1743794969007,
"tag": "0002_simple_outlaw_kid",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1744287542918,
"tag": "0003_first_big_bertha",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1744614629788,
"tag": "0004_amused_mattie_franklin",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1744614896792,
"tag": "0005_aspiring_stature",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1744634229644,
"tag": "0006_worthless_dreadnoughts",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1744634322996,
"tag": "0007_warm_secret_warriors",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1744651530530,
"tag": "0008_third_mindworm",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1744651817581,
"tag": "0009_luxuriant_wraith",
"breakpoints": true
}
]
}

View File

@@ -3,6 +3,13 @@
"version": "0.0.0",
"sideEffects": false,
"type": "module",
"scripts": {
"db:dev": "drizzle-kit",
"typecheck": "tsc --noEmit",
"db": "sst shell drizzle-kit",
"db:exec": "sst shell ../scripts/src/psql.sh",
"db:reset": "sst shell ../scripts/src/db-reset.sh"
},
"exports": {
"./*": "./src/*.ts"
},
@@ -12,13 +19,22 @@
"aws4fetch": "^1.0.20",
"loops": "^3.4.1",
"mqtt": "^5.10.3",
"remeda": "^2.19.0",
"remeda": "^2.21.2",
"ulid": "^2.3.0",
"uuid": "^11.0.3",
"zod": "^3.24.1",
"zod-openapi": "^4.2.2"
},
"dependencies": {
"@instantdb/admin": "^0.17.7"
"@aws-sdk/client-iot-data-plane": "^3.758.0",
"@aws-sdk/client-rds-data": "^3.758.0",
"@aws-sdk/client-sesv2": "^3.753.0",
"@instantdb/admin": "^0.17.7",
"@openauthjs/openauth": "*",
"@openauthjs/openevent": "^0.0.27",
"@polar-sh/sdk": "^0.26.1",
"drizzle-kit": "^0.30.5",
"drizzle-orm": "^0.40.0",
"postgres": "^3.4.5"
}
}

View File

@@ -1,86 +1,142 @@
import { z } from "zod";
import { eq } from "./drizzle";
import { ErrorCodes, VisibleError } from "./error";
import { createContext } from "./context";
import { VisibleError } from "./error";
export interface UserActor {
type: "user";
properties: {
accessToken: string;
userID: string;
auth?:
| {
type: "personal";
token: string;
}
| {
type: "oauth";
clientID: string;
};
};
import { UserFlags, userTable } from "./user/user.sql";
import { useTransaction } from "./drizzle/transaction";
export const PublicActor = z.object({
type: z.literal("public"),
properties: z.object({}),
});
export type PublicActor = z.infer<typeof PublicActor>;
export const UserActor = z.object({
type: z.literal("user"),
properties: z.object({
userID: z.string(),
email: z.string().nonempty(),
}),
});
export type UserActor = z.infer<typeof UserActor>;
export const MemberActor = z.object({
type: z.literal("member"),
properties: z.object({
memberID: z.string(),
teamID: z.string(),
}),
});
export type MemberActor = z.infer<typeof MemberActor>;
export const SystemActor = z.object({
type: z.literal("system"),
properties: z.object({
teamID: z.string(),
}),
});
export type SystemActor = z.infer<typeof SystemActor>;
export const MachineActor = z.object({
type: z.literal("machine"),
properties: z.object({
fingerprint: z.string(),
machineID: z.string(),
}),
});
export type MachineActor = z.infer<typeof MachineActor>;
export const Actor = z.discriminatedUnion("type", [
MemberActor,
UserActor,
PublicActor,
SystemActor,
MachineActor
]);
export type Actor = z.infer<typeof Actor>;
export const ActorContext = createContext<Actor>("actor");
export const useActor = ActorContext.use;
export const withActor = ActorContext.with;
/**
* Retrieves the user ID of the current actor.
*
* This function accesses the actor context and returns the `userID` if the current
* actor is of type "user". If the actor is not a user, it throws a `VisibleError`
* with an authentication error code, indicating that the caller is not authorized
* to access user-specific resources.
*
* @throws {VisibleError} When the current actor is not of type "user".
*/
export function useUserID() {
const actor = ActorContext.use();
if (actor.type === "user") return actor.properties.userID;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource`,
);
}
export interface DeviceActor {
type: "device";
properties: {
teamSlug: string;
hostname: string;
auth?:
| {
type: "personal";
token: string;
}
| {
type: "oauth";
clientID: string;
};
};
/**
* Retrieves the properties of the current user actor.
*
* This function obtains the current actor from the context and returns its properties if the actor is identified as a user.
* If the actor is not of type "user", it throws a {@link VisibleError} with an authentication error code,
* indicating that the user is not authorized to access user-specific resources.
*
* @returns The properties of the current user actor, typically including user-specific details such as userID and email.
* @throws {VisibleError} If the current actor is not a user.
*/
export function useUser() {
const actor = ActorContext.use();
if (actor.type === "user") return actor.properties;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource`,
);
}
export interface PublicActor {
type: "public";
properties: {};
}
type Actor = UserActor | PublicActor | DeviceActor;
export const ActorContext = createContext<Actor>();
export function useCurrentUser() {
const actor = ActorContext.use();
if (actor.type === "user") return {
id:actor.properties.userID,
token: actor.properties.accessToken,
};
throw new VisibleError(
"auth",
"unauthorized",
`You don't have permission to access this resource`,
);
}
export function useCurrentDevice() {
const actor = ActorContext.use();
if (actor.type === "device") return {
hostname:actor.properties.hostname,
teamSlug: actor.properties.teamSlug
};
throw new VisibleError(
"auth",
"unauthorized",
`You don't have permission to access this resource`,
);
}
export function useActor() {
try {
return ActorContext.use();
} catch {
return { type: "public", properties: {} } as PublicActor;
}
export function assertActor<T extends Actor["type"]>(type: T) {
const actor = useActor();
if (actor.type !== type) {
throw new Error(`Expected actor type ${type}, got ${actor.type}`);
}
export function assertActor<T extends Actor["type"]>(type: T) {
const actor = useActor();
if (actor.type !== type)
throw new VisibleError("auth", "actor.invalid", `Actor is not "${type}"`);
return actor as Extract<Actor, { type: T }>;
}
return actor as Extract<Actor, { type: T }>;
}
/**
* Returns the current actor's team ID.
*
* @returns The team ID associated with the current actor.
* @throws {VisibleError} If the current actor does not have a {@link teamID} property.
*/
export function useTeam() {
const actor = useActor();
if ("teamID" in actor.properties) return actor.properties.teamID;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Expected actor to have teamID`
);
}
/**
* Returns the fingerprint of the current actor if the actor has a machine identity.
*
* @returns The fingerprint of the current machine actor.
* @throws {VisibleError} If the current actor does not have a machine identity.
*/
export function useMachine() {
const actor = useActor();
if ("machineID" in actor.properties) return actor.properties.fingerprint;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Expected actor to have fingerprint`
);
}

View File

@@ -1,90 +0,0 @@
import { z } from "zod"
import { Resource } from "sst";
import { doubleFn, fn } from "../utils";
import { AwsClient } from "aws4fetch";
import { DescribeTasksCommandOutput, StopTaskCommandOutput, type RunTaskCommandOutput } from "@aws-sdk/client-ecs";
export module Aws {
export const client = async () => {
return new AwsClient({
accessKeyId: Resource.AwsAccessKey.value,
secretAccessKey: Resource.AwsSecretKey.value,
region: "us-east-1",
});
}
export const EcsRunTask = fn(z.object({
cluster: z.string(),
count: z.number(),
taskDefinition: z.string(),
launchType: z.enum(["EC2", "FARGATE"]),
overrides: z.object({
containerOverrides: z.object({
name: z.string(),
environment: z.object({
name: z.string(),
value: z.string().or(z.number())
}).array()
}).array()
})
}), async (body) => {
const c = await client();
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
const res = await c.fetch(url, {
method: "POST",
headers: {
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.RunTask",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify(body)
})
return await res.json() as RunTaskCommandOutput
})
export const EcsDescribeTasks = fn(z.object({ tasks: z.string().array(), cluster: z.string() }), async (body) => {
const c = await client();
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
const res = await c.fetch(url, {
method: "POST",
headers: {
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.DescribeTasks",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify(body)
})
return await res.json() as DescribeTasksCommandOutput
})
export const EcsStopTask = fn(z.object({
cluster: z.string().optional(),
reason: z.string().optional(),
task: z.string()
}), async (body) => {
const c = await client();
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
const res = await c.fetch(url, {
method: "POST",
headers: {
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.StopTask",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify(body)
})
return await res.json() as StopTaskCommandOutput
})
}

View File

@@ -1,7 +1,11 @@
import { sql } from "drizzle-orm";
import { z } from "zod";
import "zod-openapi/extend";
export module Common {
export namespace Common {
export const IdDescription = `Unique object identifier.
The format and length of IDs may change over time.`;
export const now = () => sql`now()`;
export const utc = () => sql`now() at time zone 'utc'`;
}

View File

@@ -1,17 +1,17 @@
import { AsyncLocalStorage } from "node:async_hooks";
export function createContext<T>() {
export function createContext<T>(name: string) {
const storage = new AsyncLocalStorage<T>();
return {
use() {
const result = storage.getStore();
if (!result) {
throw new Error("No context available");
throw new Error("Context not provided: " + name);
}
return result;
},
with<R>(value: T, fn: () => R) {
return storage.run<R>(value, fn);
return storage.run<R, any[]>(value, fn);
},
};
}

View File

@@ -1,12 +0,0 @@
import { Resource } from "sst";
import { init } from "@instantdb/admin";
import schema from "../instant.schema";
const databaseClient = () => init({
appId: Resource.InstantAppId.value,
adminToken: Resource.InstantAdminToken.value,
schema
})
export default databaseClient

View File

@@ -0,0 +1,17 @@
export * from "drizzle-orm";
import { Resource } from "sst";
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
const client = postgres({
idle_timeout: 30000,
connect_timeout: 30000,
host: Resource.Database.host,
database: Resource.Database.database,
user: Resource.Database.username,
password: Resource.Database.password,
port: Resource.Database.port,
max: parseInt(process.env.POSTGRES_POOL_MAX || "1"),
});
export const db = drizzle(client, {});

View File

@@ -0,0 +1,63 @@
import { db } from ".";
import {
PgTransaction,
PgTransactionConfig
} from "drizzle-orm/pg-core";
import {
PostgresJsQueryResultHKT
} from "drizzle-orm/postgres-js";
import { ExtractTablesWithRelations } from "drizzle-orm";
import { createContext } from "../context";
export type Transaction = PgTransaction<
PostgresJsQueryResultHKT,
Record<string, never>,
ExtractTablesWithRelations<Record<string, never>>
>;
type TxOrDb = Transaction | typeof db;
const TransactionContext = createContext<{
tx: Transaction;
effects: (() => void | Promise<void>)[];
}>("TransactionContext");
export async function useTransaction<T>(callback: (trx: TxOrDb) => Promise<T>) {
try {
const { tx } = TransactionContext.use();
return callback(tx);
} catch {
return callback(db);
}
}
export async function afterTx(effect: () => any | Promise<any>) {
try {
const { effects } = TransactionContext.use();
effects.push(effect);
} catch {
await effect();
}
}
export async function createTransaction<T>(
callback: (tx: Transaction) => Promise<T>,
isolationLevel?: PgTransactionConfig["isolationLevel"],
): Promise<T> {
try {
const { tx } = TransactionContext.use();
return callback(tx);
} catch {
const effects: (() => void | Promise<void>)[] = [];
const result = await db.transaction(
async (tx) => {
return TransactionContext.with({ tx, effects }, () => callback(tx));
},
{
isolationLevel: isolationLevel || "read committed",
},
);
await Promise.all(effects.map((x) => x()));
return result as T;
}
}

View File

@@ -0,0 +1,40 @@
import { char, timestamp as rawTs } from "drizzle-orm/pg-core";
import { teamTable } from "../team/team.sql";
export const ulid = (name: string) => char(name, { length: 26 + 4 });
export const id = {
get id() {
return ulid("id").primaryKey().notNull();
},
};
export const teamID = {
get id() {
return ulid("id").notNull();
},
get teamID() {
return ulid("team_id").notNull();
},
};
export const userID = {
get id() {
return ulid("id").notNull();
},
get userID() {
return ulid("user_id").notNull();
},
};
export const utc = (name: string) =>
rawTs(name, {
withTimezone: true,
// mode: "date"
});
export const timestamps = {
timeCreated: utc("time_created").notNull().defaultNow(),
timeUpdated: utc("time_updated").notNull().defaultNow(),
timeDeleted: utc("time_deleted"),
};

View File

@@ -1,45 +1,36 @@
import { LoopsClient } from "loops";
import { Resource } from "sst/resource"
import { Resource } from "sst";
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
export namespace Email {
export const Client = () => new LoopsClient(Resource.LoopsApiKey.value);
export const Client = new SESv2Client({});
export async function send(
to: string,
body: string,
) {
try {
await Client().sendTransactionalEmail(
{
transactionalId: "cm58pdf8d03upb5ecirnmvrfb",
email: to,
dataVariables: {
logincode: body
}
}
);
} catch (error) {
console.log("error sending email", error)
}
}
export async function sendWelcome(
to: string,
name: string,
) {
try {
await Client().sendTransactionalEmail(
{
transactionalId: "cm61jrbbx02twlstfwfcywt5u",
email: to,
dataVariables: {
name
}
}
);
} catch (error) {
console.log("error sending email", error)
}
}
export async function send(
from: string,
to: string,
subject: string,
body: string,
) {
from = from + "@" + Resource.Email.sender;
console.log("sending email", subject, from, to);
await Client.send(
new SendEmailCommand({
Destination: {
ToAddresses: [to],
},
Content: {
Simple: {
Body: {
Text: {
Data: body,
},
},
Subject: {
Data: subject,
},
},
},
FromEmailAddress: `Nestri <${from}>`,
}),
);
}
}

View File

@@ -1,9 +1,145 @@
import { z } from "zod"
/**
* Standard error response schema used for OpenAPI documentation
*/
export const ErrorResponse = z
.object({
type: z
.enum([
"validation",
"authentication",
"forbidden",
"not_found",
"already_exists",
"rate_limit",
"internal",
])
.openapi({
description: "The error type category",
examples: ["validation", "authentication"],
}),
code: z.string().openapi({
description: "Machine-readable error code identifier",
examples: ["invalid_parameter", "missing_required_field", "unauthorized"],
}),
message: z.string().openapi({
description: "Human-readable error message",
examples: ["The request was invalid", "Authentication required"],
}),
param: z
.string()
.optional()
.openapi({
description: "The parameter that caused the error (if applicable)",
examples: ["email", "user_id", "team_id"],
}),
details: z.any().optional().openapi({
description: "Additional error context information",
}),
})
.openapi({ ref: "ErrorResponse" });
export type ErrorResponseType = z.infer<typeof ErrorResponse>;
/**
* Standardized error codes for the API
*/
export const ErrorCodes = {
// Validation errors (400)
Validation: {
MISSING_REQUIRED_FIELD: "missing_required_field",
ALREADY_EXISTS: "resource_already_exists",
TEAM_ALREADY_EXISTS: "team_already_exists",
INVALID_PARAMETER: "invalid_parameter",
INVALID_FORMAT: "invalid_format",
INVALID_STATE: "invalid_state",
IN_USE: "resource_in_use",
},
// Authentication errors (401)
Authentication: {
UNAUTHORIZED: "unauthorized",
INVALID_TOKEN: "invalid_token",
EXPIRED_TOKEN: "expired_token",
INVALID_CREDENTIALS: "invalid_credentials",
},
// Permission errors (403)
Permission: {
FORBIDDEN: "forbidden",
INSUFFICIENT_PERMISSIONS: "insufficient_permissions",
ACCOUNT_RESTRICTED: "account_restricted",
},
// Resource not found errors (404)
NotFound: {
RESOURCE_NOT_FOUND: "resource_not_found",
},
// Rate limit errors (429)
RateLimit: {
TOO_MANY_REQUESTS: "too_many_requests",
QUOTA_EXCEEDED: "quota_exceeded",
},
// Server errors (500)
Server: {
INTERNAL_ERROR: "internal_error",
SERVICE_UNAVAILABLE: "service_unavailable",
DEPENDENCY_FAILURE: "dependency_failure",
},
};
/**
* Standard error that will be exposed to clients through API responses
*/
export class VisibleError extends Error {
constructor(
public kind: "input" | "auth",
public code: string,
public message: string,
) {
super(message);
constructor(
public type: ErrorResponseType["type"],
public code: string,
public message: string,
public param?: string,
public details?: any,
) {
super(message);
}
/**
* Convert this error to an HTTP status code
*/
public statusCode(): number {
switch (this.type) {
case "validation":
return 400;
case "authentication":
return 401;
case "forbidden":
return 403;
case "not_found":
return 404;
case "already_exists":
return 409;
case "rate_limit":
return 429;
case "internal":
return 500;
}
}
}
/**
* Convert this error to a standard response object
*/
public toResponse(): ErrorResponseType {
const response: ErrorResponseType = {
type: this.type,
code: this.code,
message: this.message,
};
if (this.param) response.param = this.param;
if (this.details) response.details = this.details;
return response;
}
}

View File

@@ -0,0 +1,23 @@
import { useActor } from "./actor";
import { event as sstEvent } from "sst/event";
import { ZodValidator } from "sst/event/validator";
export const createEvent = sstEvent.builder({
validator: ZodValidator,
metadata() {
return {
actor: useActor(),
};
},
});
import { openevent } from "@openauthjs/openevent/event";
export { publish } from "@openauthjs/openevent/publisher/drizzle";
export const event = openevent({
metadata() {
return {
actor: useActor(),
};
},
});

View File

@@ -1,75 +1,81 @@
export module Examples {
import { prefixes } from "./utils";
export namespace Examples {
export const Id = (prefix: keyof typeof prefixes) =>
`${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
export const User = {
id: "0bfcc712-df13-4454-81a8-fbee66eddca4",
email: "john@example.com",
};
export const Task = {
id: "0bfcc712-df13-4454-81a8-fbee66eddca4",
taskID: "b8302fca2d224d91ab342a2e4ab926d3",
type: "AWS" as const, //or "on-premises",
lastStatus: "RUNNING" as const,
healthStatus: "UNKNOWN" as const,
startedAt: '2025-01-09T01:56:23.902Z',
lastUpdated: '2025-01-09T01:56:23.902Z',
stoppedAt: '2025-01-09T04:46:23.902Z'
export const Steam = {
id: Id("steam"),
userID: Id("user"),
countryCode: "KE",
steamID: 74839300282033,
limitation: {
isLimited: false,
isBanned: false,
isLocked: false,
isAllowedToInviteFriends: false,
},
lastGame: {
gameID: 2531310,
gameName: "The Last of Us™ Part II Remastered",
},
personaName: "John",
username: "johnsteamaccount",
steamEmail: "john@example.com",
avatarUrl: "https://avatars.akamai.steamstatic.com/XXXXXXXXXXXX_full.jpg",
}
export const Profile = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
username: "janedoe47",
status: "active" as const,
export const User = {
id: Id("user"),
name: "John Doe",
email: "john@example.com",
discriminator: 47,
avatarUrl: "https://cdn.discordapp.com/avatars/xxxxxxx/xxxxxxx.png",
discriminator: 12, //it needs to be two digits
createdAt: '2025-01-04T11:56:23.902Z',
updatedAt: '2025-01-09T01:56:23.902Z'
polarCustomerID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
steamAccounts: [Steam]
};
export const Product = {
id: Id("product"),
name: "RTX 4090",
description: "Ideal for dedicated gamers who crave more flexibility and social gaming experiences.",
tokensPerHour: 20,
}
export const Subscription = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
checkoutID: "0bfcb712-df43-4454-81a8-fbee66eddca4",
// productID: "0bfcb712-df43-4454-81a8-fbee66eddca4",
// quantity: 1,
// frequency: "monthly" as const,
// next: '2025-01-09T01:56:23.902Z',
canceledAt: '2025-02-09T01:56:23.902Z'
tokens: 100,
id: Id("subscription"),
userID: Id("user"),
teamID: Id("team"),
planType: "pro" as const, // free, pro, family, enterprise
standing: "new" as const, // new, good, overdue, cancelled
polarProductID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
polarSubscriptionID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
}
export const Member = {
id: Id("member"),
email: "john@example.com",
teamID: Id("team"),
role: "admin" as const,
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
}
export const Team = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
// owner: true,
name: "Jane Doe's Games",
slug: "jane-does-games",
createdAt: '2025-01-04T11:56:23.902Z',
updatedAt: '2025-01-09T01:56:23.902Z'
id: Id("team"),
name: "John Does' Team",
slug: "john_doe",
subscriptions: [Subscription],
members: [Member]
}
export const Machine = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
hostname: "DESKTOP-EUO8VSF",
fingerprint: "fc27f428f9ca47d4b41b70889ae0c62090",
createdAt: '2025-01-04T11:56:23.902Z',
deletedAt: '2025-01-09T01:56:23.902Z'
id: Id("machine"),
userID: Id("user"),
country: "Kenya",
countryCode: "KE",
timezone: "Africa/Nairobi",
location: { latitude: 36.81550, longitude: -1.28410 },
fingerprint: "fc27f428f9ca47d4b41b707ae0c62090",
}
export const Instance = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
hostname: "a955e059f05d",
createdAt: '2025-01-04T11:56:23.902Z',
lastActive: '2025-01-09T01:56:23.902Z'
}
export const Game = {
id: '0bfcb712-df13-4454-81a8-fbee66eddca4',
name: "Control Ultimate Edition",
steamID: 870780,
}
export const Session = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
public: true,
startedAt: '2025-01-04T11:56:23.902Z',
endedAt: '2025-01-04T12:36:23.902Z'
}
}

View File

@@ -1,151 +0,0 @@
// import { z } from "zod"
// import { fn } from "../utils";
// import { Common } from "../common";
// import { Examples } from "../examples";
// import databaseClient from "../database"
// import { id as createID } from "@instantdb/admin";
// import { groupBy, map, pipe, values } from "remeda"
// import { useCurrentDevice, useCurrentUser } from "../actor";
// export module Games {
// export const Info = z
// .object({
// id: z.string().openapi({
// description: Common.IdDescription,
// example: Examples.Game.id,
// }),
// name: z.string().openapi({
// description: "A human-readable name for the game, used for easy identification.",
// example: Examples.Game.name,
// }),
// steamID: z.number().openapi({
// description: "The Steam ID of the game, used to identify it during installation and runtime.",
// example: Examples.Game.steamID,
// })
// })
// .openapi({
// ref: "Game",
// description: "Represents a Steam game that can be installed and played on a machine.",
// example: Examples.Game,
// });
// export type Info = z.infer<typeof Info>;
// export const create = fn(Info.pick({ name: true, steamID: true }), async (input) => {
// const id = createID()
// const db = databaseClient()
// const device = useCurrentDevice()
// await db.transact(
// db.tx.games[id]!.update({
// name: input.name,
// steamID: input.steamID,
// }).link({ machines: device.id })
// )
// //
// return id
// })
// export const list = async () => {
// const db = databaseClient()
// const user = useCurrentUser()
// const query = {
// $users: {
// $: { where: { id: user.id } },
// games: {}
// },
// }
// const res = await db.query(query)
// const games = res.$users[0]?.games
// if (games && games.length > 0) {
// const result = pipe(
// games,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// name: group[0].name,
// steamID: group[0].steamID,
// }))
// )
// return result
// }
// return null
// }
// export const fromSteamID = fn(z.number(), async (steamID) => {
// const db = databaseClient()
// const query = {
// games: {
// $: {
// where: {
// steamID,
// }
// }
// }
// }
// const res = await db.query(query)
// const games = res.games
// if (games.length > 0) {
// const result = pipe(
// games,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// name: group[0].name,
// steamID: group[0].steamID,
// }))
// )
// return result[0]
// }
// return null
// })
// export const linkToCurrentUser = fn(z.string(), async (steamID) => {
// const user = useCurrentUser()
// const db = databaseClient()
// await db.transact(db.tx.games[steamID]!.link({ owners: user.id }))
// return "ok"
// })
// export const unLinkFromCurrentUser = fn(z.number(), async (steamID) => {
// const user = useCurrentUser()
// const db = databaseClient()
// const query = {
// $users: {
// $: { where: { id: user.id } },
// games: {
// $: {
// where: {
// steamID,
// }
// }
// }
// },
// }
// const res = await db.query(query)
// const games = res.$users[0]?.games
// if (games && games.length > 0) {
// const game = games[0] as Info
// await db.transact(db.tx.games[game.id]!.unlink({ owners: user.id }))
// return "ok"
// }
// return null
// })
// }

View File

@@ -1,83 +0,0 @@
import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { id as createID } from "@instantdb/admin";
import { groupBy, map, pipe, values } from "remeda"
export module Instances {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Instance.id,
}),
hostname: z.string().openapi({
description: "The container's hostname",
example: Examples.Instance.hostname,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time this instances was registered on the network",
example: Examples.Instance.createdAt,
}),
lastActive: z.string().or(z.number()).optional().openapi({
description: "The time this instance was last seen on the network",
example: Examples.Instance.lastActive,
})
})
.openapi({
ref: "Instance",
description: "Represents a running container that is connected to the Nestri network..",
example: Examples.Instance,
});
export type Info = z.infer<typeof Info>;
export const create = fn(z.object({ hostname: z.string(), teamID: z.string() }), async (input) => {
const id = createID()
const now = new Date().toISOString()
const db = databaseClient()
await db.transact(
db.tx.instances[id]!.update({
hostname: input.hostname,
createdAt: now,
}).link({ owners: input.teamID })
)
return "ok"
})
export const fromTeamID = fn(z.string(), async (teamID) => {
const db = databaseClient()
const query = {
instances: {
$: {
where: {
owners: teamID
}
}
}
}
const res = await db.query(query)
const data = res.instances
if (data && data.length > 0) {
const result = pipe(
data,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
lastActive: group[0].lastActive,
hostname: group[0].hostname,
createdAt: group[0].createdAt
}))
)
return result
}
return null
})
}

View File

@@ -1,232 +1,155 @@
// import { z } from "zod"
// import { fn } from "../utils";
// import { Games } from "../game"
// import { Common } from "../common";
// import { Examples } from "../examples";
// import { useCurrentUser } from "../actor";
// import databaseClient from "../database"
// import { id as createID } from "@instantdb/admin";
// import { groupBy, map, pipe, values } from "remeda"
// export module Machines {
// export const Info = z
// .object({
// id: z.string().openapi({
// description: Common.IdDescription,
// example: Examples.Machine.id,
// }),
// hostname: z.string().openapi({
// description: "The Linux hostname that identifies this machine",
// example: Examples.Machine.hostname,
// }),
// fingerprint: z.string().openapi({
// description: "A unique identifier derived from the machine's Linux machine ID.",
// example: Examples.Machine.fingerprint,
// }),
// createdAt: z.string().or(z.number()).openapi({
// description: "Represents a machine running on the Nestri network, containing its identifying information and metadata.",
// example: Examples.Machine.createdAt,
// })
// })
// .openapi({
// ref: "Machine",
// description: "Represents a physical or virtual machine connected to the Nestri network..",
// example: Examples.Machine,
// });
import { z } from "zod";
import { Common } from "../common";
import { createID, fn } from "../utils";
import { Examples } from "../examples";
import { machineTable } from "./machine.sql";
import { getTableColumns, eq, sql, and, isNull } from "../drizzle";
import { createTransaction, useTransaction } from "../drizzle/transaction";
// export type Info = z.infer<typeof Info>;
export namespace Machine {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Machine.id,
}),
// userID: z.string().nullable().openapi({
// description: "The userID of the user who owns this machine, in the case of BYOG",
// example: Examples.Machine.userID
// }),
country: z.string().openapi({
description: "The fullname of the country this machine is running in",
example: Examples.Machine.country
}),
fingerprint: z.string().openapi({
description: "The fingerprint of this machine, deduced from the host machine's machine id - /etc/machine-id",
example: Examples.Machine.fingerprint
}),
location: z.object({ longitude: z.number(), latitude: z.number() }).openapi({
description: "This is the 2d location of this machine, they might not be accurate",
example: Examples.Machine.location
}),
countryCode: z.string().openapi({
description: "This is the 2 character country code of the country this machine [ISO 3166-1 alpha-2] ",
example: Examples.Machine.countryCode
}),
timezone: z.string().openapi({
description: "The IANA timezone formatted string of the timezone of the location where the machine is running",
example: Examples.Machine.timezone
})
})
.openapi({
ref: "Machine",
description: "Represents a hosted or BYOG machine connected to Nestri",
example: Examples.Machine,
});
// export const create = fn(Info.pick({ fingerprint: true, hostname: true }), async (input) => {
// const id = createID()
// const now = new Date().toISOString()
// const db = databaseClient()
// await db.transact(
// db.tx.machines[id]!.update({
// fingerprint: input.fingerprint,
// hostname: input.hostname,
// createdAt: now,
// //Just in case it had been previously deleted
// deletedAt: undefined
// })
// )
export type Info = z.infer<typeof Info>;
// return id
// })
export const create = fn(Info.partial({ id: true }), async (input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("machine");
await tx.insert(machineTable).values({
id,
country: input.country,
timezone: input.timezone,
fingerprint: input.fingerprint,
countryCode: input.countryCode,
// userID: input.userID,
location: { x: input.location.longitude, y: input.location.latitude },
})
// // export const fromID = fn(z.string(), async (id) => {
// const db = databaseClient()
// await afterTx(() =>
// bus.publish(Resource.Bus, Events.Created, {
// teamID: id,
// }),
// );
return id;
})
)
// const query = {
// machines: {
// $: {
// where: {
// id: id,
// deletedAt: { $isNull: true }
// }
// }
// }
// }
// export const fromUserID = fn(z.string(), async (userID) =>
// useTransaction(async (tx) =>
// tx
// .select()
// .from(machineTable)
// .where(and(eq(machineTable.userID, userID), isNull(machineTable.timeDeleted)))
// .then((rows) => rows.map(serialize))
// )
// )
// const res = await db.query(query)
// const machines = res.machines
// export const list = fn(z.void(), async () =>
// useTransaction(async (tx) =>
// tx
// .select()
// .from(machineTable)
// // Show only hosted machines, not BYOG machines
// .where(and(isNull(machineTable.userID), isNull(machineTable.timeDeleted)))
// .then((rows) => rows.map(serialize))
// )
// )
// if (machines && machines.length > 0) {
// const result = pipe(
// machines,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// fingerprint: group[0].fingerprint,
// hostname: group[0].hostname,
// createdAt: group[0].createdAt
// }))
// )
// return result
// }
export const fromID = fn(Info.shape.id, async (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(machineTable)
.where(and(eq(machineTable.id, id), isNull(machineTable.timeDeleted)))
.then((rows) => rows.map(serialize).at(0))
)
)
// return null
// })
export const fromFingerprint = fn(Info.shape.fingerprint, async (fingerprint) =>
useTransaction(async (tx) =>
tx
.select()
.from(machineTable)
.where(and(eq(machineTable.fingerprint, fingerprint), isNull(machineTable.timeDeleted)))
.execute()
.then((rows) => rows.map(serialize).at(0))
)
)
// export const installedGames = fn(z.string(), async (id) => {
// const db = databaseClient()
export const remove = fn(Info.shape.id, (id) =>
useTransaction(async (tx) => {
await tx
.update(machineTable)
.set({
timeDeleted: sql`now()`,
})
.where(and(eq(machineTable.id, id)))
.execute();
return id;
}),
);
// const query = {
// machines: {
// $: {
// where: {
// id: id,
// deletedAt: { $isNull: true }
// }
// },
// games: {}
// }
// }
export const fromLocation = fn(Info.shape.location, async (location) =>
useTransaction(async (tx) => {
const sqlDistance = sql`location <-> point(${location.longitude}, ${location.latitude})`;
return tx
.select({
...getTableColumns(machineTable),
distance: sql`round((${sqlDistance})::numeric, 2)`
})
.from(machineTable)
.where(isNull(machineTable.timeDeleted))
.orderBy(sqlDistance)
.limit(3)
.then((rows) => rows.map(serialize))
})
)
// const res = await db.query(query)
// const machines = res.machines
// if (machines && machines.length > 0) {
// const games = machines[0]?.games as any
// if (games.length > 0) {
// return games as Games.Info[]
// }
// return null
// }
// return null
// })
// export const fromFingerprint = fn(z.string(), async (input) => {
// const db = databaseClient()
// const query = {
// machines: {
// $: {
// where: {
// fingerprint: input,
// deletedAt: { $isNull: true }
// }
// }
// }
// }
// const res = await db.query(query)
// const machines = res.machines
// if (machines.length > 0) {
// const result = pipe(
// machines,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// fingerprint: group[0].fingerprint,
// hostname: group[0].hostname,
// createdAt: group[0].createdAt
// }))
// )
// return result[0]
// }
// return null
// })
// export const list = async () => {
// const user = useCurrentUser()
// const db = databaseClient()
// const query = {
// $users: {
// $: { where: { id: user.id } },
// machines: {
// $: {
// where: {
// deletedAt: { $isNull: true }
// }
// }
// }
// },
// }
// const res = await db.query(query)
// const machines = res.$users[0]?.machines
// if (machines && machines.length > 0) {
// const result = pipe(
// machines,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// fingerprint: group[0].fingerprint,
// hostname: group[0].hostname,
// createdAt: group[0].createdAt
// }))
// )
// return result
// }
// return null
// }
// export const linkToCurrentUser = fn(z.string(), async (id) => {
// const user = useCurrentUser()
// const db = databaseClient()
// await db.transact(db.tx.machines[id]!.link({ owner: user.id }))
// return "ok"
// })
// export const unLinkFromCurrentUser = fn(z.string(), async (id) => {
// const user = useCurrentUser()
// const db = databaseClient()
// const now = new Date().toISOString()
// const query = {
// $users: {
// $: { where: { id: user.id } },
// machines: {
// $: {
// where: {
// id,
// deletedAt: { $isNull: true }
// }
// }
// }
// },
// }
// const res = await db.query(query)
// const machines = res.$users[0]?.machines
// if (machines && machines.length > 0) {
// const machine = machines[0] as Info
// await db.transact(db.tx.machines[machine.id]!.update({ deletedAt: now }))
// return "ok"
// }
// return null
// })
// }
export function serialize(
input: typeof machineTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
// userID: input.userID,
country: input.country,
timezone: input.timezone,
fingerprint: input.fingerprint,
countryCode: input.countryCode,
location: { latitude: input.location.y, longitude: input.location.x },
};
}
}

View File

@@ -0,0 +1,40 @@
import { } from "drizzle-orm/postgres-js";
import { timestamps, id, ulid } from "../drizzle/types";
import {
text,
varchar,
pgTable,
uniqueIndex,
point,
primaryKey,
} from "drizzle-orm/pg-core";
export const machineTable = pgTable(
"machine",
{
...id,
...timestamps,
// userID: ulid("user_id"),
country: text('country').notNull(),
timezone: text('timezone').notNull(),
location: point('location', { mode: 'xy' }).notNull(),
fingerprint: varchar('fingerprint', { length: 32 }).notNull(),
countryCode: varchar('country_code', { length: 2 }).notNull(),
// provider: text("provider").notNull(),
// gpuType: text("gpu_type").notNull(),
// storage: numeric("storage").notNull(),
// ipaddress: text("ipaddress").notNull(),
// gpuNumber: integer("gpu_number").notNull(),
// computePrice: numeric("compute_price").notNull(),
// driverVersion: integer("driver_version").notNull(),
// operatingSystem: text("operating_system").notNull(),
// fingerprint: varchar("fingerprint", { length: 32 }).notNull(),
// externalID: varchar("external_id", { length: 255 }).notNull(),
// cudaVersion: numeric("cuda_version", { precision: 4, scale: 2 }).notNull(),
},
(table) => [
// uniqueIndex("external_id").on(table.externalID),
uniqueIndex("machine_fingerprint").on(table.fingerprint),
// primaryKey({ columns: [table.userID, table.id], }),
],
);

View File

@@ -0,0 +1,139 @@
import { z } from "zod";
import { Resource } from "sst";
import { bus } from "sst/aws/bus";
import { useTeam } from "../actor";
import { Common } from "../common";
import { createID, fn } from "../utils";
import { createEvent } from "../event";
import { Examples } from "../examples";
import { memberTable, role } from "./member.sql";
import { and, eq, sql, asc, isNull } from "../drizzle";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Member {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Member.id,
}),
timeSeen: z.date().nullable().or(z.undefined()).openapi({
description: "The last time this team member was active",
example: Examples.Member.timeSeen
}),
teamID: z.string().openapi({
description: "The unique id of the team this member is on",
example: Examples.Member.teamID
}),
role: z.enum(role).openapi({
description: "The role of this team member",
example: Examples.Member.role
}),
email: z.string().openapi({
description: "The email of this team member",
example: Examples.Member.email
})
})
.openapi({
ref: "Member",
description: "Represents a team member on Nestri",
example: Examples.Member,
});
export type Info = z.infer<typeof Info>;
export const Events = {
Created: createEvent(
"member.created",
z.object({
memberID: Info.shape.id,
}),
),
Updated: createEvent(
"member.updated",
z.object({
memberID: Info.shape.id,
}),
),
};
export const create = fn(
Info.pick({ email: true, id: true })
.partial({
id: true,
})
.extend({
first: z.boolean().optional(),
}),
(input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("member");
await tx.insert(memberTable).values({
id,
teamID: useTeam(),
email: input.email,
role: input.first ? "owner" : "member",
timeSeen: input.first ? sql`now()` : null,
})
await afterTx(() =>
async () => bus.publish(Resource.Bus, Events.Created, { memberID: id }),
);
return id;
}),
);
export const remove = fn(Info.shape.id, (id) =>
useTransaction(async (tx) => {
await tx
.update(memberTable)
.set({
timeDeleted: sql`now()`,
})
.where(and(eq(memberTable.id, id), eq(memberTable.teamID, useTeam())))
.execute();
return id;
}),
);
export const fromEmail = fn(z.string(), async (email) =>
useTransaction(async (tx) =>
tx
.select()
.from(memberTable)
.where(and(eq(memberTable.email, email), isNull(memberTable.timeDeleted)))
.orderBy(asc(memberTable.timeCreated))
.then((rows) => rows.map(serialize).at(0))
)
)
export const fromID = fn(z.string(), async (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(memberTable)
.where(and(eq(memberTable.id, id), isNull(memberTable.timeDeleted)))
.orderBy(asc(memberTable.timeCreated))
.then((rows) => rows.map(serialize).at(0))
),
)
/**
* Converts a raw member database row into a standardized {@link Member.Info} object.
*
* @param input - The database row representing a member.
* @returns The member information formatted as a {@link Member.Info} object.
*/
export function serialize(
input: typeof memberTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
role: input.role,
email: input.email,
teamID: input.teamID,
timeSeen: input.timeSeen
};
}
}

View File

@@ -0,0 +1,21 @@
import { teamIndexes } from "../team/team.sql";
import { timestamps, utc, teamID } from "../drizzle/types";
import { index, pgTable, text, uniqueIndex, varchar } from "drizzle-orm/pg-core";
export const role = ["admin", "member", "owner"] as const;
export const memberTable = pgTable(
"member",
{
...teamID,
...timestamps,
role: text("role", { enum: role }).notNull(),
timeSeen: utc("time_seen"),
email: varchar("email", { length: 255 }).notNull(),
},
(table) => [
...teamIndexes(table),
index("email_global").on(table.email),
uniqueIndex("member_email").on(table.teamID, table.email),
],
);

View File

@@ -0,0 +1,96 @@
import { z } from "zod";
import { fn } from "../utils";
import { Resource } from "sst";
import { useTeam, useUserID } from "../actor";
import { Polar as PolarSdk } from "@polar-sh/sdk";
import { validateEvent } from "@polar-sh/sdk/webhooks";
import { PlanType } from "../subscription/subscription.sql";
const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" });
const planType = z.enum(PlanType)
export namespace Polar {
export const client = polar;
export const fromUserEmail = fn(z.string().min(1), async (email) => {
try {
const customers = await client.customers.list({ email })
if (customers.result.items.length === 0) {
return await client.customers.create({ email })
} else {
return customers.result.items[0]
}
} catch (err) {
//FIXME: This is the issue [Polar.sh/#5147](https://github.com/polarsource/polar/issues/5147)
// console.log("error", err)
return undefined
}
})
const getProductIDs = (plan: z.infer<typeof planType>) => {
switch (plan) {
case "free":
return [Resource.NestriFreeMonthly.value]
case "pro":
return [Resource.NestriProYearly.value, Resource.NestriProMonthly.value]
case "family":
return [Resource.NestriFamilyYearly.value, Resource.NestriFamilyMonthly.value]
default:
return [Resource.NestriFreeMonthly.value]
}
}
export const createPortal = fn(
z.string(),
async (customerId) => {
const session = await client.customerSessions.create({
customerId
})
return session.customerPortalUrl
}
)
//TODO: Implement this
export const handleWebhook = async(payload: ReturnType<typeof validateEvent>) => {
switch (payload.type) {
case "subscription.created":
const teamID = payload.data.metadata.teamID
}
}
export const createCheckout = fn(
z
.object({
planType: z.enum(PlanType),
customerEmail: z.string(),
successUrl: z.string(),
customerID: z.string(),
allowDiscountCodes: z.boolean(),
teamID: z.string()
})
.partial({
customerEmail: true,
allowDiscountCodes: true,
customerID: true,
teamID: true
}),
async (input) => {
const productIDs = getProductIDs(input.planType)
const checkoutUrl =
await client.checkouts.create({
products: productIDs,
customerEmail: input.customerEmail ?? useUserID(),
successUrl: `${input.successUrl}?checkout={CHECKOUT_ID}`,
allowDiscountCodes: input.allowDiscountCodes ?? false,
customerId: input.customerID,
customerMetadata: {
teamID: input.teamID ?? useTeam()
}
})
return checkoutUrl.url
})
}

View File

@@ -1,412 +0,0 @@
import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database";
import { groupBy, map, pipe, values } from "remeda"
import { id as createID, } from "@instantdb/admin";
import { useCurrentUser } from "../actor";
export const userStatus = z.enum([
"active", //online and playing a game
"idle", //online and not playing
"offline",
]);
export module Profiles {
const MAX_ATTEMPTS = 50;
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Machine.id,
}),
username: z.string().openapi({
description: "The user's unique username",
example: Examples.Profile.username,
}),
avatarUrl: z.string().or(z.undefined()).openapi({
description: "The url to the profile picture.",
example: Examples.Profile.username,
}),
status: userStatus.openapi({
description: "Whether the user is active, idle or offline",
example: Examples.Profile.status
}),
discriminator: z.string().or(z.number()).openapi({
description: "The number discriminator for each username",
example: Examples.Profile.discriminator,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time when this profile was first created",
example: Examples.Profile.createdAt,
}),
updatedAt: z.string().or(z.number()).openapi({
description: "The time when this profile was last edited",
example: Examples.Profile.updatedAt,
})
})
.openapi({
ref: "Profile",
description: "Represents a profile of a user on Nestri",
example: Examples.Profile,
});
export type Info = z.infer<typeof Info>;
export type userStatus = z.infer<typeof userStatus>;
export const sanitizeUsername = (username: string): string => {
// Remove spaces and numbers
return username.replace(/[\s0-9]/g, '');
};
export const generateDiscriminator = (): string => {
return Math.floor(Math.random() * 100).toString().padStart(2, '0');
};
export const isValidDiscriminator = (discriminator: string): boolean => {
return /^\d{2}$/.test(discriminator);
};
export const fromUsername = fn(z.string(), async (input) => {
const sanitizedUsername = sanitizeUsername(input);
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
username: sanitizedUsername,
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length == 0) {
return null
}
return pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
discriminator: group[0].discriminator,
updatedAt: group[0].updatedAt,
status: group[0].status as userStatus
}))
)
})
export const findAvailableDiscriminator = fn(z.string(), async (input) => {
const db = databaseClient()
const username = sanitizeUsername(input);
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const discriminator = generateDiscriminator();
const query = {
profiles: {
$: {
where: {
username,
discriminator
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (profiles.length === 0) {
return discriminator;
}
}
return null; // No available discriminators
})
export const create = fn(z.object({ username: z.string(), customDiscriminator: z.string().optional(), avatarUrl: z.string().optional(), owner: z.string() }), async (input) => {
const username = sanitizeUsername(input.username);
const db = databaseClient()
const id = createID()
const now = new Date().toISOString()
let discriminator: string | null;
if (input.customDiscriminator) {
if (!isValidDiscriminator(input.customDiscriminator)) {
console.error('Invalid discriminator format')
return null
// throw new Error('Invalid discriminator format');
}
const query = {
profiles: {
$: {
where: {
username,
discriminator: input.customDiscriminator
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (profiles.length != 0) {
console.error("Username and discriminator combination already taken ")
return null
// throw new Error('Username and discriminator combination already taken');
}
discriminator = input.customDiscriminator
} else {
// Generate a random available discriminator
discriminator = await findAvailableDiscriminator(username);
if (!discriminator) {
console.error("No available discriminators for this username ")
return null
// throw new Error('No available discriminators for this username');
}
}
return await db.transact(
db.tx.profiles[id]!.update({
username,
avatarUrl: input.avatarUrl,
createdAt: now,
updatedAt: now,
discriminator,
status: "idle"
}).link({ owner: input.owner })
)
})
export const getFullUsername = async (username: string) => {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
username,
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
console.error('User not found')
return null
// throw new Error('User not found');
}
return `${profiles[0]?.username}#${profiles[0]?.discriminator}`;
}
export const fromOwnerID = async (ownerID: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
owner: ownerID
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const profile = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator,
status: group[0].status as userStatus
}))
)
return profile[0]
} catch (error) {
console.log("user fromOwnerID", error)
return null
}
}
export const fromID = async (id: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
id
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const profile = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator,
status: group[0].status as userStatus
}))
)
return profile[0]
} catch (error) {
console.log("user fromID", error)
return null
}
}
export const fromIDToOwner = async (id: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
id
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles as any
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
return profiles[0]!.owner as string
} catch (error) {
console.log("user fromID", error)
return null
}
}
export const getCurrentProfile = async () => {
const user = useCurrentUser()
const currentProfile = await fromOwnerID(user.id);
return currentProfile
}
export const setStatus = fn(userStatus, async (status) => {
try {
const user = useCurrentUser()
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(
db.tx.profiles[user.id]!.update({
status,
updatedAt: now
})
)
} catch (error) {
console.log("user setStatus error", error)
return null
}
})
export const list = async () => {
try {
const db = databaseClient()
// const ago = new Date(Date.now() - (60 * 1000 * 30)).toISOString()
const ago = new Date(Date.now() - (24 * 60 * 60 * 1000)).toISOString()
const query = {
profiles: {
$: {
limit: 10,
where: {
updatedAt: { $gt: ago },
},
order: {
updatedAt: "desc" as const,
},
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const result = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator,
status: group[0].status as userStatus
}))
)
return result
} catch (error) {
console.log("user list error", error)
return null
}
}
}

View File

@@ -0,0 +1,24 @@
import {
IoTDataPlaneClient,
PublishCommand,
} from "@aws-sdk/client-iot-data-plane";
import { useMachine } from "../actor";
import { Resource } from "sst";
export namespace Realtime {
const client = new IoTDataPlaneClient({});
export async function publish(message: any, subTopic?: string) {
const fingerprint = useMachine();
let topic = `${Resource.App.name}/${Resource.App.stage}/${fingerprint}/`;
if (subTopic)
topic = `${topic}${subTopic}`;
await client.send(
new PublishCommand({
payload: Buffer.from(JSON.stringify(message)),
topic: topic,
})
);
}
}

View File

@@ -1,251 +0,0 @@
import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { useCurrentUser } from "../actor";
import { groupBy, map, pipe, values } from "remeda"
import { id as createID } from "@instantdb/admin";
export module Sessions {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Session.id,
}),
public: z.boolean().openapi({
description: "If true, the session is publicly viewable by all users. If false, only authorized users can access it",
example: Examples.Session.public,
}),
endedAt: z.string().or(z.number()).or(z.undefined()).openapi({
description: "The timestamp indicating when this session was completed or terminated. Null if session is still active.",
example: Examples.Session.endedAt,
}),
startedAt: z.string().or(z.number()).openapi({
description: "The timestamp indicating when this session started.",
example: Examples.Session.startedAt,
})
})
.openapi({
ref: "Session",
description: "Represents a single game play session, tracking its lifetime and accessibility settings.",
example: Examples.Session,
});
export type Info = z.infer<typeof Info>;
export const create = fn(z.object({ public: z.boolean() }), async (input) => {
try {
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
const now = new Date().toISOString()
await db.transact(
db.tx.sessions[id]!.update({
public: input.public,
startedAt: now,
}).link({ owner: user.id })
)
return id
} catch (err) {
return null
}
})
export const getActive = async () => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
endedAt: { $isNull: true }
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No active sessions found")
}
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
}))
)
return result
} catch (error) {
return null
}
}
export const fromID = fn(z.string(), async (id) => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
id: id,
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
}))
)
return result
} catch (err) {
console.log("sessions error", err)
return null
}
})
export const fromTaskID = fn(z.string(), async (taskID) => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
task: taskID,
endedAt: { $isNull: true }
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
console.log("sessions", sessions)
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
}))
)
return result[0]
} catch (err) {
console.log("sessions error", err)
return null
}
})
export const end = fn(z.string(), async (id) => {
const user = useCurrentUser()
try {
const db = databaseClient()
const now = new Date().toISOString()
const query = {
sessions: {
$: {
where: {
owner: user.id,
id,
}
}
},
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
await db.transact(db.tx.sessions[sessions[0]!.id]!.update({ endedAt: now }))
return "ok"
} catch (error) {
return null
}
})
export const fromOwnerID = fn(z.string(), async (id) => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
owner: id,
endedAt: { $isNull: true }
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
}))
)
return result[0]
} catch (err) {
console.log("session owner error", err)
return null
}
})
}

View File

@@ -0,0 +1,137 @@
import { z } from "zod";
import { Common } from "../common";
import { Examples } from "../examples";
import { createID, fn } from "../utils";
import { useUser, useUserID } from "../actor";
import { eq, and, isNull, sql } from "../drizzle";
import { steamTable, AccountLimitation, LastGame } from "./steam.sql";
import { createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Steam {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Steam.id,
}),
avatarUrl: z.string().openapi({
description: "The avatar url of this Steam account",
example: Examples.Steam.avatarUrl
}),
steamEmail: z.string().openapi({
description: "The email regisered with this Steam account",
example: Examples.Steam.steamEmail
}),
steamID: z.number().openapi({
description: "The Steam ID this Steam account",
example: Examples.Steam.steamID
}),
limitation: AccountLimitation.openapi({
description: " The limitations of this Steam account",
example: Examples.Steam.limitation
}),
lastGame: LastGame.openapi({
description: "The last game played on this Steam account",
example: Examples.Steam.lastGame
}),
userID: z.string().openapi({
description: "The unique id of the user who owns this steam account",
example: Examples.Steam.userID
}),
username: z.string().openapi({
description: "The unique username of this steam user",
example: Examples.Steam.username
}),
personaName: z.string().openapi({
description: "The last recorded persona name used by this account",
example: Examples.Steam.personaName
}),
countryCode: z.string().openapi({
description: "The country this account is connected from",
example: Examples.Steam.countryCode
})
})
.openapi({
ref: "Steam",
description: "Represents a steam user's information stored on Nestri",
example: Examples.Steam,
});
export type Info = z.infer<typeof Info>;
export const create = fn(
Info.partial({
id: true,
userID: true,
}),
(input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("steam");
const user = useUser()
await tx.insert(steamTable).values({
id,
lastSeen: sql`now()`,
userID: input.userID ?? user.userID,
countryCode: input.countryCode,
username: input.username,
steamID: input.steamID,
lastGame: input.lastGame,
limitation: input.limitation,
steamEmail: input.steamEmail,
avatarUrl: input.avatarUrl,
personaName: input.personaName,
})
return id;
}),
);
export const fromUserID = fn(
z.string(),
(userID) =>
useTransaction((tx) =>
tx
.select()
.from(steamTable)
.where(and(eq(steamTable.userID, userID), isNull(steamTable.timeDeleted)))
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
)
export const list = () =>
useTransaction((tx) =>
tx
.select()
.from(steamTable)
.where(and(eq(steamTable.userID, useUserID()), isNull(steamTable.timeDeleted)))
.execute()
.then((rows) => rows.map(serialize)),
)
/**
* Serializes a raw Steam table record into a standardized Info object.
*
* This function maps the fields from a database record (retrieved from the Steam table) to the
* corresponding properties defined in the Info schema.
*
* @param input - A raw record from the Steam table containing user information.
* @returns An object conforming to the Info schema.
*/
export function serialize(
input: typeof steamTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
userID: input.userID,
countryCode: input.countryCode,
username: input.username,
avatarUrl: input.avatarUrl,
personaName: input.personaName,
steamEmail: input.steamEmail,
steamID: input.steamID,
limitation: input.limitation,
lastGame: input.lastGame,
};
}
}

View File

@@ -0,0 +1,45 @@
import { z } from "zod";
import { userTable } from "../user/user.sql";
import { id, timestamps, ulid, utc } from "../drizzle/types";
import { index, pgTable, integer, uniqueIndex, varchar, text, json } from "drizzle-orm/pg-core";
export const LastGame = z.object({
gameID: z.number(),
gameName: z.string()
});
export const AccountLimitation = z.object({
isLimited: z.boolean().nullable(),
isBanned: z.boolean().nullable(),
isLocked: z.boolean().nullable(),
isAllowedToInviteFriends: z.boolean().nullable(),
});
export type LastGame = z.infer<typeof LastGame>;
export type AccountLimitation = z.infer<typeof AccountLimitation>;
export const steamTable = pgTable(
"steam",
{
...id,
...timestamps,
userID: ulid("user_id")
.notNull()
.references(() => userTable.id, {
onDelete: "cascade",
}),
lastSeen: utc("last_seen").notNull(),
steamID: integer("steam_id").notNull(),
avatarUrl: text("avatar_url").notNull(),
lastGame: json("last_game").$type<LastGame>().notNull(),
username: varchar("username", { length: 255 }).notNull(),
countryCode: varchar('country_code', { length: 2 }).notNull(),
steamEmail: varchar("steam_email", { length: 255 }).notNull(),
personaName: varchar("persona_name", { length: 255 }).notNull(),
limitation: json("limitation").$type<AccountLimitation>().notNull(),
},
(table) => [
uniqueIndex("steam_id").on(table.steamID),
index("steam_user_id").on(table.userID),
],
);

View File

@@ -1,205 +1,192 @@
import { z } from "zod";
import databaseClient from "../database"
import { fn } from "../utils";
import { groupBy, map, pipe, values } from "remeda"
import { Common } from "../common";
import { Examples } from "../examples";
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
import { Email } from "../email";
import { Profiles } from "../profile";
import { createID, fn } from "../utils";
import { eq, and, isNull } from "../drizzle";
import { useTeam, useUserID } from "../actor";
import { createTransaction, useTransaction } from "../drizzle/transaction";
import { PlanType, Standing, subscriptionTable } from "./subscription.sql";
export const SubscriptionFrequency = z.enum([
"fixed",
"daily",
"weekly",
"monthly",
"yearly",
]);
export type SubscriptionFrequency = z.infer<typeof SubscriptionFrequency>;
export namespace Subscriptions {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Subscription.id,
}),
checkoutID: z.string().openapi({
description: "The polar.sh checkout id",
example: Examples.Subscription.checkoutID,
}),
// productID: z.string().openapi({
// description: "ID of the product being subscribed to.",
// example: Examples.Subscription.productID,
// }),
// quantity: z.number().int().openapi({
// description: "Quantity of the subscription.",
// example: Examples.Subscription.quantity,
// }),
// frequency: SubscriptionFrequency.openapi({
// description: "Frequency of the subscription.",
// example: Examples.Subscription.frequency,
// }),
// next: z.string().or(z.number()).openapi({
// description: "Next billing date for the subscription.",
// example: Examples.Subscription.next,
// }),
canceledAt: z.string().or(z.number()).optional().openapi({
description: "Cancelled date for the subscription.",
example: Examples.Subscription.canceledAt,
}),
})
.openapi({
ref: "Subscription",
description: "Subscription to a Nestri product.",
example: Examples.Subscription,
});
export namespace Subscription {
export const Info = z.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Subscription.id,
}),
polarSubscriptionID: z.string().nullable().or(z.undefined()).openapi({
description: "The unique id of the plan this subscription is on",
example: Examples.Subscription.polarSubscriptionID,
}),
teamID: z.string().openapi({
description: "The unique id of the team this subscription is for",
example: Examples.Subscription.teamID,
}),
userID: z.string().openapi({
description: "The unique id of the user who is paying this subscription",
example: Examples.Subscription.userID,
}),
polarProductID: z.string().nullable().or(z.undefined()).openapi({
description: "The unique id of the product this subscription is for",
example: Examples.Subscription.polarProductID,
}),
tokens: z.number().openapi({
description: "The number of tokens this subscription has left",
example: Examples.Subscription.tokens,
}),
planType: z.enum(PlanType).openapi({
description: "The type of plan this subscription is for",
example: Examples.Subscription.planType,
}),
standing: z.enum(Standing).openapi({
description: "The standing of this subscription",
example: Examples.Subscription.standing,
}),
}).openapi({
ref: "Subscription",
description: "Represents a subscription on Nestri",
example: Examples.Subscription
});
export type Info = z.infer<typeof Info>;
export const list = fn(z.string().optional(), async (userID) => {
const db = databaseClient()
const user = userID ? userID : useCurrentUser().id
export const create = fn(
Info
.partial({
teamID: true,
userID: true,
id: true,
standing: true,
planType: true,
polarProductID: true,
polarSubscriptionID: true,
}),
(input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("subscription");
const query = {
subscriptions: {
$: {
where: {
owner: user,
canceledAt: { $isNull: true }
}
},
}
}
await tx.insert(subscriptionTable).values({
id,
tokens: input.tokens,
polarProductID: input.polarProductID ?? null,
polarSubscriptionID: input.polarSubscriptionID ?? null,
standing: input.standing ?? "new",
planType: input.planType ?? "free",
userID: input.userID ?? useUserID(),
teamID: input.teamID ?? useTeam(),
});
const res = await db.query(query)
return id;
})
)
const response = res.subscriptions
if (!response || response.length === 0) {
return null
}
export const setPolarProductID = fn(
Info.pick({
id: true,
polarProductID: true,
}),
(input) =>
useTransaction(async (tx) =>
tx.update(subscriptionTable)
.set({
polarProductID: input.polarProductID,
})
.where(eq(subscriptionTable.id, input.id))
)
)
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
// next: group[0].next,
// frequency: group[0].frequency as any,
// quantity: group[0].quantity,
// productID: group[0].productID,
checkoutID: group[0].checkoutID,
}))
export const setPolarSubscriptionID = fn(
Info.pick({
id: true,
polarSubscriptionID: true,
}),
(input) =>
useTransaction(async (tx) =>
tx.update(subscriptionTable)
.set({
polarSubscriptionID: input.polarSubscriptionID,
})
.where(eq(subscriptionTable.id, input.id))
)
)
export const fromID = fn(z.string(), async (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.id, id),
isNull(subscriptionTable.timeDeleted)
)
)
.orderBy(subscriptionTable.timeCreated)
.then((rows) => rows.map(serialize))
)
return result
})
export const create = fn(Info.omit({ id: true, canceledAt: true }), async (input) => {
// const id = createID()
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
//Use the polar.sh ID
await db.transact(db.tx.subscriptions[id]!.update({
// next: input.next,
// frequency: input.frequency,
// quantity: input.quantity,
checkoutID: input.checkoutID,
}).link({ owner: user.id }))
const res = await db.auth.getUser({ id: user.id })
const profile = await Profiles.fromOwnerID(user.id)
if (profile) {
await Email.sendWelcome(res.email, profile.username)
}
})
export const remove = fn(z.string(), async (id) => {
const db = databaseClient()
await db.transact(db.tx.subscriptions[id]!.update({
canceledAt: new Date().toISOString()
}))
})
export const fromID = fn(z.string(), async (id) => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
subscriptions: {
$: {
where: {
id,
//Make sure they can only get subscriptions they own
owner: user.id,
canceledAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const response = res.subscriptions
if (!response || response.length === 0) {
return null
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
checkoutID: group[0].checkoutID,
// next: group[0].next,
// frequency: group[0].frequency as any,
// quantity: group[0].quantity,
// productID: group[0].productID,
}))
)
export const fromTeamID = fn(z.string(), async (teamID) =>
useTransaction(async (tx) =>
tx
.select()
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.teamID, teamID),
isNull(subscriptionTable.timeDeleted)
)
)
.orderBy(subscriptionTable.timeCreated)
.then((rows) => rows.map(serialize))
)
)
return result[0]
})
export const fromCheckoutID = fn(z.string(), async (id) => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
subscriptions: {
$: {
where: {
id,
//Make sure they can only get subscriptions they own
checkoutID: id,
canceledAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const response = res.subscriptions
if (!response || response.length === 0) {
return null
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
checkoutID: group[0].checkoutID,
}))
export const fromUserID = fn(z.string(), async (userID) =>
useTransaction(async (tx) =>
tx
.select()
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.userID, userID),
isNull(subscriptionTable.timeDeleted)
)
)
.orderBy(subscriptionTable.timeCreated)
.then((rows) => rows.map(serialize))
)
)
export const remove = fn(Info.shape.id, (id) =>
useTransaction(async (tx) =>
tx
.update(subscriptionTable)
.set({
timeDeleted: Common.now(),
})
.where(eq(subscriptionTable.id, id))
.execute()
)
)
/**
* Converts a raw subscription database record into a structured {@link Info} object.
*
* @param input - The subscription record retrieved from the database.
* @returns The subscription data formatted according to the {@link Info} schema.
*/
export function serialize(
input: typeof subscriptionTable.$inferSelect
): z.infer<typeof Info> {
return {
id: input.id,
userID: input.userID,
teamID: input.teamID,
standing: input.standing,
planType: input.planType,
tokens: input.tokens,
polarProductID: input.polarProductID,
polarSubscriptionID: input.polarSubscriptionID,
};
}
return result[0]
})
}

View File

@@ -0,0 +1,31 @@
import { teamTable } from "../team/team.sql";
import { ulid, userID, timestamps } from "../drizzle/types";
import { index, integer, pgTable, primaryKey, text, uniqueIndex, varchar } from "drizzle-orm/pg-core";
export const Standing = ["new", "good", "overdue", "cancelled"] as const;
export const PlanType = ["free", "pro", "family", "enterprise"] as const;
export const subscriptionTable = pgTable(
"subscription",
{
...userID,
...timestamps,
teamID: ulid("team_id")
.references(() => teamTable.id, { onDelete: "cascade" })
.notNull(),
standing: text("standing", { enum: Standing })
.notNull(),
planType: text("plan_type", { enum: PlanType })
.notNull(),
tokens: integer("tokens").notNull(),
polarProductID: varchar("product_id", { length: 255 }),
polarSubscriptionID: varchar("subscription_id", { length: 255 }),
},
(table) => [
uniqueIndex("subscription_id").on(table.id),
index("subscription_user_id").on(table.userID),
primaryKey({
columns: [table.id, table.teamID]
}),
]
)

View File

@@ -1,331 +0,0 @@
import { z } from "zod";
import { fn } from "../utils";
import { Resource } from "sst";
import { Aws } from "../aws/client";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
import { groupBy, map, pipe, values } from "remeda"
import { Sessions } from "../session";
export const lastStatus = z.enum([
"RUNNING",
"PENDING",
"UNKNOWN",
"STOPPED",
]);
export const taskType = z.enum([
"AWS",
"ON_PREMISES",
"UNKNOWN"
]);
export const healthStatus = z.enum([
"HEALTHY",
"UNHEALTHY",
"UNKNOWN",
]);
export type taskType = z.infer<typeof taskType>;
export type lastStatus = z.infer<typeof lastStatus>;
export type healthStatus = z.infer<typeof healthStatus>;
export module Tasks {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Task.id,
}),
type: taskType.openapi({
description: "Where this task is hosted on",
example: Examples.Task.type,
}),
taskID: z.string().openapi({
description: "The id of this task as seen on AWS",
example: Examples.Task.taskID,
}),
startedAt: z.string().or(z.number()).openapi({
description: "The time this task was started",
example: Examples.Task.startedAt,
}),
lastUpdated: z.string().or(z.number()).openapi({
description: "The time the information about this task was last updated",
example: Examples.Task.lastUpdated,
}),
stoppedAt: z.string().or(z.number()).optional().openapi({
description: "The time this task was stopped or quit",
example: Examples.Task.lastUpdated,
}),
lastStatus: lastStatus.openapi({
description: "The last registered status of this task",
example: Examples.Task.lastStatus,
}),
healthStatus: healthStatus.openapi({
description: "The health status of this task",
example: Examples.Task.healthStatus,
})
})
.openapi({
ref: "Subscription",
description: "Subscription to a Nestri product.",
example: Examples.Task,
});
export type Info = z.infer<typeof Info>;
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
try {
const query = {
tasks: {
$: {
where: {
stoppedAt: { $isNull: true },
owner: user.id
}
},
}
}
const data = await db.query(query)
const response = data.tasks
if (!response || response.length === 0) {
throw new Error("No task for this user were found");
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
taskID: group[0].taskID,
type: group[0].type as taskType,
lastStatus: group[0].lastStatus as lastStatus,
healthStatus: group[0].healthStatus as healthStatus,
startedAt: group[0].startedAt,
stoppedAt: group[0].stoppedAt,
lastUpdated: group[0].lastUpdated,
}))
)
return result
} catch (e) {
return null
}
}
export const create = async () => {
const user = useCurrentUser()
try {
//TODO: Use a simpler way to set the session ID
// const sessionID = createID()
const sessionID = await Sessions.create({ public: true })
if (!sessionID) throw new Error("No session id was given");
const run = await Aws.EcsRunTask({
count: 1,
cluster: Resource.NestriGPUCluster.value,
taskDefinition: Resource.NestriGPUTask.value,
launchType: "EC2",
overrides: {
containerOverrides: [
{
name: "nestri",
environment: [
{
name: "NESTRI_ROOM",
value: sessionID
}
]
}
]
}
})
if (!run.tasks || run.tasks.length === 0) {
throw new Error(`No tasks were started`);
}
// Extract task details
const task = run.tasks[0];
const taskArn = task?.taskArn!;
const taskId = taskArn.split('/').pop()!; // Extract task ID from ARN
const taskStatus = task?.lastStatus;
const taskHealthStatus = task?.healthStatus;
const startedAt = task?.startedAt!;
const id = createID()
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(db.tx.tasks[id]!.update({
taskID: taskId,
type: "AWS",
healthStatus: taskHealthStatus ? taskHealthStatus.toString() : "UNKNOWN",
startedAt: startedAt ? startedAt.toISOString() : now,
lastStatus: taskStatus,
lastUpdated: now,
}).link({ owner: user.id, sessions: sessionID }))
return id
} catch (e) {
console.error("error", e)
return null
}
}
export const fromID = fn(z.string(), async (taskID) => {
const db = databaseClient()
try {
const query = {
tasks: {
$: {
where: {
id: taskID,
stoppedAt: { $isNull: true }
}
},
}
}
const data = await db.query(query)
const response = data.tasks
if (!response || response.length === 0) {
throw new Error("No task with the given id was found");
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
taskID: group[0].taskID,
type: group[0].type as taskType,
lastStatus: group[0].lastStatus as lastStatus,
healthStatus: group[0].healthStatus as healthStatus,
startedAt: group[0].startedAt,
stoppedAt: group[0].stoppedAt,
lastUpdated: group[0].lastUpdated,
}))
)
return result[0]
} catch (error) {
return null
}
})
export const update = fn(z.string(), async (taskID) => {
try {
const db = databaseClient()
const query = {
tasks: {
$: {
where: {
id: taskID,
stoppedAt: { $isNull: true }
}
},
}
}
const data = await db.query(query)
const response = data.tasks
if (!response || response.length === 0) {
throw new Error("No task with the given taskID was found");
}
const now = new Date().toISOString()
const describeResponse = await Aws.EcsDescribeTasks({
tasks: [response[0]!.taskID],
cluster: Resource.NestriGPUCluster.value
})
if (!describeResponse.tasks || describeResponse.tasks.length === 0) {
throw new Error("No tasks were found");
}
const task = describeResponse.tasks[0]!
const updatedDb = {
healthStatus: task.healthStatus ? task.healthStatus : "UNKNOWN",
lastStatus: task.lastStatus ? task.lastStatus : "UNKNOWN",
lastUpdated: now,
}
await db.transact(db.tx.tasks[response[0]!.id]!.update({
...updatedDb
}))
const updatedRes = [{ ...response[0]!, ...updatedDb }]
const result = pipe(
updatedRes,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
taskID: group[0].taskID,
type: group[0].type as taskType,
lastStatus: group[0].lastStatus as lastStatus,
healthStatus: group[0].healthStatus as healthStatus,
startedAt: group[0].startedAt,
stoppedAt: group[0].stoppedAt,
lastUpdated: group[0].lastUpdated,
}))
)
return result
} catch (error) {
console.error("update error", error)
return null
}
})
export const stop = fn(z.object({ taskID: z.string(), id: z.string() }), async (input) => {
const db = databaseClient()
const now = new Date().toISOString()
try {
//TODO:Check whether they own this task first
const stopResponse = await Aws.EcsStopTask({
task: input.taskID,
cluster: Resource.NestriGPUCluster.value,
reason: "Client requested a shutdown"
})
if (!stopResponse.task) {
throw new Error(`No task was stopped`);
}
await db.transact(db.tx.tasks[input.id]!.update({
stoppedAt: now,
lastUpdated: now,
lastStatus: "STOPPED",
healthStatus: "UNKNOWN"
}))
return "ok"
} catch (error) {
console.error("stop error", error)
return null
}
})
}

View File

@@ -0,0 +1,20 @@
import { id, timestamps } from "../drizzle/types";
import { pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core";
//This represents a task created on a machine for running a game
//Add billing info here?
//Add who owns the task here
// Add the session ID here
//Add which machine owns this task
export const taskTable = pgTable(
"task",
{
...id,
...timestamps,
fingerprint: varchar('fingerprint', { length: 32 }).notNull(),
},
(table) => [
uniqueIndex("task_fingerprint").on(table.fingerprint),
],
);

View File

@@ -1,164 +1,218 @@
import { z } from "zod";
import databaseClient from "../database"
import { fn } from "../utils";
import { groupBy, map, pipe, values } from "remeda"
import { Common } from "../common";
import { Member } from "../member";
import { teamTable } from "./team.sql";
import { Examples } from "../examples";
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
import { assertActor } from "../actor";
import { createEvent } from "../event";
import { createID, fn } from "../utils";
import { Subscription } from "../subscription";
import { and, eq, sql, isNull } from "../drizzle";
import { memberTable } from "../member/member.sql";
import { ErrorCodes, VisibleError } from "../error";
import { groupBy, map, pipe, values } from "remeda";
import { subscriptionTable } from "../subscription/subscription.sql";
import { createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Teams {
export namespace Team {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Team.id,
}),
name: z.string().openapi({
description: "Name of the team",
example: Examples.Team.name,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time when this team was first created",
example: Examples.Team.createdAt,
}),
updatedAt: z.string().or(z.number()).openapi({
description: "The time when this team was last edited",
example: Examples.Team.updatedAt,
}),
// owner: z.boolean().openapi({
// description: "Whether this team is owned by this user",
// example: Examples.Team.owner,
// }),
slug: z.string().openapi({
description: "This is the unique name identifier for the team",
// Remove spaces and make sure it is lowercase (this is just to make sure the frontend did this)
slug: z.string().regex(/^[a-z0-9\-]+$/, "Use a URL friendly name.").openapi({
description: "The unique and url-friendly slug of this team",
example: Examples.Team.slug
})
}),
name: z.string().openapi({
description: "The name of this team",
example: Examples.Team.name
}),
members: Member.Info.array().openapi({
description: "The members of this team",
example: Examples.Team.members
}),
subscriptions: Subscription.Info.array().openapi({
description: "The subscriptions of this team",
example: Examples.Team.subscriptions
}),
})
.openapi({
ref: "Team",
description: "A group of users sharing the same machines for gaming.",
description: "Represents a team on Nestri",
example: Examples.Team,
});
export type Info = z.infer<typeof Info>;
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
export const Events = {
Created: createEvent(
"team.created",
z.object({
teamID: z.string().nonempty(),
}),
),
};
const query = {
teams: {
$: {
where: {
members: user.id,
deletedAt: { $isNull: true }
}
},
}
export class TeamExistsError extends VisibleError {
constructor(slug: string) {
super(
"already_exists",
ErrorCodes.Validation.TEAM_ALREADY_EXISTS,
`There is already a team named "${slug}"`
);
}
const res = await db.query(query)
const teams = res.teams
if (!teams || teams.length === 0) {
return null
}
const result = pipe(
teams,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
slug: group[0].slug,
//@ts-expect-error
owner: group[0].owner === user.id
}))
)
return result
}
export const create = fn(
Info.pick({ slug: true, id: true, name: true, }).partial({
id: true,
}), (input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("team");
const result = await tx.insert(teamTable).values({
id,
slug: input.slug,
name: input.name
})
.onConflictDoNothing({ target: teamTable.slug })
export const fromSlug = fn(z.string(), async (slug) => {
const db = databaseClient()
if (result.count === 0) throw new TeamExistsError(input.slug);
const query = {
teams: {
$: {
where: {
slug,
deletedAt: { $isNull: true }
}
},
}
}
return id;
})
)
const res = await db.query(query)
//TODO: "Delete" subscription and member(s) as well
export const remove = fn(Info.shape.id, (input) =>
useTransaction(async (tx) => {
const account = assertActor("user");
const row = await tx
.select({
teamID: memberTable.teamID,
})
.from(memberTable)
.where(
and(
eq(memberTable.teamID, input),
eq(memberTable.email, account.properties.email),
),
)
.execute()
.then((rows) => rows.at(0));
if (!row) return;
await tx
.update(teamTable)
.set({
timeDeleted: sql`now()`,
})
.where(eq(teamTable.id, row.teamID));
}),
);
const teams = res.teams
if (!teams || teams.length === 0) {
return null
}
const result = pipe(
teams,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
slug: group[0].slug,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
// owner: group[0].owner === user.id
}))
export const list = fn(z.void(), () => {
const actor = assertActor("user");
return useTransaction(async (tx) =>
tx
.select()
.from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(memberTable.email, actor.properties.email),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute()
.then((rows) => serialize(rows))
)
});
return result[0]
})
export const fromID = fn(z.string().min(1), async (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(teamTable.id, id),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute()
.then((rows) => serialize(rows).at(0))
),
);
export const create = fn(Info.pick({ name: true, slug: true }), async (input) => {
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
const now = new Date().toISOString()
await db.transact(db.tx.teams[id]!.update({
name: input.name,
slug: input.slug,
createdAt: now,
updatedAt: now,
}).link({ owner: user.id, members: user.id }))
return id
})
export const remove = fn(z.string(), async (id) => {
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(db.tx.teams[id]!.update({
deletedAt: now
}))
return "ok"
})
export const invite = fn(z.object({email:z.string(), id: z.string()}), async (input) => {
//TODO:
// const db = databaseClient()
// const now = new Date().toISOString()
// await db.transact(db.tx.teams[id]!.update({
// deletedAt: now
// }))
return "ok"
})
export const fromSlug = fn(z.string().min(1), async (slug) =>
useTransaction(async (tx) =>
tx
.select()
.from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(teamTable.slug, slug),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute()
.then((rows) => serialize(rows).at(0))
),
);
/**
* Transforms an array of team, subscription, and member records into structured team objects.
*
* Groups input rows by team ID and constructs an array of team objects, each including its associated members and subscriptions.
*
* @param input - Array of objects containing team, subscription, and member data.
* @returns An array of team objects with their members and subscriptions.
*/
export function serialize(
input: { team: typeof teamTable.$inferSelect, subscription: typeof subscriptionTable.$inferInsert | null, member: typeof memberTable.$inferInsert | null }[],
): z.infer<typeof Info>[] {
console.log("serialize", input)
return pipe(
input,
groupBy((row) => row.team.id),
values(),
map((group) => ({
name: group[0].team.name,
id: group[0].team.id,
slug: group[0].team.slug,
subscriptions: !group[0].subscription ?
[] :
group.map((row) => ({
planType: row.subscription!.planType,
polarProductID: row.subscription!.polarProductID,
polarSubscriptionID: row.subscription!.polarSubscriptionID,
standing: row.subscription!.standing,
tokens: row.subscription!.tokens,
teamID: row.subscription!.teamID,
userID: row.subscription!.userID,
id: row.subscription!.id,
})),
members:
!group[0].member ?
[] :
group.map((row) => ({
id: row.member!.id,
email: row.member!.email,
role: row.member!.role,
teamID: row.member!.teamID,
timeSeen: row.member!.timeSeen,
}))
})),
);
}
}

View File

@@ -0,0 +1,28 @@
import { timestamps, id } from "../drizzle/types";
import {
varchar,
pgTable,
primaryKey,
uniqueIndex,
} from "drizzle-orm/pg-core";
export const teamTable = pgTable(
"team",
{
...id,
...timestamps,
name: varchar("name", { length: 255 }).notNull(),
slug: varchar("slug", { length: 255 }).notNull(),
},
(table) => [
uniqueIndex("slug").on(table.slug)
],
);
export function teamIndexes(table: any) {
return [
primaryKey({
columns: [table.teamID, table.id],
}),
];
}

View File

@@ -1,17 +0,0 @@
export interface CloudflareCF {
colo: string;
continent: string;
country: string,
city: string;
region: string;
longitude: number;
latitude: number;
metroCode: string;
postalCode: string;
timezone: string;
regionCode: number;
}
export interface CFRequest extends Request {
cf: CloudflareCF
}

View File

@@ -1,37 +1,254 @@
import { z } from "zod";
import databaseClient from "../database"
import { fn } from "../utils";
import { Team } from "../team";
import { bus } from "sst/aws/bus";
import { Steam } from "../steam";
import { Common } from "../common";
import { Polar } from "../polar/index";
import { createID, fn } from "../utils";
import { userTable } from "./user.sql";
import { createEvent } from "../event";
import { Examples } from "../examples";
import { Resource } from "sst/resource";
import { teamTable } from "../team/team.sql";
import { steamTable } from "../steam/steam.sql";
import { assertActor, withActor } from "../actor";
import { memberTable } from "../member/member.sql";
import { pipe, groupBy, values, map } from "remeda";
import { and, eq, isNull, asc, sql } from "../drizzle";
import { subscriptionTable } from "../subscription/subscription.sql";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
export namespace User {
const MAX_ATTEMPTS = 50;
export module Users {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.User.id,
}),
email: z.string().nullable().openapi({
description: "Email address of the user.",
name: z.string().openapi({
description: "The user's unique username",
example: Examples.User.name,
}),
polarCustomerID: z.string().or(z.null()).openapi({
description: "The polar customer id for this user",
example: Examples.User.polarCustomerID,
}),
email: z.string().openapi({
description: "The email address of this user",
example: Examples.User.email,
}),
avatarUrl: z.string().or(z.null()).openapi({
description: "The url to the profile picture.",
example: Examples.User.name,
}),
discriminator: z.string().or(z.number()).openapi({
description: "The (number) discriminator for this user",
example: Examples.User.discriminator,
}),
steamAccounts: Steam.Info.array().openapi({
description: "The steam accounts for this user",
example: Examples.User.steamAccounts,
}),
})
.openapi({
ref: "User",
description: "A Nestri console user.",
description: "Represents a user on Nestri",
example: Examples.User,
});
export const fromEmail = fn(z.string(), async (email) => {
const db = databaseClient()
const res = await db.auth.getUser({ email })
return res
export type Info = z.infer<typeof Info>;
export const Events = {
Created: createEvent(
"user.created",
z.object({
userID: Info.shape.id,
}),
),
Updated: createEvent(
"user.updated",
z.object({
userID: Info.shape.id,
}),
),
};
export const sanitizeUsername = (username: string): string => {
// Remove spaces and numbers
return username.replace(/[\s0-9]/g, '');
};
export const generateDiscriminator = (): string => {
return Math.floor(Math.random() * 100).toString().padStart(2, '0');
};
export const isValidDiscriminator = (discriminator: string): boolean => {
return /^\d{2}$/.test(discriminator);
};
export const findAvailableDiscriminator = fn(z.string(), async (input) => {
const username = sanitizeUsername(input);
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const discriminator = generateDiscriminator();
const users = await useTransaction(async (tx) =>
tx
.select()
.from(userTable)
.where(and(eq(userTable.name, username), eq(userTable.discriminator, Number(discriminator))))
)
if (users.length === 0) {
return discriminator;
}
}
return null;
})
export const create = fn(z.string(), async (email) => {
const db = databaseClient()
const token = await db.auth.createToken(email)
export const create = fn(Info.omit({ polarCustomerID: true, discriminator: true, steamAccounts: true }).partial({ avatarUrl: true, id: true }), async (input) => {
const userID = createID("user")
return token
//FIXME: Do this much later, as Polar.sh has so many inconsistencies for fuck's sake
const customer = await Polar.fromUserEmail(input.email)
console.log("customer", customer)
const name = sanitizeUsername(input.name);
// Generate a random available discriminator
const discriminator = await findAvailableDiscriminator(name);
if (!discriminator) {
console.error("No available discriminators for this username ")
return null
}
createTransaction(async (tx) => {
const id = input.id ?? userID;
await tx.insert(userTable).values({
id,
name: input.name,
avatarUrl: input.avatarUrl,
email: input.email,
discriminator: Number(discriminator),
polarCustomerID: customer?.id
})
await afterTx(() =>
withActor({
type: "user",
properties: {
userID: id,
email: input.email
},
},
async () => bus.publish(Resource.Bus, Events.Created, { userID: id }),
)
);
})
return userID;
})
export const fromEmail = fn(z.string(), async (email) =>
useTransaction(async (tx) =>
tx
.select()
.from(userTable)
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
.where(and(eq(userTable.email, email), isNull(userTable.timeDeleted)))
.orderBy(asc(userTable.timeCreated))
.then((rows => serialize(rows).at(0)))
)
)
export const fromID = fn(z.string(), (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(userTable)
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
.where(and(eq(userTable.id, id), isNull(userTable.timeDeleted), isNull(steamTable.timeDeleted)))
.orderBy(asc(userTable.timeCreated))
.then((rows) => serialize(rows).at(0))
),
)
export const remove = fn(Info.shape.id, (id) =>
useTransaction(async (tx) => {
await tx
.update(userTable)
.set({
timeDeleted: sql`now()`,
})
.where(and(eq(userTable.id, id)))
.execute();
return id;
}),
);
/**
* Converts an array of user and Steam account records into structured user objects with associated Steam accounts.
*
* @param input - An array of objects containing user data and optional Steam account data.
* @returns An array of user objects, each including a list of their associated Steam accounts.
*/
export function serialize(
input: { user: typeof userTable.$inferSelect; steam: typeof steamTable.$inferSelect | null }[],
): z.infer<typeof Info>[] {
return pipe(
input,
groupBy((row) => row.user.id),
values(),
map((group) => ({
...group[0].user,
steamAccounts: !group[0].steam ?
[] :
group.map((row) => ({
id: row.steam!.id,
lastSeen: row.steam!.lastSeen,
countryCode: row.steam!.countryCode,
username: row.steam!.username,
steamID: row.steam!.steamID,
lastGame: row.steam!.lastGame,
limitation: row.steam!.limitation,
steamEmail: row.steam!.steamEmail,
userID: row.steam!.userID,
personaName: row.steam!.personaName,
avatarUrl: row.steam!.avatarUrl,
})),
})),
)
}
/**
* Retrieves the list of teams that the current user belongs to.
*
* @returns An array of team information objects representing the user's active team memberships.
*
* @remark Only teams and memberships that have not been deleted are included in the result.
*/
export function teams() {
const actor = assertActor("user");
return useTransaction(async (tx) =>
tx
.select()
.from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(memberTable.email, actor.properties.email),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute()
.then((rows) => Team.serialize(rows))
)
}
}

Some files were not shown because too many files have changed in this diff Show More