29 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
255 changed files with 32736 additions and 5069 deletions

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

@@ -27,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:
@@ -55,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: ./
@@ -107,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: ./
@@ -116,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 }}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

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

@@ -44,7 +44,7 @@
"@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",
@@ -67,7 +67,7 @@
"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>
)
})
*/

2883
bun.lock

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

@@ -13,6 +13,7 @@ WORKDIR /relay
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"
@@ -23,6 +24,7 @@ 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

View File

@@ -85,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
@@ -133,8 +133,8 @@ RUN sed -i \
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 \
sudo xorg-xwayland seatd libinput labwc wlr-randr mangohud \
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 && \
@@ -144,6 +144,9 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
gst-plugins-bad gst-plugin-pipewire \
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}/*
@@ -185,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,82 +1,63 @@
import { bus } from "./bus";
import { auth } from "./auth";
import { domain } from "./dns";
import { email } from "./email";
import { secret } from "./secret";
import { database } from "./database";
import { cluster } from "./cluster";
import { postgres } from "./postgres";
sst.Linkable.wrap(random.RandomString, (resource) => ({
properties: {
value: resource.result,
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",
},
}));
export const urls = new sst.Linkable("Urls", {
properties: {
api: "https://api." + domain,
auth: "https://auth." + domain,
site: $dev ? "http://localhost:4321" : "https://" + domain,
environment: {
NO_COLOR: "1",
},
});
export const authFingerprintKey = new random.RandomString(
"AuthFingerprintKey",
{
length: 32,
},
);
export const auth = new sst.aws.Auth("Auth", {
issuer: {
timeout: "3 minutes",
handler: "./packages/functions/src/auth.handler",
link: [
bus,
email,
database,
authFingerprintKey,
secret.PolarSecret,
secret.GithubClientID,
secret.DiscordClientID,
secret.GithubClientSecret,
secret.DiscordClientSecret,
],
permissions: [
loadBalancer: {
rules: [
{
actions: ["ses:SendEmail"],
resources: ["*"],
listen: "80/http",
forward: "3001/http",
},
],
},
domain: {
name: "auth." + domain,
dns: sst.cloudflare.dns(),
dev: {
command: "bun dev:api",
directory: "packages/functions",
url: "http://localhost:3001",
},
})
scaling:
$app.stage === "production"
? {
min: 2,
max: 10,
}
: undefined,
});
export const apiFunction = new sst.aws.Function("ApiFn", {
handler: "packages/functions/src/api/index.handler",
link: [
bus,
urls,
database,
secret.PolarSecret,
],
timeout: "3 minutes",
streaming: !$dev,
url: true
})
export const api = new sst.aws.Router("Api", {
export const apiRoute = new sst.aws.Router("ApiRoute", {
routes: {
"/*": apiFunction.url
// I think api.url should work all the same
"/*": api.nodes.loadBalancer.dnsName,
},
domain: {
name: "api." + domain,
dns: sst.cloudflare.dns(),
},
})
export const outputs = {
auth: auth.url,
api: api.url,
};
})

66
infra/auth.ts Normal file
View File

@@ -0,0 +1,66 @@
import { bus } from "./bus";
import { domain } from "./dns";
import { secret } from "./secret";
import { cluster } from "./cluster";
import { postgres } from "./postgres";
//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(),
},
})

View File

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

6
infra/cluster.ts Normal file
View File

@@ -0,0 +1,6 @@
import { vpc } from "./vpc";
export const cluster = new sst.aws.Cluster("Cluster", {
vpc,
forceUpgrade: "v2"
});

View File

@@ -1,40 +0,0 @@
//Created manually from the dashboard and shared with the whole team/org
const dbProject = neon.getProjectOutput({
id: "black-sky-26872933"
})
const dbBranchId = $app.stage !== "production" ?
new neon.Branch("NeonBranch", {
parentId: dbProject.defaultBranchId,
projectId: dbProject.id,
name: $app.stage,
}).id : dbProject.defaultBranchId
const dbEndpoint = new neon.Endpoint("NeonEndpoint", {
projectId: dbProject.id,
branchId: dbBranchId,
poolerEnabled: true,
type: "read_write",
})
const dbRole = new neon.Role("NeonRole", {
name: "admin",
branchId: dbBranchId,
projectId: dbProject.id,
})
const db = new neon.Database("NeonDatabase", {
branchId: dbBranchId,
projectId: dbProject.id,
ownerName: dbRole.name,
name: `nestri-${$app.stage}`,
})
export const database = new sst.Linkable("Database", {
properties: {
name: db.name,
user: dbRole.name,
host: dbEndpoint.host,
password: dbRole.password,
},
});

View File

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

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,11 +1,17 @@
export const secret = {
// InstantAppId: new sst.Secret("InstantAppId"),
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"),
// InstantAdminToken: new sst.Secret("InstantAdminToken"),
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);

2
infra/stage.ts Normal file
View File

@@ -0,0 +1,2 @@
export const isPermanentStage =
$app.stage === "production" || $app.stage === "dev";

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,
},
});

1
infra/storage.ts Normal file
View File

@@ -0,0 +1 @@
export const storage = new sst.aws.Bucket("Storage");

11
infra/vpc.ts Normal file
View File

@@ -0,0 +1,11 @@
import { isPermanentStage } from "./stage";
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");

View File

@@ -1,9 +1,11 @@
// 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";
import { auth, api } from "./api";
new sst.aws.StaticSite("Web", {
path: "./packages/www",
path: "packages/www",
build: {
output: "./dist",
command: "bun run build",
@@ -14,7 +16,8 @@ new sst.aws.StaticSite("Web", {
},
environment: {
VITE_API_URL: api.url,
VITE_AUTH_URL: auth.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

@@ -16,9 +16,19 @@
"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": [
@@ -26,6 +36,6 @@
"packages/*"
],
"dependencies": {
"sst": "3.9.1"
"sst": "^3.11.21"
}
}

View File

@@ -1,20 +1,19 @@
import { Resource } from "sst";
import { defineConfig } from "drizzle-kit";
function addPoolerSuffix(original: string): string {
const firstDotIndex = original.indexOf('.');
if (firstDotIndex === -1) return original + '-pooler';
return original.slice(0, firstDotIndex) + '-pooler' + original.slice(firstDotIndex);
}
const dbHost = addPoolerSuffix(Resource.Database.host)
const connection = {
user: Resource.Database.username,
password: Resource.Database.password,
host: Resource.Database.host,
};
export default defineConfig({
schema: "./src/**/*.sql.ts",
verbose: true,
strict: true,
out: "./migrations",
dialect: "postgresql",
verbose: true,
dbCredentials: {
url: `postgresql://${Resource.Database.user}:${Resource.Database.password}@${dbHost}/${Resource.Database.name}?sslmode=require`,
url: `postgres://${connection.user}:${connection.password}@${connection.host}/nestri`,
},
schema: "./src/**/*.sql.ts",
});

View File

