@0x0501/mystack 0.0.0-stage → 0.1.0-bootstrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/assets/auth.sqlite.schema.ts +479 -0
- package/assets/bun-sqlite.test.ts +46 -0
- package/assets/d1.test.ts +41 -0
- package/assets/drizzle.config.ts +2 -0
- package/assets/generate-auth-schema.mjs +25 -0
- package/assets/platform.sqlite.schema.ts +46 -0
- package/assets/polar-checkout.ts +10 -0
- package/assets/polar-route.ts +19 -0
- package/assets/polar.test.ts +30 -0
- package/assets/polar.ts +12 -0
- package/assets/purchase.bun-sqlite.ts +38 -0
- package/assets/purchase.sqlite.ts +39 -0
- package/assets/sqlite-migrations/0000_thin_phalanx.sql +295 -0
- package/assets/sqlite-migrations/meta/0000_snapshot.json +2058 -0
- package/assets/sqlite-migrations/meta/_journal.json +13 -0
- package/assets/turso.test.ts +40 -0
- package/package.json +44 -5
- package/src/adapters/database.mjs +448 -0
- package/src/adapters/deployment.mjs +101 -0
- package/src/adapters/email.mjs +75 -0
- package/src/adapters/payments.mjs +270 -0
- package/src/catalog.mjs +128 -0
- package/src/cli.mjs +294 -0
- package/src/generate.mjs +183 -0
- package/src/template.mjs +93 -0
- package/template/.env.docker +16 -0
- package/template/.env.example +67 -0
- package/template/DESIGN.md +58 -0
- package/template/LICENSE +21 -0
- package/template/biome.json +46 -0
- package/template/docker-compose.yml +47 -0
- package/template/docs/project-guide.md +61 -0
- package/template/drizzle/0000_create-app-role.sql +54 -0
- package/template/drizzle/0001_add-rls-context.sql +21 -0
- package/template/drizzle/0002_platform_tables.sql +320 -0
- package/template/drizzle/meta/0002_snapshot.json +2125 -0
- package/template/drizzle/meta/_journal.json +27 -0
- package/template/drizzle.config.ts +25 -0
- package/template/e2e/authenticated-ui.spec.ts +40 -0
- package/template/e2e/headless-design.spec.ts +66 -0
- package/template/e2e/navigation-loading.spec.ts +70 -0
- package/template/e2e/theme-responsive.spec.ts +260 -0
- package/template/gitignore +24 -0
- package/template/messages/de.json +212 -0
- package/template/messages/en.json +212 -0
- package/template/package.json +101 -0
- package/template/paraglide.config.ts +49 -0
- package/template/playwright.config.ts +58 -0
- package/template/project.inlang/settings.json +12 -0
- package/template/public/drizzle.svg +1 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logo192.png +0 -0
- package/template/public/logo512.png +0 -0
- package/template/public/manifest.json +25 -0
- package/template/public/robots.txt +3 -0
- package/template/scripts/check-client-bundle.ts +150 -0
- package/template/scripts/check-server-fns.ts +290 -0
- package/template/scripts/check-sql-raw.ts +50 -0
- package/template/scripts/compile-paraglide.ts +12 -0
- package/template/scripts/postgres/README.md +76 -0
- package/template/scripts/postgres/create-app-role.sh +60 -0
- package/template/scripts/seed-local.ts +331 -0
- package/template/scripts/validate-prod-env.ts +53 -0
- package/template/src/__tests__/README.md +9 -0
- package/template/src/__tests__/account-security.test.ts +664 -0
- package/template/src/__tests__/admin-http-surface.test.ts +311 -0
- package/template/src/__tests__/admin-peer-protection.test.ts +178 -0
- package/template/src/__tests__/auth-harness.ts +104 -0
- package/template/src/__tests__/auth-rate-limit.test.ts +43 -0
- package/template/src/__tests__/auth-redirect.test.ts +120 -0
- package/template/src/__tests__/body-limit.test.ts +47 -0
- package/template/src/__tests__/creem.test.ts +62 -0
- package/template/src/__tests__/demo-checkout.test.ts +141 -0
- package/template/src/__tests__/email-locale.test.ts +110 -0
- package/template/src/__tests__/global-setup.ts +80 -0
- package/template/src/__tests__/input-rules.test.ts +0 -0
- package/template/src/__tests__/message-parity.test.ts +38 -0
- package/template/src/__tests__/navigation-loading.test.tsx +252 -0
- package/template/src/__tests__/nowpayments.test.ts +106 -0
- package/template/src/__tests__/paraglide-url-contract.test.ts +87 -0
- package/template/src/__tests__/purchase-fulfillment.test.ts +204 -0
- package/template/src/__tests__/query-unauthorized-redirect.test.ts +95 -0
- package/template/src/__tests__/rls-policy.test.ts +120 -0
- package/template/src/__tests__/same-page-locale-href.test.ts +47 -0
- package/template/src/__tests__/search-params-oauth.test.ts +57 -0
- package/template/src/__tests__/security-headers.test.ts +12 -0
- package/template/src/__tests__/server-fn-error-contract.test.ts +173 -0
- package/template/src/__tests__/stripe.test.ts +84 -0
- package/template/src/__tests__/test-db.ts +32 -0
- package/template/src/__tests__/test-rls-db.ts +23 -0
- package/template/src/__tests__/user-update-guard.test.ts +109 -0
- package/template/src/components/LocaleSwitcher.tsx +79 -0
- package/template/src/components/ThemeToggle.tsx +47 -0
- package/template/src/components/auth/social.tsx +90 -0
- package/template/src/components/auth/turnstile.tsx +67 -0
- package/template/src/components/root/navigation-progress.tsx +32 -0
- package/template/src/components/root/not-found.tsx +20 -0
- package/template/src/components/root/pending.tsx +9 -0
- package/template/src/components/root/root-document.tsx +37 -0
- package/template/src/components/root/root-error.tsx +34 -0
- package/template/src/components/shells.tsx +201 -0
- package/template/src/components/ui/styles.ts +9 -0
- package/template/src/db/auth.schema.ts +455 -0
- package/template/src/db/helper.ts +38 -0
- package/template/src/db/index.ts +15 -0
- package/template/src/db/platform.schema.ts +122 -0
- package/template/src/db/schema.ts +2 -0
- package/template/src/env.ts +65 -0
- package/template/src/hooks/use-theme.ts +32 -0
- package/template/src/integrations/better-auth/header-user.tsx +36 -0
- package/template/src/integrations/better-auth/session-query.ts +11 -0
- package/template/src/integrations/react-email/PasswordOtpEmail.tsx +62 -0
- package/template/src/integrations/react-email/ResetPasswordEmail.tsx +64 -0
- package/template/src/integrations/react-email/VerifyEmail.tsx +72 -0
- package/template/src/integrations/react-email/theme.ts +75 -0
- package/template/src/integrations/tanstack-query/devtools.tsx +6 -0
- package/template/src/integrations/tanstack-query/root-provider.tsx +60 -0
- package/template/src/lib/auth-client.ts +27 -0
- package/template/src/lib/auth-paths.ts +94 -0
- package/template/src/lib/auth-plugins/existing-email-signup.ts +86 -0
- package/template/src/lib/auth-plugins/field-rules.ts +84 -0
- package/template/src/lib/auth-plugins/social-two-factor.ts +194 -0
- package/template/src/lib/auth-timing.ts +9 -0
- package/template/src/lib/auth.ts +279 -0
- package/template/src/lib/captcha.ts +11 -0
- package/template/src/lib/email.tsx +162 -0
- package/template/src/lib/github.ts +18 -0
- package/template/src/lib/locale-cookie.ts +12 -0
- package/template/src/lib/localized-pages.ts +25 -0
- package/template/src/lib/oauth-metadata.ts +19 -0
- package/template/src/lib/permissions.ts +29 -0
- package/template/src/lib/security-headers.ts +20 -0
- package/template/src/lib/site.ts +59 -0
- package/template/src/lib/theme.ts +42 -0
- package/template/src/lib/user-update-guard.ts +35 -0
- package/template/src/middlewares/admin.ts +46 -0
- package/template/src/middlewares/auth-rate-limit.ts +37 -0
- package/template/src/middlewares/better-auth.ts +15 -0
- package/template/src/middlewares/body-limit.ts +62 -0
- package/template/src/middlewares/database.ts +13 -0
- package/template/src/middlewares/protected.ts +45 -0
- package/template/src/routeTree.gen.ts +536 -0
- package/template/src/router.tsx +53 -0
- package/template/src/routes/[.]well-known/oauth-authorization-server/api/auth.ts +18 -0
- package/template/src/routes/[.]well-known/oauth-protected-resource/index.ts +32 -0
- package/template/src/routes/__root.tsx +175 -0
- package/template/src/routes/admin/audit.tsx +74 -0
- package/template/src/routes/admin/index.tsx +7 -0
- package/template/src/routes/admin/oauth-apps.tsx +164 -0
- package/template/src/routes/admin/route.tsx +27 -0
- package/template/src/routes/admin/users.tsx +215 -0
- package/template/src/routes/api/auth/$.ts +14 -0
- package/template/src/routes/api/auth/[.]well-known/openid-configuration.ts +15 -0
- package/template/src/routes/api/webhooks/creem.ts +69 -0
- package/template/src/routes/api/webhooks/nowpayments.ts +62 -0
- package/template/src/routes/api/webhooks/stripe.ts +74 -0
- package/template/src/routes/dashboard/account.tsx +430 -0
- package/template/src/routes/dashboard/index.tsx +128 -0
- package/template/src/routes/dashboard/route.tsx +17 -0
- package/template/src/routes/forgot-password.tsx +65 -0
- package/template/src/routes/index.tsx +85 -0
- package/template/src/routes/oauth/consent.tsx +77 -0
- package/template/src/routes/reset-password.tsx +78 -0
- package/template/src/routes/sign-in.tsx +174 -0
- package/template/src/routes/sign-up.tsx +135 -0
- package/template/src/routes/two-factor.tsx +74 -0
- package/template/src/server/README.md +3 -0
- package/template/src/server/account-security.ts +163 -0
- package/template/src/server/admin-audit.ts +37 -0
- package/template/src/server/admin-users.ts +112 -0
- package/template/src/server/auth.ts +44 -0
- package/template/src/server/demo-checkout.ts +67 -0
- package/template/src/server/oauth-clients.ts +83 -0
- package/template/src/server/payment-toggles.ts +39 -0
- package/template/src/server.ts +20 -0
- package/template/src/services/README.md +3 -0
- package/template/src/services/account-security.ts +893 -0
- package/template/src/services/admin-security.ts +164 -0
- package/template/src/services/admin-users.ts +181 -0
- package/template/src/services/auth-rate-limit.ts +32 -0
- package/template/src/services/creem.ts +83 -0
- package/template/src/services/demo-checkout.ts +100 -0
- package/template/src/services/nowpayments.ts +154 -0
- package/template/src/services/oauth-client.ts +85 -0
- package/template/src/services/oauth-clients.ts +15 -0
- package/template/src/services/payment-providers.ts +157 -0
- package/template/src/services/payment-toggles.ts +48 -0
- package/template/src/services/purchase.ts +233 -0
- package/template/src/services/stripe.ts +106 -0
- package/template/src/start.ts +35 -0
- package/template/src/styles/layout.css +93 -0
- package/template/src/styles/theme.css +84 -0
- package/template/src/styles/ui.css +233 -0
- package/template/src/styles.css +90 -0
- package/template/src/utils/api-error.ts +129 -0
- package/template/src/utils/auth-errors.ts +157 -0
- package/template/src/utils/auth-redirect.ts +61 -0
- package/template/src/utils/body-limit.ts +84 -0
- package/template/src/utils/cn.ts +6 -0
- package/template/src/utils/input-rules.ts +172 -0
- package/template/src/utils/locale-paths.ts +18 -0
- package/template/src/utils/same-page-locale-href.ts +20 -0
- package/template/src/utils/search-params.ts +71 -0
- package/template/tsconfig.json +30 -0
- package/template/tsr.config.json +6 -0
- package/template/vite.config.ts +24 -0
- package/template/vitest.config.ts +26 -0
- package/template/worker-configuration.d.ts +14660 -0
- package/template/wrangler.jsonc +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Darling
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mystack CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create a configured TanStack Start application with the `@0x0501/mystack` CLI. Install Node.js 22.12+ and Bun, then open the wizard:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bunx @0x0501/mystack
|
|
7
|
+
bunx @0x0501/mystack@latest create my-app
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The package is owned by the personal npm account `0x0501`. Generated projects use Bun and ship their own configuration-specific `README.md` plus `docs/project-guide.md`. New application users do not need a starter checkout. You can also install the CLI globally with `npm install --global @0x0501/mystack` and run `mystack create`.
|
|
11
|
+
|
|
12
|
+
For explicit configurations:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bunx @0x0501/mystack@latest create my-app --yes --database d1 --deployment cloudflare --email resend --payments stripe,polar
|
|
16
|
+
bunx @0x0501/mystack@latest create my-app --yes --database turso-local --deployment bun --email resend --payments none --no-install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The executable runs on Node 22.12+. Generated projects use Bun to install dependencies and run scripts. The wizard follows TanStack CLI's Clack interaction: guided selections, keyboard hints, a configuration summary and final confirmation, progress indicators, and commands to start the project. Use arrow keys to navigate, Space to toggle payment providers, and Enter to confirm; an empty payment selection means none. Ctrl-C or Escape cancels during configuration without creating a project. D1 and Cloudflare Email show incompatible options as disabled with a reason. Flags use the same validation rules; `--yes` skips all prompts. Failed dependency installation keeps the generated project and shows the command to retry.
|
|
20
|
+
|
|
21
|
+
## Databases
|
|
22
|
+
|
|
23
|
+
| Choice | Flag | Connection | Deployments |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| PostgreSQL | `pg` | Direct app-role `DATABASE_URL` | Nitro, Bun |
|
|
26
|
+
| PostgreSQL (Hyperdrive) | `pg-hyperdrive` | Cloudflare `HYPERDRIVE` binding | Cloudflare Workers |
|
|
27
|
+
| Cloudflare D1 | `d1` | Cloudflare `DB` binding | Cloudflare Workers |
|
|
28
|
+
| Turso Cloud | `turso-cloud` | Remote `TURSO_DATABASE_URL` and required `TURSO_AUTH_TOKEN` | All |
|
|
29
|
+
| Turso Local (libSQL) | `turso-local` | `TURSO_DATABASE_URL=file:local.db`, no cloud token | Nitro, Bun |
|
|
30
|
+
| Bun SQLite | `bun-sqlite` | Native `bun:sqlite`, `SQLITE_DATABASE_PATH=local.db` | Bun |
|
|
31
|
+
|
|
32
|
+
The default is PostgreSQL (Hyperdrive) with Cloudflare Workers. PostgreSQL migrations use the privileged `DATABASE_MIGRATION_URL`; runtime connections use the app role. Turso Cloud accepts remote `libsql://` or `https://` URLs and requires cloud credentials before migration and startup. Local databases need persistent file storage in production; generated projects ignore database files and their WAL sidecars in Git.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
bunx @0x0501/mystack@latest create cloud-app --database turso-cloud
|
|
36
|
+
bunx @0x0501/mystack@latest create local-app --database turso-local --deployment nitro --email resend
|
|
37
|
+
bunx @0x0501/mystack@latest create bun-app --database bun-sqlite --deployment bun --email resend
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Start your application
|
|
41
|
+
|
|
42
|
+
Enter the generated directory, copy `.env.example` to `.env`, and configure the selected database/provider credentials. If dependencies were skipped or installation failed, run `bun install`. PostgreSQL variants need `bun run db:up` before migration; all variants use `bun run db:migrate` then `bun run dev`. Open http://localhost:3000. Authentication email is logged in development when provider credentials are absent. Follow the generated README for deployment.
|
|
43
|
+
|
|
44
|
+
`mystack.json` records the creation choices. Editing it does not regenerate or convert an existing application.
|
|
45
|
+
|
|
46
|
+
## Maintain and distribute the CLI
|
|
47
|
+
|
|
48
|
+
Contributors use the repository's [maintainer guide](../../docs/contributing.md) for local CLI development, schema generation, tests and release packaging. The root application is the single runnable template source; `template/` is an ignored release snapshot and `assets/` contains alternate files and database fixtures. See [ADR 0012](../../docs/adr/0012-mystack-project-generation.md).
|
|
49
|
+
|
|
50
|
+
The tarball contains the snapshot and adapters, and works outside the checkout without fetching a moving Git branch. `prepack` refreshes the snapshot from an explicit allowlist; secrets, Git history, dependencies, and local build output are excluded. Tagged releases publish through GitHub Actions using npm trusted publishing; see the repository's [publishing guide](../../docs/publishing.md) for account setup.
|
|
51
|
+
|
|
52
|
+
## Extend
|
|
53
|
+
|
|
54
|
+
`src/catalog.mjs` defines option IDs, labels, and compatibility requirements. Both the wizard and noninteractive validation consume it. Category adapters in `src/adapters/` produce source, dependencies, scripts, environment declarations and resource configuration. Add an option to its catalog and implement it in the category adapter. Payment API definitions live in the payment adapter. Add tests for new option combinations before exposing them. Template edits use checked anchors and fail explicitly when the underlying starter changes.
|
|
55
|
+
|
|
56
|
+
SQLite auth schema is generated from the starter's actual auth config:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
node packages/mystack/scripts/generate-auth-schema.mjs
|
|
60
|
+
bun x drizzle-kit generate --config packages/mystack/assets/drizzle.config.ts
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Never hand-edit the generated auth schema. Platform SQLite tables are maintained in `assets/platform.sqlite.schema.ts`. D1 and Turso use atomic batch operations and a durable `purchase_delivery` event; product fulfillment consumes it using `purchase_id` as its idempotency key. PG supports the transaction callback in `onPurchasePaid`. SQLite authorization is enforced in authenticated/admin middleware and explicit user predicates, rather than PG RLS.
|
|
64
|
+
|
|
65
|
+
Run `bun run test:cli` for the 208 valid selection combinations, filesystem safety and flag constraints. Run `bun run --cwd packages/mystack test:generated` to install, check, typecheck, test and build all 13 database/runtime/email tuples with all payment providers. PG integration checks need Docker. SQLite tests use real local libSQL, D1 (workerd), and native Bun SQLite, including concurrent confirmation, rollback and credential revocation. Turso Cloud uses an isolated local libSQL fixture for service tests; validating a live cloud connection requires your URL and token. Bun SQLite runs its native database tests with `bun test` after Vitest because `bun:sqlite` is unavailable in Node.
|
|
66
|
+
|
|
67
|
+
## Reference
|
|
68
|
+
|
|
69
|
+
The deployment adapters follow [TanStack Start hosting](https://tanstack.com/start/latest/docs/framework/react/guide/hosting). TanStack CLI composes [base, add-on and template files](https://github.com/TanStack/cli/blob/3f2d3d32e0bc745a9b892ce767555a7f7b4dd591/packages/create/src/create-app.ts#L66-L149), renders EJS and merges package metadata. Mystack uses its own fixed catalog, file adapters and checked source transformations; it does not invoke TanStack CLI. See the repository's [source-backed comparison](../../docs/research/tanstack-cli-generation.md). Nitro uses its Node preset; Bun uses Start's server bundle with a native `Bun.serve` entry. Credentials and cloud resource IDs must be supplied by the project owner; generation does not create paid cloud resources.
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { relations, sql } from "drizzle-orm";
|
|
2
|
+
import {
|
|
3
|
+
sqliteTable,
|
|
4
|
+
text,
|
|
5
|
+
integer,
|
|
6
|
+
index,
|
|
7
|
+
uniqueIndex,
|
|
8
|
+
} from "drizzle-orm/sqlite-core";
|
|
9
|
+
|
|
10
|
+
export const user = sqliteTable("user", {
|
|
11
|
+
id: text("id").primaryKey(),
|
|
12
|
+
name: text("name").notNull(),
|
|
13
|
+
email: text("email").notNull().unique(),
|
|
14
|
+
emailVerified: integer("email_verified", { mode: "boolean" })
|
|
15
|
+
.default(false)
|
|
16
|
+
.notNull(),
|
|
17
|
+
image: text("image"),
|
|
18
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
19
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
20
|
+
.notNull(),
|
|
21
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
22
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
23
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
24
|
+
.notNull(),
|
|
25
|
+
role: text("role"),
|
|
26
|
+
banned: integer("banned", { mode: "boolean" }).default(false),
|
|
27
|
+
banReason: text("ban_reason"),
|
|
28
|
+
banExpires: integer("ban_expires", { mode: "timestamp_ms" }),
|
|
29
|
+
twoFactorEnabled: integer("two_factor_enabled", { mode: "boolean" }).default(
|
|
30
|
+
false,
|
|
31
|
+
),
|
|
32
|
+
tokensRevokedAt: integer("tokens_revoked_at", { mode: "timestamp_ms" }),
|
|
33
|
+
locale: text("locale"),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const session = sqliteTable(
|
|
37
|
+
"session",
|
|
38
|
+
{
|
|
39
|
+
id: text("id").primaryKey(),
|
|
40
|
+
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
41
|
+
token: text("token").notNull().unique(),
|
|
42
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
43
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
44
|
+
.notNull(),
|
|
45
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
46
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
47
|
+
.notNull(),
|
|
48
|
+
ipAddress: text("ip_address"),
|
|
49
|
+
userAgent: text("user_agent"),
|
|
50
|
+
userId: text("user_id")
|
|
51
|
+
.notNull()
|
|
52
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
53
|
+
impersonatedBy: text("impersonated_by"),
|
|
54
|
+
},
|
|
55
|
+
(table) => [index("session_userId_idx").on(table.userId)],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export const account = sqliteTable(
|
|
59
|
+
"account",
|
|
60
|
+
{
|
|
61
|
+
id: text("id").primaryKey(),
|
|
62
|
+
accountId: text("account_id").notNull(),
|
|
63
|
+
providerId: text("provider_id").notNull(),
|
|
64
|
+
userId: text("user_id")
|
|
65
|
+
.notNull()
|
|
66
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
67
|
+
accessToken: text("access_token"),
|
|
68
|
+
refreshToken: text("refresh_token"),
|
|
69
|
+
idToken: text("id_token"),
|
|
70
|
+
accessTokenExpiresAt: integer("access_token_expires_at", {
|
|
71
|
+
mode: "timestamp_ms",
|
|
72
|
+
}),
|
|
73
|
+
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
|
74
|
+
mode: "timestamp_ms",
|
|
75
|
+
}),
|
|
76
|
+
scope: text("scope"),
|
|
77
|
+
password: text("password"),
|
|
78
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
79
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
80
|
+
.notNull(),
|
|
81
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
82
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
83
|
+
.notNull(),
|
|
84
|
+
},
|
|
85
|
+
(table) => [index("account_userId_idx").on(table.userId)],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const verification = sqliteTable(
|
|
89
|
+
"verification",
|
|
90
|
+
{
|
|
91
|
+
id: text("id").primaryKey(),
|
|
92
|
+
identifier: text("identifier").notNull(),
|
|
93
|
+
value: text("value").notNull(),
|
|
94
|
+
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
95
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
96
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
97
|
+
.notNull(),
|
|
98
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
99
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
100
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
101
|
+
.notNull(),
|
|
102
|
+
},
|
|
103
|
+
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
export const jwks = sqliteTable("jwks", {
|
|
107
|
+
id: text("id").primaryKey(),
|
|
108
|
+
publicKey: text("public_key").notNull(),
|
|
109
|
+
privateKey: text("private_key").notNull(),
|
|
110
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
111
|
+
expiresAt: integer("expires_at", { mode: "timestamp_ms" }),
|
|
112
|
+
alg: text("alg"),
|
|
113
|
+
crv: text("crv"),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const oauthClient = sqliteTable(
|
|
117
|
+
"oauth_client",
|
|
118
|
+
{
|
|
119
|
+
id: text("id").primaryKey(),
|
|
120
|
+
clientId: text("client_id").notNull().unique(),
|
|
121
|
+
clientSecret: text("client_secret"),
|
|
122
|
+
clientDiscoveryId: text("client_discovery_id"),
|
|
123
|
+
disabled: integer("disabled", { mode: "boolean" }).default(false),
|
|
124
|
+
skipConsent: integer("skip_consent", { mode: "boolean" }),
|
|
125
|
+
enableEndSession: integer("enable_end_session", { mode: "boolean" }),
|
|
126
|
+
subjectType: text("subject_type"),
|
|
127
|
+
scopes: text("scopes", { mode: "json" }),
|
|
128
|
+
clientCredentialsScopes: text("client_credentials_scopes", {
|
|
129
|
+
mode: "json",
|
|
130
|
+
}).default([]),
|
|
131
|
+
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
|
132
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }),
|
|
133
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" }),
|
|
134
|
+
name: text("name"),
|
|
135
|
+
uri: text("uri"),
|
|
136
|
+
icon: text("icon"),
|
|
137
|
+
contacts: text("contacts", { mode: "json" }),
|
|
138
|
+
tos: text("tos"),
|
|
139
|
+
policy: text("policy"),
|
|
140
|
+
softwareId: text("software_id"),
|
|
141
|
+
softwareVersion: text("software_version"),
|
|
142
|
+
softwareStatement: text("software_statement"),
|
|
143
|
+
redirectUris: text("redirect_uris", { mode: "json" }).notNull(),
|
|
144
|
+
postLogoutRedirectUris: text("post_logout_redirect_uris", { mode: "json" }),
|
|
145
|
+
backchannelLogoutUri: text("backchannel_logout_uri"),
|
|
146
|
+
backchannelLogoutSessionRequired: integer(
|
|
147
|
+
"backchannel_logout_session_required",
|
|
148
|
+
{ mode: "boolean" },
|
|
149
|
+
),
|
|
150
|
+
tokenEndpointAuthMethod: text("token_endpoint_auth_method"),
|
|
151
|
+
applicationType: text("application_type"),
|
|
152
|
+
jwks: text("jwks"),
|
|
153
|
+
jwksUri: text("jwks_uri"),
|
|
154
|
+
grantTypes: text("grant_types", { mode: "json" }),
|
|
155
|
+
responseTypes: text("response_types", { mode: "json" }),
|
|
156
|
+
requirePKCE: integer("require_pkce", { mode: "boolean" }),
|
|
157
|
+
dpopBoundAccessTokens: integer("dpop_bound_access_tokens", {
|
|
158
|
+
mode: "boolean",
|
|
159
|
+
}).default(false),
|
|
160
|
+
referenceId: text("reference_id"),
|
|
161
|
+
metadata: text("metadata", { mode: "json" }),
|
|
162
|
+
},
|
|
163
|
+
(table) => [index("oauthClient_userId_idx").on(table.userId)],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
export const oauthResource = sqliteTable("oauth_resource", {
|
|
167
|
+
id: text("id").primaryKey(),
|
|
168
|
+
identifier: text("identifier").notNull().unique(),
|
|
169
|
+
name: text("name").notNull(),
|
|
170
|
+
accessTokenTtl: integer("access_token_ttl"),
|
|
171
|
+
refreshTokenTtl: integer("refresh_token_ttl"),
|
|
172
|
+
signingAlgorithm: text("signing_algorithm"),
|
|
173
|
+
signingKeyId: text("signing_key_id"),
|
|
174
|
+
allowedScopes: text("allowed_scopes", { mode: "json" }),
|
|
175
|
+
customClaims: text("custom_claims", { mode: "json" }),
|
|
176
|
+
dpopBoundAccessTokensRequired: integer("dpop_bound_access_tokens_required", {
|
|
177
|
+
mode: "boolean",
|
|
178
|
+
}).default(false),
|
|
179
|
+
disabled: integer("disabled", { mode: "boolean" }).default(false),
|
|
180
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }),
|
|
181
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" }),
|
|
182
|
+
policyVersion: integer("policy_version").default(1),
|
|
183
|
+
metadata: text("metadata", { mode: "json" }),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export const oauthClientResource = sqliteTable(
|
|
187
|
+
"oauth_client_resource",
|
|
188
|
+
{
|
|
189
|
+
id: text("id").primaryKey(),
|
|
190
|
+
clientId: text("client_id")
|
|
191
|
+
.notNull()
|
|
192
|
+
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
|
193
|
+
resourceId: text("resource_id")
|
|
194
|
+
.notNull()
|
|
195
|
+
.references(() => oauthResource.identifier, { onDelete: "cascade" }),
|
|
196
|
+
metadata: text("metadata", { mode: "json" }),
|
|
197
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }),
|
|
198
|
+
},
|
|
199
|
+
(table) => [
|
|
200
|
+
uniqueIndex("oauthClientResource_clientId_resourceId_uidx").on(
|
|
201
|
+
table.clientId,
|
|
202
|
+
table.resourceId,
|
|
203
|
+
),
|
|
204
|
+
index("oauthClientResource_clientId_idx").on(table.clientId),
|
|
205
|
+
index("oauthClientResource_resourceId_idx").on(table.resourceId),
|
|
206
|
+
],
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
export const oauthRefreshToken = sqliteTable(
|
|
210
|
+
"oauth_refresh_token",
|
|
211
|
+
{
|
|
212
|
+
id: text("id").primaryKey(),
|
|
213
|
+
token: text("token").notNull().unique(),
|
|
214
|
+
clientId: text("client_id")
|
|
215
|
+
.notNull()
|
|
216
|
+
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
|
217
|
+
sessionId: text("session_id").references(() => session.id, {
|
|
218
|
+
onDelete: "set null",
|
|
219
|
+
}),
|
|
220
|
+
userId: text("user_id")
|
|
221
|
+
.notNull()
|
|
222
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
223
|
+
referenceId: text("reference_id"),
|
|
224
|
+
authorizationCodeId: text("authorization_code_id"),
|
|
225
|
+
resources: text("resources", { mode: "json" }),
|
|
226
|
+
requestedUserInfoClaims: text("requested_user_info_claims", {
|
|
227
|
+
mode: "json",
|
|
228
|
+
}),
|
|
229
|
+
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
230
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
231
|
+
revoked: integer("revoked", { mode: "timestamp_ms" }),
|
|
232
|
+
rotatedAt: integer("rotated_at", { mode: "timestamp_ms" }),
|
|
233
|
+
rotationReplayResponse: text("rotation_replay_response"),
|
|
234
|
+
rotationReplayExpiresAt: integer("rotation_replay_expires_at", {
|
|
235
|
+
mode: "timestamp_ms",
|
|
236
|
+
}),
|
|
237
|
+
authTime: integer("auth_time", { mode: "timestamp_ms" }),
|
|
238
|
+
confirmation: text("confirmation", { mode: "json" }),
|
|
239
|
+
scopes: text("scopes", { mode: "json" }).notNull(),
|
|
240
|
+
},
|
|
241
|
+
(table) => [
|
|
242
|
+
index("oauthRefreshToken_clientId_idx").on(table.clientId),
|
|
243
|
+
index("oauthRefreshToken_sessionId_idx").on(table.sessionId),
|
|
244
|
+
index("oauthRefreshToken_userId_idx").on(table.userId),
|
|
245
|
+
index("oauthRefreshToken_authorizationCodeId_idx").on(
|
|
246
|
+
table.authorizationCodeId,
|
|
247
|
+
),
|
|
248
|
+
],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
export const oauthAccessToken = sqliteTable(
|
|
252
|
+
"oauth_access_token",
|
|
253
|
+
{
|
|
254
|
+
id: text("id").primaryKey(),
|
|
255
|
+
token: text("token").notNull().unique(),
|
|
256
|
+
clientId: text("client_id")
|
|
257
|
+
.notNull()
|
|
258
|
+
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
|
259
|
+
sessionId: text("session_id").references(() => session.id, {
|
|
260
|
+
onDelete: "set null",
|
|
261
|
+
}),
|
|
262
|
+
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
|
263
|
+
referenceId: text("reference_id"),
|
|
264
|
+
authorizationCodeId: text("authorization_code_id"),
|
|
265
|
+
resources: text("resources", { mode: "json" }),
|
|
266
|
+
requestedUserInfoClaims: text("requested_user_info_claims", {
|
|
267
|
+
mode: "json",
|
|
268
|
+
}),
|
|
269
|
+
refreshId: text("refresh_id").references(() => oauthRefreshToken.id, {
|
|
270
|
+
onDelete: "cascade",
|
|
271
|
+
}),
|
|
272
|
+
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
273
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
274
|
+
revoked: integer("revoked", { mode: "timestamp_ms" }),
|
|
275
|
+
confirmation: text("confirmation", { mode: "json" }),
|
|
276
|
+
scopes: text("scopes", { mode: "json" }).notNull(),
|
|
277
|
+
},
|
|
278
|
+
(table) => [
|
|
279
|
+
index("oauthAccessToken_clientId_idx").on(table.clientId),
|
|
280
|
+
index("oauthAccessToken_sessionId_idx").on(table.sessionId),
|
|
281
|
+
index("oauthAccessToken_userId_idx").on(table.userId),
|
|
282
|
+
index("oauthAccessToken_authorizationCodeId_idx").on(
|
|
283
|
+
table.authorizationCodeId,
|
|
284
|
+
),
|
|
285
|
+
index("oauthAccessToken_refreshId_idx").on(table.refreshId),
|
|
286
|
+
],
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
export const oauthConsent = sqliteTable(
|
|
290
|
+
"oauth_consent",
|
|
291
|
+
{
|
|
292
|
+
id: text("id").primaryKey(),
|
|
293
|
+
clientId: text("client_id")
|
|
294
|
+
.notNull()
|
|
295
|
+
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
|
296
|
+
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
|
297
|
+
referenceId: text("reference_id"),
|
|
298
|
+
resources: text("resources", { mode: "json" }),
|
|
299
|
+
requestedUserInfoClaims: text("requested_user_info_claims", {
|
|
300
|
+
mode: "json",
|
|
301
|
+
}),
|
|
302
|
+
scopes: text("scopes", { mode: "json" }).notNull(),
|
|
303
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
304
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
|
|
305
|
+
},
|
|
306
|
+
(table) => [
|
|
307
|
+
index("oauthConsent_clientId_idx").on(table.clientId),
|
|
308
|
+
index("oauthConsent_userId_idx").on(table.userId),
|
|
309
|
+
],
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
export const oauthClientAssertion = sqliteTable("oauth_client_assertion", {
|
|
313
|
+
id: text("id").primaryKey(),
|
|
314
|
+
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
export const twoFactor = sqliteTable(
|
|
318
|
+
"two_factor",
|
|
319
|
+
{
|
|
320
|
+
id: text("id").primaryKey(),
|
|
321
|
+
secret: text("secret").notNull(),
|
|
322
|
+
backupCodes: text("backup_codes").notNull(),
|
|
323
|
+
userId: text("user_id")
|
|
324
|
+
.notNull()
|
|
325
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
326
|
+
verified: integer("verified", { mode: "boolean" }).default(true),
|
|
327
|
+
failedVerificationCount: integer("failed_verification_count").default(0),
|
|
328
|
+
lockedUntil: integer("locked_until", { mode: "timestamp_ms" }),
|
|
329
|
+
},
|
|
330
|
+
(table) => [
|
|
331
|
+
index("twoFactor_secret_idx").on(table.secret),
|
|
332
|
+
index("twoFactor_userId_idx").on(table.userId),
|
|
333
|
+
],
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
export const passkey = sqliteTable(
|
|
337
|
+
"passkey",
|
|
338
|
+
{
|
|
339
|
+
id: text("id").primaryKey(),
|
|
340
|
+
name: text("name"),
|
|
341
|
+
publicKey: text("public_key").notNull(),
|
|
342
|
+
userId: text("user_id")
|
|
343
|
+
.notNull()
|
|
344
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
345
|
+
credentialID: text("credential_id").notNull(),
|
|
346
|
+
counter: integer("counter").notNull(),
|
|
347
|
+
deviceType: text("device_type").notNull(),
|
|
348
|
+
backedUp: integer("backed_up", { mode: "boolean" }).notNull(),
|
|
349
|
+
transports: text("transports"),
|
|
350
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }),
|
|
351
|
+
aaguid: text("aaguid"),
|
|
352
|
+
},
|
|
353
|
+
(table) => [
|
|
354
|
+
index("passkey_userId_idx").on(table.userId),
|
|
355
|
+
index("passkey_credentialID_idx").on(table.credentialID),
|
|
356
|
+
],
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
export const userRelations = relations(user, ({ many }) => ({
|
|
360
|
+
sessions: many(session),
|
|
361
|
+
accounts: many(account),
|
|
362
|
+
oauthClients: many(oauthClient),
|
|
363
|
+
oauthRefreshTokens: many(oauthRefreshToken),
|
|
364
|
+
oauthAccessTokens: many(oauthAccessToken),
|
|
365
|
+
oauthConsents: many(oauthConsent),
|
|
366
|
+
twoFactors: many(twoFactor),
|
|
367
|
+
passkeys: many(passkey),
|
|
368
|
+
}));
|
|
369
|
+
|
|
370
|
+
export const sessionRelations = relations(session, ({ one, many }) => ({
|
|
371
|
+
user: one(user, {
|
|
372
|
+
fields: [session.userId],
|
|
373
|
+
references: [user.id],
|
|
374
|
+
}),
|
|
375
|
+
oauthRefreshTokens: many(oauthRefreshToken),
|
|
376
|
+
oauthAccessTokens: many(oauthAccessToken),
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
export const accountRelations = relations(account, ({ one }) => ({
|
|
380
|
+
user: one(user, {
|
|
381
|
+
fields: [account.userId],
|
|
382
|
+
references: [user.id],
|
|
383
|
+
}),
|
|
384
|
+
}));
|
|
385
|
+
|
|
386
|
+
export const oauthClientRelations = relations(oauthClient, ({ one, many }) => ({
|
|
387
|
+
user: one(user, {
|
|
388
|
+
fields: [oauthClient.userId],
|
|
389
|
+
references: [user.id],
|
|
390
|
+
}),
|
|
391
|
+
oauthClientResources: many(oauthClientResource),
|
|
392
|
+
oauthRefreshTokens: many(oauthRefreshToken),
|
|
393
|
+
oauthAccessTokens: many(oauthAccessToken),
|
|
394
|
+
oauthConsents: many(oauthConsent),
|
|
395
|
+
}));
|
|
396
|
+
|
|
397
|
+
export const oauthResourceRelations = relations(oauthResource, ({ many }) => ({
|
|
398
|
+
oauthClientResources: many(oauthClientResource),
|
|
399
|
+
}));
|
|
400
|
+
|
|
401
|
+
export const oauthClientResourceRelations = relations(
|
|
402
|
+
oauthClientResource,
|
|
403
|
+
({ one }) => ({
|
|
404
|
+
oauthClient: one(oauthClient, {
|
|
405
|
+
fields: [oauthClientResource.clientId],
|
|
406
|
+
references: [oauthClient.clientId],
|
|
407
|
+
}),
|
|
408
|
+
oauthResource: one(oauthResource, {
|
|
409
|
+
fields: [oauthClientResource.resourceId],
|
|
410
|
+
references: [oauthResource.identifier],
|
|
411
|
+
}),
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
export const oauthRefreshTokenRelations = relations(
|
|
416
|
+
oauthRefreshToken,
|
|
417
|
+
({ one, many }) => ({
|
|
418
|
+
oauthClient: one(oauthClient, {
|
|
419
|
+
fields: [oauthRefreshToken.clientId],
|
|
420
|
+
references: [oauthClient.clientId],
|
|
421
|
+
}),
|
|
422
|
+
session: one(session, {
|
|
423
|
+
fields: [oauthRefreshToken.sessionId],
|
|
424
|
+
references: [session.id],
|
|
425
|
+
}),
|
|
426
|
+
user: one(user, {
|
|
427
|
+
fields: [oauthRefreshToken.userId],
|
|
428
|
+
references: [user.id],
|
|
429
|
+
}),
|
|
430
|
+
oauthAccessTokens: many(oauthAccessToken),
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
export const oauthAccessTokenRelations = relations(
|
|
435
|
+
oauthAccessToken,
|
|
436
|
+
({ one }) => ({
|
|
437
|
+
oauthClient: one(oauthClient, {
|
|
438
|
+
fields: [oauthAccessToken.clientId],
|
|
439
|
+
references: [oauthClient.clientId],
|
|
440
|
+
}),
|
|
441
|
+
session: one(session, {
|
|
442
|
+
fields: [oauthAccessToken.sessionId],
|
|
443
|
+
references: [session.id],
|
|
444
|
+
}),
|
|
445
|
+
user: one(user, {
|
|
446
|
+
fields: [oauthAccessToken.userId],
|
|
447
|
+
references: [user.id],
|
|
448
|
+
}),
|
|
449
|
+
oauthRefreshToken: one(oauthRefreshToken, {
|
|
450
|
+
fields: [oauthAccessToken.refreshId],
|
|
451
|
+
references: [oauthRefreshToken.id],
|
|
452
|
+
}),
|
|
453
|
+
}),
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
export const oauthConsentRelations = relations(oauthConsent, ({ one }) => ({
|
|
457
|
+
oauthClient: one(oauthClient, {
|
|
458
|
+
fields: [oauthConsent.clientId],
|
|
459
|
+
references: [oauthClient.clientId],
|
|
460
|
+
}),
|
|
461
|
+
user: one(user, {
|
|
462
|
+
fields: [oauthConsent.userId],
|
|
463
|
+
references: [user.id],
|
|
464
|
+
}),
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
export const twoFactorRelations = relations(twoFactor, ({ one }) => ({
|
|
468
|
+
user: one(user, {
|
|
469
|
+
fields: [twoFactor.userId],
|
|
470
|
+
references: [user.id],
|
|
471
|
+
}),
|
|
472
|
+
}));
|
|
473
|
+
|
|
474
|
+
export const passkeyRelations = relations(passkey, ({ one }) => ({
|
|
475
|
+
user: one(user, {
|
|
476
|
+
fields: [passkey.userId],
|
|
477
|
+
references: [user.id],
|
|
478
|
+
}),
|
|
479
|
+
}));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createDB } from "@/db";
|
|
7
|
+
import { purchase, purchaseDelivery, session, user } from "@/db/schema";
|
|
8
|
+
import { revokeUserCredentials } from "@/services/admin-security";
|
|
9
|
+
import { createPendingPurchase, markPurchasePaid } from "@/services/purchase";
|
|
10
|
+
import { eq } from "drizzle-orm";
|
|
11
|
+
const temporary = mkdtempSync(join(tmpdir(), "mystack-db-"));
|
|
12
|
+
const db = createDB(join(temporary, "test.db"));
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
db.$client.exec(await readFile("drizzle/0000_thin_phalanx.sql", "utf8"));
|
|
15
|
+
await db.insert(user).values({ id: "test-user", email: "test@example.com", name: "Test", emailVerified: true });
|
|
16
|
+
});
|
|
17
|
+
afterAll(() => { db.$client.close(); rmSync(temporary, { recursive: true, force: true }); });
|
|
18
|
+
test("concurrent confirmations deliver a purchase once", async () => {
|
|
19
|
+
const { purchaseId } = await createPendingPurchase(db, { userId: "test-user", provider: "stripe", externalId: "checkout-1", amount: 500 });
|
|
20
|
+
const outcomes = await Promise.all([1, 2].map(() => markPurchasePaid(db, { provider: "stripe", externalId: "checkout-1", reportedAmount: 500 })));
|
|
21
|
+
expect(outcomes.map((outcome) => outcome.status).sort()).toEqual(["already_paid", "fulfilled"]);
|
|
22
|
+
expect((await db.select().from(purchase).where(eq(purchase.id, purchaseId)))[0].status).toBe("paid");
|
|
23
|
+
expect(await db.select().from(purchaseDelivery).where(eq(purchaseDelivery.purchaseId, purchaseId))).toHaveLength(1);
|
|
24
|
+
});
|
|
25
|
+
test("failed delivery insertion rolls back the paid flip and permits retry", async () => {
|
|
26
|
+
const { purchaseId } = await createPendingPurchase(db, { userId: "test-user", provider: "stripe", externalId: "checkout-2", amount: 500 });
|
|
27
|
+
db.$client.exec("CREATE TRIGGER reject_delivery BEFORE INSERT ON purchase_delivery BEGIN SELECT RAISE(ABORT, 'reject'); END");
|
|
28
|
+
try { await expect(markPurchasePaid(db, { provider: "stripe", externalId: "checkout-2" })).rejects.toThrow(); }
|
|
29
|
+
finally { db.$client.exec("DROP TRIGGER reject_delivery"); }
|
|
30
|
+
expect((await db.select().from(purchase).where(eq(purchase.id, purchaseId)))[0].status).toBe("pending");
|
|
31
|
+
expect((await markPurchasePaid(db, { provider: "stripe", externalId: "checkout-2" })).status).toBe("fulfilled");
|
|
32
|
+
});
|
|
33
|
+
test("credential revocation clears sessions and records token revocation time", async () => {
|
|
34
|
+
await db.insert(session).values({ id: "test-session", userId: "test-user", token: "test-token", expiresAt: new Date(Date.now() + 60_000) });
|
|
35
|
+
await revokeUserCredentials(db, "test-user");
|
|
36
|
+
expect(await db.select().from(session)).toEqual([]);
|
|
37
|
+
expect((await db.select().from(user).where(eq(user.id, "test-user")))[0].tokensRevokedAt).toBeInstanceOf(Date);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("failed credential revocation rolls back session deletion", async () => {
|
|
41
|
+
await db.insert(session).values({ id: "rollback-session", userId: "test-user", token: "rollback-token", expiresAt: new Date(Date.now() + 60_000) });
|
|
42
|
+
db.$client.exec("CREATE TRIGGER reject_revocation BEFORE UPDATE ON user BEGIN SELECT RAISE(ABORT, 'reject'); END");
|
|
43
|
+
try { await expect(revokeUserCredentials(db, "test-user")).rejects.toThrow(); }
|
|
44
|
+
finally { db.$client.exec("DROP TRIGGER reject_revocation"); }
|
|
45
|
+
expect(await db.select().from(session).where(eq(session.id, "rollback-session"))).toHaveLength(1);
|
|
46
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { afterAll, beforeAll, expect, test } from "vitest";
|
|
4
|
+
import { getPlatformProxy } from "wrangler";
|
|
5
|
+
import { createDB, type Database } from "@/db";
|
|
6
|
+
import { purchase, purchaseDelivery, session, user } from "@/db/schema";
|
|
7
|
+
import { revokeUserCredentials } from "@/services/admin-security";
|
|
8
|
+
import { createPendingPurchase, markPurchasePaid } from "@/services/purchase";
|
|
9
|
+
let db: Database;
|
|
10
|
+
let dispose: () => Promise<void>;
|
|
11
|
+
let binding: D1Database;
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
const proxy = await getPlatformProxy<{ DB: D1Database }>({ configPath: "wrangler.jsonc", persist: false });
|
|
14
|
+
dispose = proxy.dispose;
|
|
15
|
+
binding = proxy.env.DB;
|
|
16
|
+
const migration = await readFile("drizzle/0000_thin_phalanx.sql", "utf8");
|
|
17
|
+
for (const statement of migration.split("--> statement-breakpoint")) if (statement.trim()) await binding.prepare(statement).run();
|
|
18
|
+
db = createDB(binding);
|
|
19
|
+
await db.insert(user).values({ id: "test-user", email: "test@example.com", name: "Test", emailVerified: true });
|
|
20
|
+
});
|
|
21
|
+
afterAll(async () => { await dispose?.(); });
|
|
22
|
+
test("concurrent confirmations create one durable delivery event", async () => {
|
|
23
|
+
const { purchaseId } = await createPendingPurchase(db, { userId: "test-user", provider: "stripe", externalId: "checkout-1", amount: 500 });
|
|
24
|
+
const outcomes = await Promise.all([1, 2].map(() => markPurchasePaid(db, { provider: "stripe", externalId: "checkout-1", reportedAmount: 500 })));
|
|
25
|
+
expect(outcomes.map((outcome) => outcome.status).sort()).toEqual(["already_paid", "fulfilled"]);
|
|
26
|
+
expect(await db.select().from(purchaseDelivery).where(eq(purchaseDelivery.purchaseId, purchaseId))).toHaveLength(1);
|
|
27
|
+
});
|
|
28
|
+
test("a failed delivery insert rolls back the paid flip and allows retry", async () => {
|
|
29
|
+
const { purchaseId } = await createPendingPurchase(db, { userId: "test-user", provider: "stripe", externalId: "checkout-2", amount: 500 });
|
|
30
|
+
await binding.prepare("CREATE TRIGGER reject_delivery BEFORE INSERT ON purchase_delivery BEGIN SELECT RAISE(ABORT, 'reject'); END").run();
|
|
31
|
+
try { await expect(markPurchasePaid(db, { provider: "stripe", externalId: "checkout-2" })).rejects.toThrow(); }
|
|
32
|
+
finally { await binding.prepare("DROP TRIGGER reject_delivery").run(); }
|
|
33
|
+
expect((await db.select().from(purchase).where(eq(purchase.id, purchaseId)))[0].status).toBe("pending");
|
|
34
|
+
expect((await markPurchasePaid(db, { provider: "stripe", externalId: "checkout-2" })).status).toBe("fulfilled");
|
|
35
|
+
});
|
|
36
|
+
test("credential revocation clears sessions and records token revocation time", async () => {
|
|
37
|
+
await db.insert(session).values({ id: "test-session", userId: "test-user", token: "test-token", expiresAt: new Date(Date.now() + 60_000) });
|
|
38
|
+
await revokeUserCredentials(db, "test-user");
|
|
39
|
+
expect(await db.select().from(session)).toEqual([]);
|
|
40
|
+
expect((await db.select().from(user).where(eq(user.id, "test-user")))[0].tokensRevokedAt).toBeInstanceOf(Date);
|
|
41
|
+
});
|