@@ -14,8 +14,9 @@ CREATE TABLE "team" (
"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,
"name" varchar(255) NOT NULL
"plan_type" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user" (
@@ -24,14 +25,15 @@ CREATE TABLE "user" (
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"avatar_url" text,
"email" varchar(255) NOT NULL,
"name" varchar(255) NOT NULL,
"discriminator" integer NOT NULL,
"polar_customer_id" varchar(255) 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 UNIQUE INDEX "member_email" ON "member" USING btree ("team_id","email");--> statement-breakpoint
CREATE INDEX "email_global" ON "member" USING btree ("email");--> statement-breakpoint
CREATE UNIQUE INDEX "slug" ON "team" USING btree ("slug");--> 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

@@ -1 +0,0 @@
ALTER TABLE "user" ALTER COLUMN "polar_customer_id" DROP NOT NULL;

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

@@ -1,5 +1,5 @@
{
"id": "08ba0262-ce0a-4d87-b4e2-0d17dc0ee28c",
"id": "f09034df-208a-42b3-b61f-f842921c6e24",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
@@ -54,6 +54,21 @@
}
},
"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": [
@@ -74,21 +89,6 @@
"concurrently": false,
"method": "btree",
"with": {}
},
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
@@ -136,22 +136,28 @@
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"team_slug": {
"name": "team_slug",
"columns": [
{
"expression": "slug",
@@ -209,12 +215,6 @@
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
@@ -227,11 +227,24 @@
"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": true
"notNull": false
},
"flags": {
"name": "flags",
"type": "json",
"primaryKey": false,
"notNull": false,
"default": "'{}'::json"
}
},
"indexes": {

View File

@@ -1,6 +1,6 @@
{
"id": "c09359df-19fe-4246-9a41-43b3a429c12f",
"prevId": "08ba0262-ce0a-4d87-b4e2-0d17dc0ee28c",
"id": "6f428226-b5d8-4182-a676-d04f842f9ded",
"prevId": "f09034df-208a-42b3-b61f-f842921c6e24",
"version": "7",
"dialect": "postgresql",
"tables": {
@@ -54,6 +54,21 @@
}
},
"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": [
@@ -74,21 +89,6 @@
"concurrently": false,
"method": "btree",
"with": {}
},
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
@@ -136,15 +136,21 @@
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
@@ -209,12 +215,6 @@
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
@@ -227,11 +227,24 @@
"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": {

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

@@ -5,15 +5,71 @@
{
"idx": 0,
"version": "7",
"when": 1740345380808,
"tag": "0000_wise_black_widow",
"when": 1741759978256,
"tag": "0000_flaky_matthew_murdock",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1740487217291,
"tag": "0001_flaky_tomorrow_man",
"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

@@ -4,12 +4,11 @@
"sideEffects": false,
"type": "module",
"scripts": {
"db:dev": "drizzle-kit",
"typecheck": "tsc --noEmit",
"db": "sst shell drizzle-kit",
"db:push": "sst shell drizzle-kit push",
"db:migrate": "sst shell drizzle-kit migrate",
"db:generate": "sst shell drizzle-kit generate",
"db:connect": "sst shell ../scripts/src/psql.ts",
"db:move": "sst shell drizzle-kit generate && sst shell drizzle-kit migrate && sst shell drizzle-kit push"
"db:exec": "sst shell ../scripts/src/psql.sh",
"db:reset": "sst shell ../scripts/src/db-reset.sh"
},
"exports": {
"./*": "./src/*.ts"
@@ -18,23 +17,24 @@
"@tsconfig/node20": "^20.1.4",
"aws-iot-device-sdk-v2": "^1.21.1",
"aws4fetch": "^1.0.20",
"drizzle-kit": "^0.30.4",
"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": {
"@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",
"@neondatabase/serverless": "^0.10.4",
"@openauthjs/openauth": "0.4.3",
"@openauthjs/openauth": "*",
"@openauthjs/openevent": "^0.0.27",
"@polar-sh/sdk": "^0.26.1",
"drizzle-orm": "^0.39.3",
"ws": "^8.18.1"
"drizzle-kit": "^0.30.5",
"drizzle-orm": "^0.40.0",
"postgres": "^3.4.5"
}
}

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { eq } from "./drizzle";
import { VisibleError } from "./error";
import { ErrorCodes, VisibleError } from "./error";
import { createContext } from "./context";
import { UserFlags, userTable } from "./user/user.sql";
import { useTransaction } from "./drizzle/transaction";
@@ -37,24 +37,65 @@ export const SystemActor = z.object({
});
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>;
const ActorContext = createContext<Actor>("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(
"unauthorized",
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource`,
);
}
/**
* 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`,
);
}
@@ -68,25 +109,34 @@ export function assertActor<T extends Actor["type"]>(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 Error(`Expected actor to have teamID`);
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Expected actor to have teamID`
);
}
export async function assertUserFlag(flag: keyof UserFlags) {
return useTransaction((tx) =>
tx
.select({ flags: userTable.flags })
.from(userTable)
.where(eq(userTable.id, useUserID()))
.then((rows) => {
const flags = rows[0]?.flags;
if (!flags)
throw new VisibleError(
"user.flags",
"Actor does not have " + flag + " flag",
);
}),
/**
* 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,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,30 +1,17 @@
export * from "drizzle-orm";
import ws from 'ws';
import { Resource } from "sst";
import { drizzle as neonDrizzle, NeonDatabase } from "drizzle-orm/neon-serverless";
// import { drizzle } from 'drizzle-orm/postgres-js';
import { Pool, neonConfig } from "@neondatabase/serverless";
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
neonConfig.webSocketConstructor = ws;
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"),
});
function addPoolerSuffix(original: string): string {
const firstDotIndex = original.indexOf('.');
if (firstDotIndex === -1) return original + '-pooler';
return original.slice(0, firstDotIndex) + '-pooler' + original.slice(firstDotIndex);
}
const dbHost = addPoolerSuffix(Resource.Database.host)
const client = new Pool({ connectionString: `postgres://${Resource.Database.user}:${Resource.Database.password}@${dbHost}/${Resource.Database.name}?sslmode=require` })
export const db = neonDrizzle(client, {
logger:
process.env.DRIZZLE_LOG === "true"
? {
logQuery(query, params) {
console.log("query", query);
console.log("params", params);
},
}
: undefined,
});
export const db = drizzle(client, {});

View File

@@ -4,14 +4,13 @@ import {
PgTransactionConfig
} from "drizzle-orm/pg-core";
import {
NeonQueryResultHKT
// NeonHttpQueryResultHKT
} from "drizzle-orm/neon-serverless";
PostgresJsQueryResultHKT
} from "drizzle-orm/postgres-js";
import { ExtractTablesWithRelations } from "drizzle-orm";
import { createContext } from "../context";
export type Transaction = PgTransaction<
NeonQueryResultHKT,
PostgresJsQueryResultHKT,
Record<string, never>,
ExtractTablesWithRelations<Record<string, never>>
>;
@@ -59,7 +58,6 @@ export async function createTransaction<T>(
},
);
await Promise.all(effects.map((x) => x()));
// await db.$client.end()
return result as T;
}
}

View File

@@ -1,4 +1,5 @@
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 });
@@ -17,6 +18,15 @@ export const teamID = {
},
};
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,

View File

@@ -10,7 +10,7 @@ export namespace Email {
subject: string,
body: string,
) {
from = from + "@" + Resource.Mail.sender;
from = from + "@" + Resource.Email.sender;
console.log("sending email", subject, from, to);
await Client.send(
new SendEmailCommand({

View File

@@ -1,8 +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 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

@@ -1,8 +1,29 @@
import { prefixes } from "./utils";
export module Examples {
export namespace Examples {
export const Id = (prefix: keyof typeof prefixes) =>
`${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
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 User = {
id: Id("user"),
name: "John Doe",
@@ -10,24 +31,51 @@ export module Examples {
discriminator: 47,
avatarUrl: "https://cdn.discordapp.com/avatars/xxxxxxx/xxxxxxx.png",
polarCustomerID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
steamAccounts: [Steam]
};
export const Team = {
id: Id("team"),
name: "John Does' Team",
slug: "john_doe",
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 = {
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 Polar = {
teamID: Id("team"),
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
export const Team = {
id: Id("team"),
name: "John Does' Team",
slug: "john_doe",
subscriptions: [Subscription],
members: [Member]
}
export const Machine = {
id: Id("machine"),
userID: Id("user"),
country: "Kenya",
countryCode: "KE",
timezone: "Africa/Nairobi",
location: { latitude: 36.81550, longitude: -1.28410 },
fingerprint: "fc27f428f9ca47d4b41b707ae0c62090",
}
}

View File

@@ -0,0 +1,155 @@
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 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 type Info = z.infer<typeof Info>;
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 },
})
// await afterTx(() =>
// bus.publish(Resource.Bus, Events.Created, {
// teamID: id,
// }),
// );
return id;
})
)
// 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))
// )
// )
// 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))
// )
// )
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))
)
)
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 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;
}),
);
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))
})
)
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

@@ -6,18 +6,18 @@ import { Common } from "../common";
import { createID, fn } from "../utils";
import { createEvent } from "../event";
import { Examples } from "../examples";
import { memberTable } from "./member.sql";
import { memberTable, role } from "./member.sql";
import { and, eq, sql, asc, isNull } from "../drizzle";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
export module Member {
export namespace Member {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Member.id,
}),
timeSeen: z.date().or(z.null()).openapi({
timeSeen: z.date().nullable().or(z.undefined()).openapi({
description: "The last time this team member was active",
example: Examples.Member.timeSeen
}),
@@ -25,6 +25,10 @@ export module Member {
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
@@ -66,15 +70,12 @@ export module Member {
const id = input.id ?? createID("member");
await tx.insert(memberTable).values({
id,
email: input.email,
teamID: useTeam(),
timeSeen: input.first ? sql`CURRENT_TIMESTAMP()` : null,
}).onConflictDoUpdate({
target: memberTable.id,
set: {
timeDeleted: null,
}
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 }),
);
@@ -82,16 +83,16 @@ export module Member {
}),
);
export const remove = fn(Info.shape.id, (input) =>
export const remove = fn(Info.shape.id, (id) =>
useTransaction(async (tx) => {
await tx
.update(memberTable)
.set({
timeDeleted: sql`CURRENT_TIMESTAMP()`,
timeDeleted: sql`now()`,
})
.where(and(eq(memberTable.id, input), eq(memberTable.teamID, useTeam())))
.where(and(eq(memberTable.id, id), eq(memberTable.teamID, useTeam())))
.execute();
return input;
return id;
}),
);
@@ -102,11 +103,10 @@ export module Member {
.from(memberTable)
.where(and(eq(memberTable.email, email), isNull(memberTable.timeDeleted)))
.orderBy(asc(memberTable.timeCreated))
.then((rows) => rows.map(serialize))
.then((rows) => rows.at(0))
),
.then((rows) => rows.map(serialize).at(0))
)
)
export const fromID = fn(z.string(), async (id) =>
useTransaction(async (tx) =>
tx
@@ -114,16 +114,22 @@ export module Member {
.from(memberTable)
.where(and(eq(memberTable.id, id), isNull(memberTable.timeDeleted)))
.orderBy(asc(memberTable.timeCreated))
.then((rows) => rows.map(serialize))
.then((rows) => rows.at(0))
.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

@@ -1,18 +1,21 @@
import { teamIndexes } from "../team/team.sql";
import { timestamps, utc, teamID } from "../drizzle/types";
import { index, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core";
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),
uniqueIndex("member_email").on(table.teamID, table.email),
index("email_global").on(table.email),
uniqueIndex("member_email").on(table.teamID, table.email),
],
);

View File

@@ -1,69 +1,16 @@
import { z } from "zod";
import { fn } from "../utils";
import { Resource } from "sst";
import { eq, and } from "../drizzle";
import { useTeam } from "../actor";
import { createEvent } from "../event";
import { polarTable, Standing } from "./polar.sql";
import { useTeam, useUserID } from "../actor";
import { Polar as PolarSdk } from "@polar-sh/sdk";
import { useTransaction } from "../drizzle/transaction";
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" });
export module Polar {
const planType = z.enum(PlanType)
export namespace Polar {
export const client = polar;
export const Info = z.object({
teamID: z.string(),
customerID: z.string(),
subscriptionID: z.string().nullable(),
subscriptionItemID: z.string().nullable(),
standing: z.enum(Standing),
});
export type Info = z.infer<typeof Info>;
export const Checkout = z.object({
annual: z.boolean().optional(),
successUrl: z.string(),
cancelUrl: z.string(),
});
export const CheckoutSession = z.object({
url: z.string().nullable(),
});
export const CustomerSubscriptionEventType = [
"created",
"updated",
"deleted",
] as const;
export const Events = {
CustomerSubscriptionEvent: createEvent(
"polar.customer-subscription-event",
z.object({
type: z.enum(CustomerSubscriptionEventType),
status: z.string(),
teamID: z.string().min(1),
customerID: z.string().min(1),
subscriptionID: z.string().min(1),
subscriptionItemID: z.string().min(1),
}),
),
};
export function get() {
return useTransaction(async (tx) =>
tx
.select()
.from(polarTable)
.where(eq(polarTable.teamID, useTeam()))
.execute()
.then((rows) => rows.map(serialize).at(0)),
);
}
export const fromUserEmail = fn(z.string().min(1), async (email) => {
try {
const customers = await client.customers.list({ email })
@@ -81,89 +28,69 @@ export module Polar {
}
})
export const setCustomerID = fn(Info.shape.customerID, async (customerID) =>
useTransaction(async (tx) =>
tx
.insert(polarTable)
.values({
teamID: useTeam(),
customerID,
standing: "new",
})
.execute(),
),
);
export const setSubscription = fn(
Info.pick({
subscriptionID: true,
subscriptionItemID: true,
}),
(input) =>
useTransaction(async (tx) =>
tx
.update(polarTable)
.set({
subscriptionID: input.subscriptionID,
subscriptionItemID: input.subscriptionItemID,
})
.where(eq(polarTable.teamID, useTeam()))
.returning()
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
);
export const removeSubscription = fn(
z.string().min(1),
(stripeSubscriptionID) =>
useTransaction((tx) =>
tx
.update(polarTable)
.set({
subscriptionItemID: null,
subscriptionID: null,
})
.where(and(eq(polarTable.subscriptionID, stripeSubscriptionID)))
.execute(),
),
);
export const setStanding = fn(
Info.pick({
subscriptionID: true,
standing: true,
}),
(input) =>
useTransaction((tx) =>
tx
.update(polarTable)
.set({ standing: input.standing })
.where(and(eq(polarTable.subscriptionID, input.subscriptionID!)))
.execute(),
),
);
export const fromCustomerID = fn(Info.shape.customerID, (customerID) =>
useTransaction((tx) =>
tx
.select()
.from(polarTable)
.where(and(eq(polarTable.customerID, customerID)))
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
);
function serialize(
input: typeof polarTable.$inferSelect,
): z.infer<typeof Info> {
return {
teamID: input.teamID,
customerID: input.customerID,
subscriptionID: input.subscriptionID,
subscriptionItemID: input.subscriptionItemID,
standing: input.standing,
};
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,22 +0,0 @@
import { timestamps, teamID } from "../drizzle/types";
import { teamIndexes, teamTable } from "../team/team.sql";
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
export const Standing = ["new", "good", "overdue"] as const;
export const polarTable = pgTable(
"polar",
{
teamID: teamID.teamID.primaryKey().references(() => teamTable.id),
...timestamps,
customerID: varchar("customer_id", { length: 255 }).notNull(),
subscriptionID: varchar("subscription_id", { length: 255 }),
subscriptionItemID: varchar("subscription_item_id", {
length: 255,
}),
standing: text("standing", { enum: Standing }).notNull(),
},
(table) => ({
...teamIndexes(table),
})
)

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

@@ -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

@@ -0,0 +1,192 @@
import { z } from "zod";
import { Common } from "../common";
import { Examples } from "../examples";
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 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 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");
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(),
});
return id;
})
)
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))
)
)
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))
)
)
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))
)
)
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,
};
}
}

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

@@ -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,32 +1,43 @@
import { z } from "zod";
import { Resource } from "sst";
import { bus } from "sst/aws/bus";
import { Common } from "../common";
import { createID, fn } from "../utils";
import { Examples } from "../examples";
import { Member } from "../member";
import { teamTable } from "./team.sql";
import { Examples } from "../examples";
import { assertActor } from "../actor";
import { createEvent } from "../event";
import { assertActor, withActor } from "../actor";
import { and, eq, sql } from "../drizzle";
import { createID, fn } from "../utils";
import { Subscription } from "../subscription";
import { and, eq, sql, isNull } from "../drizzle";
import { memberTable } from "../member/member.sql";
import { HTTPException } from 'hono/http-exception';
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
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 module Team {
export namespace Team {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Team.id,
}),
slug: z.string().openapi({
// 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",
@@ -45,41 +56,36 @@ export module Team {
),
};
export class TeamExistsError extends HTTPException {
export class TeamExistsError extends VisibleError {
constructor(slug: string) {
super(
400,
{ message: `There is already a team named "${slug}"`, }
"already_exists",
ErrorCodes.Validation.TEAM_ALREADY_EXISTS,
`There is already a team named "${slug}"`
);
}
}
export const create = fn(
Info.pick({ slug: true, id: true, name: true }).partial({
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 })
if (!result.rowCount) throw new TeamExistsError(input.slug);
await afterTx(() =>
withActor({ type: "system", properties: { teamID: id } }, () =>
bus.publish(Resource.Bus, Events.Created, {
teamID: id,
})
),
);
return id;
}), (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 })
if (result.count === 0) throw new TeamExistsError(input.slug);
return id;
})
)
//TODO: "Delete" subscription and member(s) as well
export const remove = fn(Info.shape.id, (input) =>
useTransaction(async (tx) => {
const account = assertActor("user");
@@ -106,48 +112,107 @@ export module Team {
}),
);
export const list = fn(z.void(), () =>
useTransaction((tx) =>
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) => rows.map(serialize)),
.then((rows) => serialize(rows))
)
});
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 fromID = fn(z.string().min(1), async (id) =>
useTransaction(async (tx) => {
return tx
export const fromSlug = fn(z.string().min(1), async (slug) =>
useTransaction(async (tx) =>
tx
.select()
.from(teamTable)
.where(eq(teamTable.id, id))
.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) => rows.map(serialize))
.then((rows) => rows.at(0));
}),
);
export const fromSlug = fn(z.string().min(1), async (input) =>
useTransaction(async (tx) => {
return tx
.select()
.from(teamTable)
.where(eq(teamTable.slug, input))
.execute()
.then((rows) => rows.map(serialize))
.then((rows) => rows.at(0));
}),
.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: typeof teamTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
name: input.name,
slug: input.slug,
};
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

@@ -1,10 +1,9 @@
import {} from "drizzle-orm/postgres-js";
import { timestamps, id } from "../drizzle/types";
import {
varchar,
pgTable,
primaryKey,
uniqueIndex,
varchar,
} from "drizzle-orm/pg-core";
export const teamTable = pgTable(
@@ -12,10 +11,12 @@ export const teamTable = pgTable(
{
...id,
...timestamps,
slug: varchar("slug", { length: 255 }).notNull(),
name: varchar("name", { length: 255 }).notNull(),
slug: varchar("slug", { length: 255 }).notNull(),
},
(table) => [uniqueIndex("slug").on(table.slug)],
(table) => [
uniqueIndex("slug").on(table.slug)
],
);
export function teamIndexes(table: any) {

View File

@@ -1,21 +1,25 @@
import { z } from "zod";
import { Polar } from "../polar";
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 { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle";
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 module User {
export namespace User {
const MAX_ATTEMPTS = 50;
export const Info = z
@@ -44,6 +48,10 @@ export module User {
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",
@@ -102,7 +110,7 @@ export module User {
return null;
})
export const create = fn(Info.omit({ polarCustomerID: true, discriminator: true }).partial({ avatarUrl: true, id: true }), async (input) => {
export const create = fn(Info.omit({ polarCustomerID: true, discriminator: true, steamAccounts: true }).partial({ avatarUrl: true, id: true }), async (input) => {
const userID = createID("user")
//FIXME: Do this much later, as Polar.sh has so many inconsistencies for fuck's sake
@@ -151,57 +159,86 @@ export module User {
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) => rows.map(serialize))
.then((rows) => rows.at(0))
),
.then((rows => serialize(rows).at(0)))
)
)
export const fromID = fn(z.string(), async (id) =>
export const fromID = fn(z.string(), (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(userTable)
.where(and(eq(userTable.id, id), isNull(userTable.timeDeleted)))
.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) => rows.map(serialize))
.then((rows) => rows.at(0))
.then((rows) => serialize(rows).at(0))
),
)
export function serialize(
input: typeof userTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
name: input.name,
email: input.email,
avatarUrl: input.avatarUrl,
discriminator: input.discriminator,
polarCustomerID: input.polarCustomerID,
};
}
export const remove = fn(Info.shape.id, (input) =>
export const remove = fn(Info.shape.id, (id) =>
useTransaction(async (tx) => {
await tx
.update(userTable)
.set({
timeDeleted: sql`CURRENT_TIMESTAMP()`,
timeDeleted: sql`now()`,
})
.where(and(eq(userTable.id, input)))
.where(and(eq(userTable.id, id)))
.execute();
return input;
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((tx) =>
return useTransaction(async (tx) =>
tx
.select(getTableColumns(teamTable))
.select()
.from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
@@ -211,7 +248,7 @@ export module User {
),
)
.execute()
.then((rows) => rows.map(Team.serialize))
);
.then((rows) => Team.serialize(rows))
)
}
}

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { id, timestamps } from "../drizzle/types";
import { integer, pgTable, text, uniqueIndex, varchar,json } from "drizzle-orm/pg-core";
import { integer, pgTable, text, uniqueIndex, varchar, json } from "drizzle-orm/pg-core";
// Whether this user is part of the Nestri Team, comes with privileges
export const UserFlags = z.object({
@@ -15,13 +15,13 @@ export const userTable = pgTable(
...id,
...timestamps,
avatarUrl: text("avatar_url"),
email: varchar("email", { length: 255 }).notNull(),
name: varchar("name", { length: 255 }).notNull(),
discriminator: integer("discriminator").notNull(),
email: varchar("email", { length: 255 }).notNull(),
polarCustomerID: varchar("polar_customer_id", { length: 255 }).unique(),
flags: json("flags").$type<UserFlags>().default({}),
// flags: json("flags").$type<UserFlags>().default({}),
},
(user) => [
uniqueIndex("user_email").on(user.email),
],
]
);

View File

@@ -2,10 +2,26 @@ import { ulid } from "ulid";
export const prefixes = {
user: "usr",
team: "tea",
member: "mbr"
team: "tem",
task: "tsk",
machine: "mch",
member: "mbr",
steam: "stm",
subscription: "sub",
invite: "inv",
product: "prd",
} as const;
/**
* Generates a unique identifier by concatenating a predefined prefix with a ULID.
*
* Given a key from the predefined prefixes mapping (e.g., "user", "team", "member", "steam"),
* this function retrieves the corresponding prefix and combines it with a ULID using an underscore
* as a separator. The resulting identifier is formatted as "prefix_ulid".
*
* @param prefix - A key from the prefixes mapping.
* @returns A unique identifier string.
*/
export function createID(prefix: keyof typeof prefixes): string {
return [prefixes[prefix], ulid()].join("_");
}

View File

@@ -1,9 +1,8 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"strict": true,
"module": "esnext",
"jsx": "react-jsx",
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true,
}
}

View File

@@ -0,0 +1,17 @@
FROM mirror.gcr.io/oven/bun:1.2
# TODO: Add a way to build C# Steam.exe and start it to run in the container before the API
ADD ./package.json .
ADD ./bun.lock .
ADD ./packages/core/package.json ./packages/core/package.json
ADD ./packages/functions/package.json ./packages/functions/package.json
ADD ./patches ./patches
RUN bun install --ignore-scripts
ADD ./packages/functions ./packages/functions
ADD ./packages/core ./packages/core
WORKDIR ./packages/functions
CMD ["bun", "run", "./src/api/index.ts"]

View File

@@ -1,7 +1,13 @@
{
"name": "@nestri/functions",
"module": "index.ts",
"type": "module",
"exports": {
"./*": "./src/*.ts"
},
"scripts": {
"dev:auth": "bun run --watch ./src/auth.ts",
"dev:api": "bun run --watch ./src/api/index.ts"
},
"devDependencies": {
"@aws-sdk/client-ecs": "^3.738.0",
"@aws-sdk/client-sqs": "^3.734.0",
@@ -14,9 +20,12 @@
"typescript": "^5.0.0"
},
"dependencies": {
"@openauthjs/openauth": "0.4.3",
"@actor-core/bun": "^0.7.9",
"@openauthjs/openauth": "*",
"actor-core": "^0.7.9",
"hono": "^4.6.15",
"hono-openapi": "^0.3.1",
"partysocket": "1.0.3"
"partysocket": "1.0.3",
"postgres": "^3.4.5"
}
}
}

View File

@@ -1,21 +1,22 @@
import { z } from "zod";
import { Hono } from "hono";
import { notPublic } from "./auth";
import { Result } from "../common";
import { resolver } from "hono-openapi/zod";
import { describeRoute } from "hono-openapi";
import { User } from "@nestri/core/user/index";
import { Team } from "@nestri/core/team/index";
import { assertActor } from "@nestri/core/actor";
import { Examples } from "@nestri/core/examples";
import { ErrorResponses, Result } from "./common";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
export module AccountApi {
export namespace AccountApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Account"],
summary: "Retrieve the current user's details",
description: "Returns the user's account details, plus the teams they have joined",
summary: "Get user account",
description: "Get the current user's account details",
responses: {
200: {
content: {
@@ -24,39 +25,34 @@ export module AccountApi {
z.object({
...User.Info.shape,
teams: Team.Info.array(),
}).openapi({
description: "User account information",
example: { ...Examples.User, teams: [Examples.Team] }
})
),
},
},
description: "Successfully retrieved account details"
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "This account does not exist",
description: "User account details"
},
404: ErrorResponses[404],
429: ErrorResponses[429]
}
}),
async (c) => {
const actor = assertActor("user");
const [currentUser, teams] = await Promise.all([User.fromID(actor.properties.userID), User.teams()])
if (!currentUser) return c.json({ error: "This account does not exist; it may have been deleted" }, 404)
const { id, email, name, polarCustomerID, avatarUrl, discriminator } = currentUser
if (!currentUser)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User not found",
);
return c.json({
data: {
id,
email,
name,
...currentUser,
teams,
avatarUrl,
discriminator,
polarCustomerID,
}
}, 200);
},

View File

@@ -1,47 +1,51 @@
import { Resource } from "sst";
import { subjects } from "../subjects";
import { type MiddlewareHandler } from "hono";
// import { User } from "@nestri/core/user/index";
import { VisibleError } from "@nestri/core/error";
import { HTTPException } from "hono/http-exception";
import { useActor, withActor } from "@nestri/core/actor";
import { createClient } from "@openauthjs/openauth/client";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
const client = createClient({
issuer: Resource.Urls.auth,
issuer: Resource.Auth.url,
clientID: "api",
});
export const notPublic: MiddlewareHandler = async (c, next) => {
const actor = useActor();
if (actor.type === "public")
throw new HTTPException(401, { message: "Unauthorized" });
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
"Missing authorization header",
);
return next();
};
export const auth: MiddlewareHandler = async (c, next) => {
const authHeader =
c.req.query("authorization") ?? c.req.header("authorization");
if (!authHeader) return next();
if (!authHeader) return withActor({ type: "public", properties: {} }, next);
const match = authHeader.match(/^Bearer (.+)$/);
if (!match) {
throw new VisibleError(
"auth.token",
"Bearer token not found or improperly formatted",
"authentication",
ErrorCodes.Authentication.INVALID_TOKEN,
"Invalid personal access token",
);
}
const bearerToken = match[1];
let result = await client.verify(subjects, bearerToken!);
if (result.err) {
throw new HTTPException(401, {
message: "Unauthorized",
});
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.INVALID_TOKEN,
"Invalid bearer token",
);
}
if (result.subject.type === "user") {
const teamID = c.req.header("x-nestri-team") //|| c.req.query("teamID");
const teamID = c.req.header("x-nestri-team");
if (!teamID) return withActor(result.subject, next);
// const email = result.subject.properties.email;
return withActor(
{
type: "system",
@@ -49,21 +53,11 @@ export const auth: MiddlewareHandler = async (c, next) => {
teamID,
},
},
next
// async () => {
// const user = await User.fromEmail(email);
// if (!user || user.length === 0) {
// c.status(401);
// return c.text("Unauthorized");
// }
// return withActor(
// {
// type: "member",
// properties: { userID: user[0].id, workspaceID: user.workspaceID },
// },
// next,
// );
// },
async () =>
withActor(
result.subject,
next,
)
);
}
};

View File

@@ -0,0 +1,246 @@
import { z, ZodSchema } from "zod";
import {type Hook } from "./types/hook";
import { ErrorCodes, ErrorResponse } from "@nestri/core/error";
import type { MiddlewareHandler, ValidationTargets } from "hono";
import { resolver, validator as zodValidator } from "hono-openapi/zod";
export function Result<T extends z.ZodTypeAny>(schema: T) {
return resolver(
z.object({
data: schema,
}),
);
}
/**
* Custom validator wrapper around hono-openapi/zod validator that formats errors
* according to our standard API error format
*/
export const validator = <
T extends ZodSchema,
Target extends keyof ValidationTargets
>(
target: Target,
schema: T
): MiddlewareHandler<
any,
string,
{
in: {
[K in Target]: z.input<T>;
};
out: {
[K in Target]: z.output<T>;
};
}
> => {
// Create a custom error handler that formats errors according to our standards
// const standardErrorHandler: Parameters<typeof zodValidator>[2] = (
const standardErrorHandler: Hook<z.infer<T>, any, any, Target> = (
result,
c,
) => {
if (!result.success) {
// Get the validation issues
const issues = result.error.issues || result.error.errors || [];
if (issues.length === 0) {
// If there are no issues, return a generic error
return c.json(
{
type: "validation",
code: ErrorCodes.Validation.INVALID_PARAMETER,
message: "Invalid request data",
},
400,
);
}
// Get the first error for the main response
const firstIssue = issues[0]!;
const fieldPath = firstIssue.path
? Array.isArray(firstIssue.path)
? firstIssue.path.join(".")
: firstIssue.path
: undefined;
// Map Zod error codes to our standard error codes
let errorCode = ErrorCodes.Validation.INVALID_PARAMETER;
if (
firstIssue.code === "invalid_type" &&
firstIssue.received === "undefined"
) {
errorCode = ErrorCodes.Validation.MISSING_REQUIRED_FIELD;
} else if (
["invalid_string", "invalid_date", "invalid_regex"].includes(
firstIssue.code,
)
) {
errorCode = ErrorCodes.Validation.INVALID_FORMAT;
}
// Create our standardized error response
const response = {
type: "validation",
code: errorCode,
message: firstIssue.message,
param: fieldPath,
details: undefined as any,
};
// Add details if we have multiple issues
if (issues.length > 0) {
response.details = {
issues: issues.map((issue) => ({
path: issue.path
? Array.isArray(issue.path)
? issue.path.join(".")
: issue.path
: undefined,
code: issue.code,
message: issue.message,
// @ts-expect-error
expected: issue.expected,
// @ts-expect-error
received: issue.received,
})),
};
}
console.log("Validation error in validator:", response);
return c.json(response, 400);
}
};
// Use the original validator with our custom error handler
return zodValidator(target, schema, standardErrorHandler);
};
/**
* Standard error responses for OpenAPI documentation
*/
export const ErrorResponses = {
400: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Validation error",
example: {
type: "validation",
code: "invalid_parameter",
message: "The request was invalid",
param: "email",
},
}),
),
},
},
description:
"Bad Request - The request could not be understood or was missing required parameters.",
},
401: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Authentication error",
example: {
type: "authentication",
code: "unauthorized",
message: "Authentication required",
},
}),
),
},
},
description:
"Unauthorized - Authentication is required and has failed or has not been provided.",
},
403: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Permission error",
example: {
type: "forbidden",
code: "permission_denied",
message: "You do not have permission to access this resource",
},
}),
),
},
},
description:
"Forbidden - You do not have permission to access this resource.",
},
404: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Not found error",
example: {
type: "not_found",
code: "resource_not_found",
message: "The requested resource could not be found",
},
}),
),
},
},
description: "Not Found - The requested resource does not exist.",
},
409: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Conflict Error",
example: {
type: "already_exists",
code: "resource_already_exists",
message: "The resource could not be created because it already exists",
},
}),
),
},
},
description: "Conflict - The resource could not be created because it already exists.",
},
429: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Rate limit error",
example: {
type: "rate_limit",
code: "too_many_requests",
message: "Rate limit exceeded",
},
}),
),
},
},
description:
"Too Many Requests - You have made too many requests in a short period of time.",
},
500: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Server error",
example: {
type: "internal",
code: "internal_error",
message: "Internal server error",
},
}),
),
},
},
description: "Internal Server Error - Something went wrong on our end.",
},
};

View File

@@ -1,18 +1,23 @@
import "zod-openapi/extend";
import { Hono } from "hono";
import { auth } from "./auth";
import { ZodError } from "zod";
import { cors } from "hono/cors";
import { TeamApi } from "./team";
import { PolarApi } from "./polar";
import { logger } from "hono/logger";
import { Realtime } from "./realtime";
import { AccountApi } from "./account";
import { MachineApi } from "./machine";
import { openAPISpecs } from "hono-openapi";
import { VisibleError } from "@nestri/core/error";
import { patchLogger } from "../log-polyfill";
import { HTTPException } from "hono/http-exception";
import { handle, streamHandle } from "hono/aws-lambda";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
const app = new Hono();
export const app = new Hono();
app
.use(logger(), async (c, next) => {
.use(logger())
.use(cors())
.use(async (c, next) => {
c.header("Cache-Control", "no-store");
return next();
})
@@ -20,42 +25,34 @@ app
const routes = app
.get("/", (c) => c.text("Hello World!"))
.route("/realtime", Realtime.route)
.route("/team", TeamApi.route)
.route("/polar", PolarApi.route)
.route("/account", AccountApi.route)
.route("/machine", MachineApi.route)
.onError((error, c) => {
console.warn(error);
if (error instanceof VisibleError) {
return c.json(
{
code: error.code,
message: error.message,
},
400
);
}
if (error instanceof ZodError) {
const e = error.errors[0];
if (e) {
return c.json(
{
code: e?.code,
message: e?.message,
},
400,
);
}
console.error("api error:", error);
// @ts-expect-error
return c.json(error.toResponse(), error.statusCode());
}
// Handle HTTP exceptions
if (error instanceof HTTPException) {
console.error("http error:", error);
return c.json(
{
code: "request",
type: "validation",
code: ErrorCodes.Validation.INVALID_PARAMETER,
message: "Invalid request",
},
400,
error.status,
);
}
console.error("unhandled error:", error);
return c.json(
{
code: "internal",
type: "internal",
code: ErrorCodes.Server.INTERNAL_ERROR,
message: "Internal server error",
},
500,
@@ -68,9 +65,8 @@ app.get(
documentation: {
info: {
title: "Nestri API",
description:
"The Nestri API gives you the power to run your own customized cloud gaming platform.",
version: "0.3.0",
description: "The Nestri API gives you the power to run your own customized cloud gaming platform.",
version: "0.0.1",
},
components: {
securitySchemes: {
@@ -81,19 +77,30 @@ app.get(
},
TeamID: {
type: "apiKey",
description:"The team ID to use for this query",
description: "The team ID to use for this query",
in: "header",
name: "x-nestri-team"
},
},
},
security: [{ Bearer: [], TeamID:[] }],
security: [{ Bearer: [], TeamID: [] }],
servers: [
{ description: "Production", url: "https://api.nestri.io" },
{ description: "Sandbox", url: "https://api.dev.nestri.io" },
],
},
}),
);
export type Routes = typeof routes;
export const handler = process.env.SST_DEV ? handle(app) : streamHandle(app);
patchLogger();
export default {
port: 3001,
idleTimeout: 255,
webSocketHandler: Realtime.webSocketHandler,
fetch: (req: Request) =>
app.fetch(req, undefined, {
waitUntil: (fn) => fn,
passThroughOnException: () => { },
}),
};

View File

@@ -0,0 +1,292 @@
import { z } from "zod"
import { Hono } from "hono";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { Examples } from "@nestri/core/examples";
import { assertActor } from "@nestri/core/actor";
import { ErrorResponses, Result } from "./common";
import { Machine } from "@nestri/core/machine/index";
import { Realtime } from "@nestri/core/realtime/index";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
import { CreateMessageSchema, StartMessageSchema, StopMessageSchema } from "./messages.ts";
export namespace MachineApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Machine"],
summary: "Get all BYOG machines",
description: "All the BYOG machines owned by this user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Machine.Info.array().openapi({
description: "All the user's BYOG machines",
example: [Examples.Machine],
}),
),
},
},
description: "Successfully retrieved all the user's machines",
},
404: ErrorResponses[404],
429: ErrorResponses[429]
}
}),
async (c) => {
const user = assertActor("user");
const machineInfo = await Machine.fromUserID(user.properties.userID);
if (!machineInfo)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"No machines not found",
);
return c.json({ data: machineInfo, }, 200);
})
.get("/hosted",
describeRoute({
tags: ["Machine"],
summary: "Get all cloud machines",
description: "All the machines that are connected to Nestri",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Machine.Info.array().openapi({
description: "All the machines connected to Nestri",
example: [{ ...Examples.Machine, userID: null }],
}),
),
},
},
description: "Successfully retrieved all the hosted machines",
},
404: ErrorResponses[404],
429: ErrorResponses[429]
}
}),
async (c) => {
const machineInfo = await Machine.list();
if (!machineInfo)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"No machines not found",
);
return c.json({ data: machineInfo, }, 200);
})
.post("/",
describeRoute({
tags: ["Machine"],
summary: "Send messages to the machine",
description: "Send messages directly to the machine",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.literal("ok")
),
},
},
description: "Successfully sent the message to Maitred"
},
}
}),
validator(
"json",
z.any()
),
async (c) => {
const actor = assertActor("machine");
console.log("actor.id", actor.properties.machineID)
await Realtime.publish(c.req.valid("json"))
return c.json({
data: "ok"
}, 200);
},
)
.post("/:machineID/create",
describeRoute({
tags: ["Machine"],
summary: "Request to create a container for a specific machine",
description: "Publishes a message to create a container via MQTT for the given machine ID",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
message: z.literal("create request sent"),
})
),
},
},
description: "Create request successfully sent to MQTT",
},
400: {
content: {
"application/json": {
schema: Result(
z.object({ error: z.string() })
),
},
},
description: "Failed to publish create request",
},
},
}),
validator("json", CreateMessageSchema.shape.payload.optional()), // No payload required for create
async (c) => {
const actor = assertActor("machine");
const body = c.req.valid("json");
const message = {
type: "create" as const,
payload: body || {}, // Empty payload if none provided
};
try {
await Realtime.publish(message, "create");
console.log("Published create request to");
} catch (error) {
console.error("Failed to publish to MQTT:", error);
return c.json({ error: "Failed to send create request" }, 400);
}
return c.json({
data: {
message: "create request sent",
},
}, 200);
}
)
.post("/:machineID/start",
describeRoute({
tags: ["Machine"],
summary: "Request to start a container for a specific machine",
description: "Publishes a message to start a container via MQTT for the given machine ID",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
message: z.literal("start request sent"),
})
),
},
},
description: "Start request successfully sent to MQTT",
},
400: {
content: {
"application/json": {
schema: Result(
z.object({ error: z.string() })
),
},
},
description: "Failed to publish start request",
},
},
}),
validator("json", StartMessageSchema.shape.payload), // Use the payload schema
async (c) => {
const actor = assertActor("machine");
const body = c.req.valid("json");
const message = {
type: "start" as const,
payload: {
container_id: body.container_id,
},
};
try {
await Realtime.publish(message, "start");
console.log("Published start request");
} catch (error) {
console.error("Failed to publish to MQTT:", error);
return c.json({ error: "Failed to send start request" }, 400);
}
return c.json({
data: {
message: "start request sent",
},
}, 200);
}
)
.post("/:machineID/stop",
describeRoute({
tags: ["Machine"],
summary: "Request to stop a container for a specific machine",
description: "Publishes a message to stop a container via MQTT for the given machine ID",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
message: z.literal("stop request sent"),
})
),
},
},
description: "Stop request successfully sent to MQTT",
},
400: {
content: {
"application/json": {
schema: Result(
z.object({ error: z.string() })
),
},
},
description: "Failed to publish start request",
},
},
}),
validator("json", StopMessageSchema.shape.payload), // Use the payload schema
async (c) => {
const actor = assertActor("machine");
const body = c.req.valid("json");
const message = {
type: "stop" as const,
payload: {
container_id: body.container_id,
},
};
try {
await Realtime.publish(message, "stop");
console.log("Published stop request");
} catch (error) {
console.error("Failed to publish to MQTT:", error);
return c.json({ error: "Failed to send stop request" }, 400);
}
return c.json({
data: {
message: "stop request sent",
},
}, 200);
}
)
}

View File

@@ -0,0 +1,54 @@
import { z } from "zod"
// Base message interface
export interface BaseMessage {
type: string; // e.g., "start", "stop", "status"
payload: Record<string, any>; // Generic payload, refined by specific types
}
// Specific message types
export interface StartMessage extends BaseMessage {
type: "start";
payload: {
container_id: string;
[key: string]: any; // Allow additional fields for future expansion
};
}
// Example future message type
export interface StopMessage extends BaseMessage {
type: "stop";
payload: {
container_id: string;
[key: string]: any;
};
}
// Union type for all possible messages (expandable)
export type MachineMessage = StartMessage | StopMessage; // Add more types as needed
// Zod schema for validation
export const BaseMessageSchema = z.object({
type: z.string(),
payload: z.record(z.any()),
});
export const CreateMessageSchema = BaseMessageSchema.extend({
type: z.literal("create"),
});
export const StartMessageSchema = BaseMessageSchema.extend({
type: z.literal("start"),
payload: z.object({
container_id: z.string(),
}).passthrough(),
});
export const StopMessageSchema = BaseMessageSchema.extend({
type: z.literal("stop"),
payload: z.object({
container_id: z.string(),
}).passthrough(),
});
export const MachineMessageSchema = z.union([StartMessageSchema, StopMessageSchema]);

View File

@@ -0,0 +1,174 @@
import { z } from "zod";
import { Hono } from "hono";
import { Resource } from "sst";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { User } from "@nestri/core/user/index";
import { assertActor } from "@nestri/core/actor";
import { Polar } from "@nestri/core/polar/index";
import { Examples } from "@nestri/core/examples";
import { ErrorResponses, Result, validator } from "./common";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
import { PlanType } from "@nestri/core/subscription/subscription.sql";
import { WebhookVerificationError, validateEvent } from "@polar-sh/sdk/webhooks";
export namespace PolarApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Polar"],
summary: "Create a Polar.sh customer portal",
description: "Creates Polar.sh's customer portal url where the user can manage their payments",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
portalUrl: z.string()
}).openapi({
description: "The customer portal url",
example: { portalUrl: "https://polar.sh/portal/39393jdie09292" }
})
),
},
},
description: "customer portal url"
},
400: ErrorResponses[400],
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
async (c) => {
const actor = assertActor("user");
const user = await User.fromID(actor.properties.userID);
if (!user)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User not found",
);
if (!user.polarCustomerID)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User does not contain Polar customer ID"
)
const portalUrl = await Polar.createPortal(user.polarCustomerID)
return c.json({
data: {
portalUrl
}
})
}
)
.post("/checkout",
describeRoute({
tags: ["Polar"],
summary: "Create a checkout url",
description: "Creates a Polar.sh's checkout url for the user to pay a subscription for this team",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
checkoutUrl: z.string()
}).openapi({
description: "The checkout url",
example: { checkoutUrl: "https://polar.sh/portal/39393jdie09292" }
})
),
},
},
description: "checkout url"
},
400: ErrorResponses[400],
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
validator(
"json",
z
.object({
planType: z.enum(PlanType),
successUrl: z.string().url("Success url must be a valid url")
})
.openapi({
description: "Details of the team to create",
example: {
planType: Examples.Subscription.planType,
successUrl: "https://your-url.io/thanks"
},
})
),
async (c) => {
const body = c.req.valid("json");
const actor = assertActor("user");
const user = await User.fromID(actor.properties.userID);
if (!user)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User not found",
);
if (!user.polarCustomerID)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User does not contain Polar customer ID"
)
const checkoutUrl = await Polar.createCheckout({ customerID: user.polarCustomerID, planType: body.planType, successUrl: body.successUrl })
return c.json({
data: {
checkoutUrl,
}
})
}
)
.post("/webhook",
async (c) => {
const requestBody = await c.req.text();
const webhookSecret = Resource.PolarWebhookSecret.value
const webhookHeaders = {
"webhook-id": c.req.header("webhook-id") ?? "",
"webhook-timestamp": c.req.header("webhook-timestamp") ?? "",
"webhook-signature": c.req.header("webhook-signature") ?? "",
};
let webhookPayload: ReturnType<typeof validateEvent>;
try {
webhookPayload = validateEvent(
requestBody,
webhookHeaders,
webhookSecret,
);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return c.json({ received: false }, { status: 403 });
}
throw error;
}
await Polar.handleWebhook(webhookPayload)
return c.json({ received: true });
}
)
}

View File

@@ -0,0 +1,28 @@
import { actor } from "actor-core";
// Define a chat room actor
const chatRoom = actor({
// Initialize state when the actor is first created
createState: () => ({
messages: [] as any[],
}),
// Define actions clients can call
actions: {
// Action to send a message
sendMessage: (c, sender, text) => {
// Update state
c.state.messages.push({ sender, text });
// Broadcast to all connected clients
c.broadcast("newMessage", { sender, text });
},
// Action to get chat history
getHistory: (c) => {
return c.state.messages;
}
}
});
export default chatRoom;

View File

@@ -0,0 +1,15 @@
import { setup } from "actor-core";
import chatRoom from "./actor-core";
import { createRouter } from "@actor-core/bun";
export namespace Realtime {
const app = setup({
actors: { chatRoom },
basePath: "/realtime"
});
const realtimeRouter = createRouter(app);
export const route = realtimeRouter.router;
export const webSocketHandler = realtimeRouter.webSocketHandler;
}

View File

@@ -0,0 +1,46 @@
import { Hono } from "hono";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { Examples } from "@nestri/core/examples";
import { assertActor } from "@nestri/core/actor";
import { ErrorResponses, Result } from "./common";
import { Subscription } from "@nestri/core/subscription/index";
export namespace SubscriptionApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Subscription"],
summary: "Get user subscriptions",
description: "Get all user subscriptions",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Subscription.Info.array().openapi({
description: "All the subscriptions this user has",
example: [Examples.Subscription]
})
),
},
},
description: "All user subscriptions"
},
400: ErrorResponses[400],
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
async (c) => {
const actor = assertActor("user")
const subscriptions = await Subscription.fromUserID(actor.properties.userID)
return c.json({
data: subscriptions
})
}
)
}

View File

@@ -0,0 +1,124 @@
import { z } from "zod";
import { Hono } from "hono";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { User } from "@nestri/core/user/index";
import { Team } from "@nestri/core/team/index";
import { Examples } from "@nestri/core/examples";
import { Polar } from "@nestri/core/polar/index";
import { Member } from "@nestri/core/member/index";
import { assertActor, withActor } from "@nestri/core/actor";
import { ErrorResponses, Result, validator } from "./common";
import { Subscription } from "@nestri/core/subscription/index";
import { PlanType } from "@nestri/core/subscription/subscription.sql";
export namespace TeamApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Team"],
summary: "List teams",
description: "List the teams associated with the current user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Team.Info.array().openapi({
description: "List of teams",
example: [Examples.Team]
})
),
},
},
description: "List of teams"
},
}
}),
async (c) => {
return c.json({
data: await User.teams()
}, 200);
},
)
.post("/",
describeRoute({
tags: ["Team"],
summary: "Create a team",
description: "Create a team for the current user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
checkoutUrl: z.string().openapi({
description: "The checkout url to confirm subscription for this team",
example: "https://polar.sh/checkout/2903038439320298377"
})
})
)
}
},
description: "Team created succesfully"
},
400: ErrorResponses[400],
409: ErrorResponses[409],
429: ErrorResponses[429],
500: ErrorResponses[500],
}
}),
validator(
"json",
Team.create.schema
.pick({ slug: true, name: true })
.extend({ planType: z.enum(PlanType), successUrl: z.string().url("Success url must be a valid url") })
.openapi({
description: "Details of the team to create",
example: {
slug: Examples.Team.slug,
name: Examples.Team.name,
planType: Examples.Subscription.planType,
successUrl: "https://your-url.io/thanks"
},
})
),
async (c) => {
const body = c.req.valid("json")
const actor = assertActor("user");
const teamID = await Team.create({ name: body.name, slug: body.slug });
await withActor(
{
type: "system",
properties: {
teamID,
},
},
async () => {
await Member.create({
first: true,
email: actor.properties.email,
});
await Subscription.create({
planType: body.planType,
userID: actor.properties.userID,
// FIXME: Make this make sense
tokens: body.planType === "free" ? 100 : body.planType === "pro" ? 1000 : body.planType === "family" ? 10000 : 0,
});
}
);
const checkoutUrl = await Polar.createCheckout({ planType: body.planType, successUrl: body.successUrl, teamID })
return c.json({
data: {
checkoutUrl,
}
})
}
)
}

View File

@@ -0,0 +1,20 @@
import { ZodError, ZodSchema, z } from 'zod';
import type { Env, ValidationTargets, Context, TypedResponse, Input, MiddlewareHandler } from 'hono';
type Hook<T, E extends Env, P extends string, Target extends keyof ValidationTargets = keyof ValidationTargets, O = {}> = (result: ({
success: true;
data: T;
} | {
success: false;
error: ZodError;
data: T;
}) & {
target: Target;
}, c: Context<E, P>) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>;
type HasUndefined<T> = undefined extends T ? true : false;
declare const zValidator: <T extends ZodSchema<any, z.ZodTypeDef, any>, Target extends keyof ValidationTargets, E extends Env, P extends string, In = z.input<T>, Out = z.output<T>, I extends Input = {
in: HasUndefined<In> extends true ? { [K in Target]?: (In extends ValidationTargets[K] ? In : { [K2 in keyof In]?: ValidationTargets[K][K2] | undefined; }) | undefined; } : { [K_1 in Target]: In extends ValidationTargets[K_1] ? In : { [K2_1 in keyof In]: ValidationTargets[K_1][K2_1]; }; };
out: { [K_2 in Target]: Out; };
}, V extends I = I>(target: Target, schema: T, hook?: Hook<z.TypeOf<T>, E, P, Target, {}> | undefined) => MiddlewareHandler<E, P, V>;
export { type Hook, zValidator };

View File

@@ -2,16 +2,18 @@ import { Resource } from "sst"
import { Select } from "./ui/select";
import { subjects } from "./subjects"
import { logger } from "hono/logger";
import { handle } from "hono/aws-lambda";
import { PasswordUI } from "./ui/password"
import { patchLogger } from "./log-polyfill";
import { issuer } from "@openauthjs/openauth";
import { User } from "@nestri/core/user/index"
import { Email } from "@nestri/core/email/index";
import { handleDiscord, handleGithub } from "./utils";
import { GithubAdapter } from "./ui/adapters/github";
import { Machine } from "@nestri/core/machine/index"
import { DiscordAdapter } from "./ui/adapters/discord";
import { PasswordAdapter } from "./ui/adapters/password"
import { PasswordAdapter } from "./ui/adapters/password";
import { type Provider } from "@openauthjs/openauth/provider/provider"
import { MemoryStorage } from "@openauthjs/openauth/storage/memory";
type OauthUser = {
primary: {
@@ -22,13 +24,20 @@ type OauthUser = {
avatar: any;
username: any;
}
console.log("STORAGE", process.env.STORAGE)
const app = issuer({
select: Select({
providers: {
device: {
hide: true,
},
},
machine: {
hide: true
}
}
}),
//TODO: Create our own Storage
storage: MemoryStorage({
persist: process.env.STORAGE //"/tmp/persist.json",
}),
theme: {
title: "Nestri | Auth",
@@ -44,9 +53,7 @@ const app = issuer({
font: {
family: "Geist, sans-serif",
},
css: `
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap');
`,
css: `@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap');`,
},
subjects,
providers: {
@@ -73,29 +80,25 @@ const app = issuer({
},
}),
),
device: {
type: "device",
machine: {
type: "machine",
async client(input) {
if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
throw new Error("Invalid authorization token");
}
const teamSlug = input.params.team;
if (!teamSlug) {
throw new Error("Team slug is required");
}
// FIXME: Do we really need this?
// if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
// throw new Error("Invalid authorization token");
// }
const hostname = input.params.hostname;
if (!hostname) {
const fingerprint = input.params.fingerprint;
if (!fingerprint) {
throw new Error("Hostname is required");
}
return {
hostname,
teamSlug
fingerprint,
};
},
init() { }
} as Provider<{ teamSlug: string; hostname: string; }>,
} as Provider<{ fingerprint: string; }>,
},
allow: async (input) => {
const url = new URL(input.redirectURI);
@@ -104,20 +107,47 @@ const app = issuer({
if (hostname === "localhost") return true;
return false;
},
success: async (ctx, value) => {
// if (value.provider === "device") {
// const team = await Teams.fromSlug(value.teamSlug)
// console.log("team", team)
// console.log("teamSlug", value.teamSlug)
// if (team) {
// await Instances.create({ hostname: value.hostname, teamID: team.id })
success: async (ctx, value, req) => {
if (value.provider === "machine") {
const countryCode = req.headers.get('CloudFront-Viewer-Country') || 'Unknown'
const country = req.headers.get('CloudFront-Viewer-Country-Name') || 'Unknown'
const latitude = Number(req.headers.get('CloudFront-Viewer-Latitude')) || 0
const longitude = Number(req.headers.get('CloudFront-Viewer-Longitude')) || 0
const timezone = req.headers.get('CloudFront-Viewer-Time-Zone') || 'Unknown'
const fingerprint = value.fingerprint
// return await ctx.subject("device", {
// teamSlug: value.teamSlug,
// hostname: value.hostname,
// })
// }
// }
const existing = await Machine.fromFingerprint(fingerprint)
if (!existing) {
const machineID = await Machine.create({
countryCode,
country,
fingerprint,
timezone,
location: {
latitude,
longitude
},
//FIXME: Make this better
// userID: null
})
return ctx.subject("machine", {
machineID,
fingerprint
});
}
return ctx.subject("machine", {
machineID: existing.id,
fingerprint
});
}
// TODO: This works, so use this while registering the task
// console.log("country_code", req.headers.get('CloudFront-Viewer-Country'))
// console.log("country_name", req.headers.get('CloudFront-Viewer-Country-Name'))
// console.log("latitude", req.headers.get('CloudFront-Viewer-Latitude'))
// console.log("longitude", req.headers.get('CloudFront-Viewer-Longitude'))
// console.log("timezone", req.headers.get('CloudFront-Viewer-Time-Zone'))
if (value.provider === "password") {
const email = value.email
@@ -136,12 +166,17 @@ const app = issuer({
return ctx.subject("user", {
userID,
email
}, {
subject: email
});
} else if (matching) {
//Sign In
return ctx.subject("user", {
userID: matching.id,
email
}, {
subject: email
});
}
}
@@ -175,12 +210,16 @@ const app = issuer({
return ctx.subject("user", {
userID,
email: user.primary.email
}, {
subject: user.primary.email
});
} else {
//Sign In
return await ctx.subject("user", {
userID: matching.id,
email: user.primary.email
}, {
subject: user.primary.email
});
}
@@ -194,4 +233,14 @@ const app = issuer({
},
}).use(logger())
export const handler = handle(app)
patchLogger();
export default {
port: 3002,
idleTimeout: 255,
fetch: (req: Request) =>
app.fetch(req, undefined, {
waitUntil: (fn) => fn,
passThroughOnException: () => { },
}),
};

View File

@@ -1,10 +0,0 @@
import { z } from "zod";
import { resolver } from "hono-openapi/zod";
export function Result<T extends z.ZodTypeAny>(schema: T) {
return resolver(
z.object({
data: schema,
}),
);
}

View File

@@ -1,9 +0,0 @@
export class VisibleError extends Error {
constructor(
public kind: "input" | "auth",
public code: string,
public message: string,
) {
super(message);
}
}

View File

@@ -0,0 +1,27 @@
import { format } from "util";
/**
* Overrides the default Node.js console logging methods with a custom logger.
*
* This function patches console.log, console.warn, console.error, console.trace, and console.debug so that each logs
* messages prefixed with a log level. The messages are formatted using Node.js formatting conventions, with newline
* characters replaced by carriage returns, and are written directly to standard output.
*
* @example
* patchLogger();
* console.info("Server started on port %d", 3000);
*/
export function patchLogger() {
const log =
(level: "INFO" | "WARN" | "TRACE" | "DEBUG" | "ERROR") =>
(msg: string, ...rest: any[]) => {
let line = `${level}\t${format(msg, ...rest)}`;
line = line.replace(/\n/g, "\r");
process.stdout.write(line + "\n");
};
console.log = log("INFO");
console.warn = log("WARN");
console.error = log("ERROR");
console.trace = log("TRACE");
console.debug = log("DEBUG");
}

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