@12-apps/prisma 5.1.0 → 6.0.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/package.json +16 -13
- package/prisma/migrations/20260731210000_shift_delete_guard/migration.sql +1 -1
- package/prisma/migrations/20260812150000_add_mcp_oauth_tables/migration.sql +6 -6
- package/prisma/migrations/20260812150000_add_onboarding_states/migration.sql +3 -3
- package/prisma/migrations/20260813140000_add_notification_tables/migration.sql +1 -1
- package/prisma/migrations/20260819230000_auth_email_password/migration.sql +58 -0
- package/prisma/plugin-migrations.json +2 -1
- package/prisma/schema/auth.prisma +90 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/prisma",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Prisma host: the multi-file schema folder, the plugin migration seam, and the shared PrismaClient singleton with its audit / append-only extensions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"lint": "pnpm run lint:files .",
|
|
21
21
|
"lint:fix": "bash -c 'eslint \"${@:-.}\" --max-warnings 0 --fix' _",
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
|
-
"prisma:generate": "node scripts/sync-lifecycle-schema.mjs && node scripts/sync-research-schema.mjs && node scripts/sync-shift-schema.mjs --check && node scripts/sync-jobs-schema.mjs --check && node scripts/sync-entitlements-schema.mjs --check && node scripts/sync-payments-schema.mjs --check && node scripts/sync-report-builder-schema.mjs --check && node scripts/sync-rbac-schema.mjs --check && node scripts/sync-notifications-schema.mjs --check && node scripts/sync-onboarding-schema.mjs --check && node scripts/sync-mcp-schema.mjs --check && node scripts/sync-realtime-schema.mjs --check && node scripts/sync-audit-schema.mjs --check && node scripts/sync-prisma-plugins.mjs --check && prisma generate",
|
|
23
|
+
"prisma:generate": "node scripts/sync-lifecycle-schema.mjs && node scripts/sync-research-schema.mjs && node scripts/sync-shift-schema.mjs --check && node scripts/sync-jobs-schema.mjs --check && node scripts/sync-entitlements-schema.mjs --check && node scripts/sync-payments-schema.mjs --check && node scripts/sync-auth-schema.mjs --check && node scripts/sync-report-builder-schema.mjs --check && node scripts/sync-rbac-schema.mjs --check && node scripts/sync-notifications-schema.mjs --check && node scripts/sync-onboarding-schema.mjs --check && node scripts/sync-mcp-schema.mjs --check && node scripts/sync-realtime-schema.mjs --check && node scripts/sync-audit-schema.mjs --check && node scripts/sync-prisma-plugins.mjs --check && prisma generate",
|
|
24
24
|
"prisma:migrate": "prisma migrate dev",
|
|
25
25
|
"prisma:push": "prisma db push",
|
|
26
26
|
"prisma:studio": "prisma studio",
|
|
@@ -49,7 +49,9 @@
|
|
|
49
49
|
"prisma:sync-realtime": "node scripts/sync-realtime-schema.mjs",
|
|
50
50
|
"prisma:sync-realtime:check": "node scripts/sync-realtime-schema.mjs --check",
|
|
51
51
|
"prisma:sync-audit": "node scripts/sync-audit-schema.mjs",
|
|
52
|
-
"prisma:sync-audit:check": "node scripts/sync-audit-schema.mjs --check"
|
|
52
|
+
"prisma:sync-audit:check": "node scripts/sync-audit-schema.mjs --check",
|
|
53
|
+
"prisma:sync-auth": "node scripts/sync-auth-schema.mjs",
|
|
54
|
+
"prisma:sync-auth:check": "node scripts/sync-auth-schema.mjs --check"
|
|
53
55
|
},
|
|
54
56
|
"dependencies": {
|
|
55
57
|
"@electric-sql/pglite": "0.2.17",
|
|
@@ -59,20 +61,21 @@
|
|
|
59
61
|
"pglite-prisma-adapter": "0.7.2"
|
|
60
62
|
},
|
|
61
63
|
"devDependencies": {
|
|
62
|
-
"@12-apps/audit": "^
|
|
64
|
+
"@12-apps/audit": "^5.0.1",
|
|
65
|
+
"@12-apps/auth": "^2.0.0",
|
|
63
66
|
"@12-apps/entitlements": "^3.1.0",
|
|
64
|
-
"@12-apps/entity-lifecycle": "^4.
|
|
67
|
+
"@12-apps/entity-lifecycle": "^4.2.0",
|
|
65
68
|
"@12-apps/eslint-config": "^1.20.0",
|
|
66
|
-
"@12-apps/jobs": "^4.
|
|
67
|
-
"@12-apps/mcp": "^3.
|
|
68
|
-
"@12-apps/notifications": "^4.1.
|
|
69
|
-
"@12-apps/onboarding": "^2.0.
|
|
70
|
-
"@12-apps/payments-backend": "^4.
|
|
69
|
+
"@12-apps/jobs": "^4.2.0",
|
|
70
|
+
"@12-apps/mcp": "^3.4.0",
|
|
71
|
+
"@12-apps/notifications": "^4.1.1",
|
|
72
|
+
"@12-apps/onboarding": "^2.0.1",
|
|
73
|
+
"@12-apps/payments-backend": "^4.11.0",
|
|
71
74
|
"@12-apps/product-research": "^2.0.0",
|
|
72
|
-
"@12-apps/rbac": "^4.0.
|
|
75
|
+
"@12-apps/rbac": "^4.0.2",
|
|
73
76
|
"@12-apps/realtime": "^2.0.0",
|
|
74
|
-
"@12-apps/report-builder": "^5.0.
|
|
75
|
-
"@12-apps/shift": "^3.0.
|
|
77
|
+
"@12-apps/report-builder": "^5.0.2",
|
|
78
|
+
"@12-apps/shift": "^3.0.1",
|
|
76
79
|
"@12-apps/typescript-config": "^1.20.0",
|
|
77
80
|
"@types/node": "^22.10.6",
|
|
78
81
|
"@vitest/coverage-v8": "^3.2.4",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
-- with no foreign key (that is what keeps the package host-agnostic), so a host
|
|
6
6
|
-- that deletes its tenant row and then sweeps the remaining by-value tables by
|
|
7
7
|
-- `client_id` could never remove a shift: the RAISE aborted the whole
|
|
8
|
-
-- transaction.
|
|
8
|
+
-- transaction. Future Pay's demo-store reset does exactly that on EVERY deploy,
|
|
9
9
|
-- so a single shift row would have wedged the deploy step.
|
|
10
10
|
--
|
|
11
11
|
-- Deletability is host policy, not a package invariant. What the guard actually
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
-- Authorization codes are deliberately absent: they are STATELESS signed blobs,
|
|
19
19
|
-- so there is no table to create and nothing to sweep.
|
|
20
20
|
--
|
|
21
|
-
-- The columns, defaults, indexes and CHECK are
|
|
21
|
+
-- The columns, defaults, indexes and CHECK are future-pay's
|
|
22
22
|
-- `20260713120000_add_oauth_client_refresh`,
|
|
23
23
|
-- `20260715180000_add_onboarding_state_mcp_connection` (the mcp_connections half
|
|
24
24
|
-- — the onboarding half belongs to @12-apps/onboarding) and
|
|
25
25
|
-- `20260720120000_add_mcp_connection_host` verbatim, minus the FK to `users`:
|
|
26
26
|
-- this package cannot know the name of a host's user table, and a host that has
|
|
27
|
-
-- one keeps its own constraint (
|
|
27
|
+
-- one keeps its own constraint (future-pay's is ON DELETE CASCADE).
|
|
28
28
|
--
|
|
29
29
|
-- EVERY statement is guarded (`IF NOT EXISTS`, and a conrelid-scoped DO block for
|
|
30
30
|
-- the CHECK, which has no IF NOT EXISTS form). That is what makes adoption by a
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
-- the difference bites exactly the host this file is written for: `CREATE TABLE IF
|
|
36
36
|
-- NOT EXISTS` skips the whole table, columns included, so a host holding an OLDER
|
|
37
37
|
-- shape of one of these tables silently keeps it. Each table below is therefore
|
|
38
|
-
-- followed by a guarded `ADD COLUMN` for every column that reached
|
|
38
|
+
-- followed by a guarded `ADD COLUMN` for every column that reached future-pay in a
|
|
39
39
|
-- LATER migration than its own CREATE. The full audit: `oauth_refresh_tokens
|
|
40
40
|
-- .user_sub` (`20260713150000_add_oauth_refresh_user_sub`) and `mcp_connections
|
|
41
41
|
-- .host` (`20260720120000_add_mcp_connection_host`). `oauth_clients` needs none —
|
|
@@ -112,10 +112,10 @@ CREATE INDEX IF NOT EXISTS "oauth_refresh_tokens_user_email_client_id_idx"
|
|
|
112
112
|
-- `CREATE TABLE IF NOT EXISTS` skips the WHOLE table, so a host that already holds
|
|
113
113
|
-- `oauth_refresh_tokens` in an OLDER SHAPE gets none of the columns declared above
|
|
114
114
|
-- — statement-level guarding is not the same as column-level guarding. That is
|
|
115
|
-
-- precisely how
|
|
115
|
+
-- precisely how future-pay's own history ran: `user_sub` arrived in a SECOND
|
|
116
116
|
-- migration (FUT-105, `20260713150000_add_oauth_refresh_user_sub`), so a host
|
|
117
117
|
-- frozen before it would adopt this file, skip the CREATE, never get the column,
|
|
118
|
-
-- and then fail on every refresh the package serves. Mirror
|
|
118
|
+
-- and then fail on every refresh the package serves. Mirror future-pay's pair
|
|
119
119
|
-- verbatim — guarded add with a backfill default to satisfy NOT NULL, then drop
|
|
120
120
|
-- the default so the column matches the Prisma schema (`String`, no default).
|
|
121
121
|
-- Both statements are no-ops on a fresh host and on a replay.
|
|
@@ -147,6 +147,6 @@ CREATE INDEX IF NOT EXISTS "mcp_connections_last_active_at_idx"
|
|
|
147
147
|
ON "mcp_connections"("last_active_at");
|
|
148
148
|
|
|
149
149
|
-- A host adopting this migration where `mcp_connections` predates the `host`
|
|
150
|
-
-- column (
|
|
150
|
+
-- column (future-pay added it in a later migration) gets it here; a fresh host
|
|
151
151
|
-- already has it from the CREATE above, so the guard makes both cases a no-op.
|
|
152
152
|
ALTER TABLE "mcp_connections" ADD COLUMN IF NOT EXISTS "host" TEXT;
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
-- the package and copied into a host's migrations folder by its
|
|
3
3
|
-- plugin-migration sync.
|
|
4
4
|
--
|
|
5
|
-
-- The columns, defaults, indexes and the status CHECK are
|
|
5
|
+
-- The columns, defaults, indexes and the status CHECK are future-pay's
|
|
6
6
|
-- `20260715180000_add_onboarding_state_mcp_connection` verbatim, minus two
|
|
7
7
|
-- things that are the HOST's vocabulary rather than the package's:
|
|
8
8
|
--
|
|
9
9
|
-- * the FKs to `users` / `clients` — this package cannot know the name of a
|
|
10
10
|
-- host's user or tenant table. A host that has them keeps its own
|
|
11
|
-
-- constraints (
|
|
11
|
+
-- constraints (future-pay's are ON DELETE CASCADE) and they stay
|
|
12
12
|
-- compatible with everything the package writes;
|
|
13
13
|
-- * the `mcp_connections` half of that migration, which belongs to
|
|
14
14
|
-- @12-apps/mcp and ships in ITS folder.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
-- EVERY statement is guarded (`IF NOT EXISTS`, and a conrelid-scoped DO block
|
|
17
17
|
-- for the CHECK, which has no IF NOT EXISTS form). That is what makes adoption
|
|
18
18
|
-- by a host that ALREADY has the table a no-op instead of a failed deploy —
|
|
19
|
-
--
|
|
19
|
+
-- future-pay applies this and nothing changes, no `prisma migrate resolve`
|
|
20
20
|
-- dance required. It is also what lets the PGlite provisioner replay it into an
|
|
21
21
|
-- existing schema, which is how the harness and the integration suites run.
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
--
|
|
25
25
|
-- ============================ REPLAY SAFETY ================================
|
|
26
26
|
-- Every statement is guarded, because the first adopters ALREADY HAVE these
|
|
27
|
-
-- tables:
|
|
27
|
+
-- tables: future-pay created them by hand before the package existed, so this
|
|
28
28
|
-- migration must be a no-op there and correct on an empty database.
|
|
29
29
|
--
|
|
30
30
|
-- The guards are per COLUMN, not per table. `CREATE TABLE IF NOT EXISTS` alone
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- @12-apps/auth's own tables. Self-contained by design: every user reference is
|
|
2
|
+
-- an opaque `user_id` with NO foreign key into a host table, so this migration
|
|
3
|
+
-- applies to any schema regardless of what the host calls its users.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS "auth_credentials" (
|
|
6
|
+
"user_id" TEXT NOT NULL,
|
|
7
|
+
"password_hash" TEXT,
|
|
8
|
+
"password_updated_at" TIMESTAMP(3),
|
|
9
|
+
"email_verified_at" TIMESTAMP(3),
|
|
10
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
11
|
+
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
|
|
13
|
+
CONSTRAINT "auth_credentials_pkey" PRIMARY KEY ("user_id")
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS "auth_tokens" (
|
|
17
|
+
"id" TEXT NOT NULL,
|
|
18
|
+
"user_id" TEXT NOT NULL,
|
|
19
|
+
"purpose" TEXT NOT NULL,
|
|
20
|
+
"token_hash" TEXT NOT NULL,
|
|
21
|
+
"expires_at" TIMESTAMP(3) NOT NULL,
|
|
22
|
+
"consumed_at" TIMESTAMP(3),
|
|
23
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
|
|
25
|
+
CONSTRAINT "auth_tokens_pkey" PRIMARY KEY ("id")
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
-- The purposes, as a CHECK rather than a Prisma enum (house style). A token of
|
|
29
|
+
-- one purpose presented to the other endpoint must read as "no such token".
|
|
30
|
+
ALTER TABLE "auth_tokens"
|
|
31
|
+
ADD CONSTRAINT "auth_tokens_purpose_check"
|
|
32
|
+
CHECK ("purpose" IN ('EMAIL_VERIFICATION', 'PASSWORD_RESET'));
|
|
33
|
+
|
|
34
|
+
-- Unique because the hash IS the lookup key: a collision would mean two
|
|
35
|
+
-- accounts sharing one link.
|
|
36
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "auth_tokens_token_hash_key" ON "auth_tokens"("token_hash");
|
|
37
|
+
CREATE INDEX IF NOT EXISTS "auth_tokens_user_id_purpose_idx" ON "auth_tokens"("user_id", "purpose");
|
|
38
|
+
CREATE INDEX IF NOT EXISTS "auth_tokens_expires_at_idx" ON "auth_tokens"("expires_at");
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS "auth_platform_settings" (
|
|
41
|
+
"key" TEXT NOT NULL,
|
|
42
|
+
"value" JSONB NOT NULL,
|
|
43
|
+
"updated_by" TEXT,
|
|
44
|
+
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
+
|
|
47
|
+
CONSTRAINT "auth_platform_settings_pkey" PRIMARY KEY ("key")
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
-- The two switches, seeded to the posture this ships in: the method is OFF
|
|
51
|
+
-- until an operator turns it on, and verification is ON so that turning the
|
|
52
|
+
-- method on cannot accidentally open unverified registration. Both are the
|
|
53
|
+
-- conservative half of their own trade-off.
|
|
54
|
+
INSERT INTO "auth_platform_settings" ("key", "value", "updated_at")
|
|
55
|
+
VALUES
|
|
56
|
+
('auth.email_password.enabled', 'false'::jsonb, CURRENT_TIMESTAMP),
|
|
57
|
+
('auth.email_password.require_verification', 'true'::jsonb, CURRENT_TIMESTAMP)
|
|
58
|
+
ON CONFLICT ("key") DO NOTHING;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// OWNED BY @12-apps/auth — the host's multi-file schema folder holds a COPY of
|
|
3
|
+
// this file, written by `pnpm --filter @12-apps/auth prisma:sync`. The
|
|
4
|
+
// credential persistence model lives here, in the package, never in the
|
|
5
|
+
// application.
|
|
6
|
+
//
|
|
7
|
+
// COPIED, never symlinked (the entity-lifecycle doctrine): `npm pack` silently
|
|
8
|
+
// drops symlinked entries, `turbo prune` dangles a link whose owner is not a
|
|
9
|
+
// declared dependency, and Prisma silently SKIPS a symlinked migration
|
|
10
|
+
// directory — a green deploy that applied no schema.
|
|
11
|
+
//
|
|
12
|
+
// Deliberately self-contained (the payments-backend doctrine, and the same rule
|
|
13
|
+
// `report-builder.prisma` states): every user reference is a `user_id` String
|
|
14
|
+
// column — NO foreign key into any host table — so these models work in a repo
|
|
15
|
+
// whose user table is called something else, or is in another database, or does
|
|
16
|
+
// not exist yet at migrate time.
|
|
17
|
+
//
|
|
18
|
+
// That constraint is why the three credential columns are a TABLE here rather
|
|
19
|
+
// than columns on the host's `users`, which is where they started. A package
|
|
20
|
+
// cannot add columns to a table it does not own, and a host should not have to
|
|
21
|
+
// hand-edit its own model to install a login flow.
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/// One account's password state, keyed by the host's user id as an OPAQUE
|
|
25
|
+
/// scalar. A row exists only once the address has a credential — an account
|
|
26
|
+
/// that has only ever signed in with Google has none, which is exactly the
|
|
27
|
+
/// "no password yet" state the security card reads.
|
|
28
|
+
model AuthCredential {
|
|
29
|
+
/// The host's user id. No relation by design: a stale id simply matches no
|
|
30
|
+
/// row, which is the same answer a deleted user should produce.
|
|
31
|
+
userId String @id @map("user_id")
|
|
32
|
+
/// `@12-apps/auth`'s self-describing scrypt string. Nullable because a row
|
|
33
|
+
/// may be created by verification before any password is set.
|
|
34
|
+
passwordHash String? @map("password_hash")
|
|
35
|
+
passwordUpdatedAt DateTime? @map("password_updated_at")
|
|
36
|
+
/// Answers "has this address been proven to belong to its owner", which is
|
|
37
|
+
/// not the same question as "does this account have a password".
|
|
38
|
+
emailVerifiedAt DateTime? @map("email_verified_at")
|
|
39
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
40
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
41
|
+
|
|
42
|
+
@@map("auth_credentials")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// A single-use link: e-mail confirmation, or password reset.
|
|
46
|
+
///
|
|
47
|
+
/// Only the HASH is stored. The raw token exists in the mail and nowhere else,
|
|
48
|
+
/// so a database read cannot mint a working link.
|
|
49
|
+
model AuthToken {
|
|
50
|
+
id String @id @default(uuid())
|
|
51
|
+
/// The host's user id, opaque. See the header — no FK.
|
|
52
|
+
userId String @map("user_id")
|
|
53
|
+
/// EMAIL_VERIFICATION | PASSWORD_RESET — a String + DB CHECK, the house style
|
|
54
|
+
/// used elsewhere rather than a Prisma enum. The two purposes never share a
|
|
55
|
+
/// namespace: a verification token presented to the reset endpoint is not a
|
|
56
|
+
/// token at all.
|
|
57
|
+
purpose String
|
|
58
|
+
/// SHA-256 of the raw token, lower-case hex. Unique because it IS the lookup
|
|
59
|
+
/// key, and a collision would mean two accounts sharing one link.
|
|
60
|
+
tokenHash String @unique @map("token_hash")
|
|
61
|
+
expiresAt DateTime @map("expires_at")
|
|
62
|
+
consumedAt DateTime? @map("consumed_at")
|
|
63
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
64
|
+
|
|
65
|
+
/// The sweep-on-password-change query, and the resend path's "does this user
|
|
66
|
+
/// already have one".
|
|
67
|
+
@@index([userId, purpose])
|
|
68
|
+
/// The retention job drops abandoned rows cheaply.
|
|
69
|
+
@@index([expiresAt])
|
|
70
|
+
@@map("auth_tokens")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Platform-wide switches, read on every sign-in attempt.
|
|
74
|
+
///
|
|
75
|
+
/// Prefixed `auth_` rather than the bare `platform_settings` it started as: a
|
|
76
|
+
/// host may well own a settings table of its own, and a package must not claim
|
|
77
|
+
/// a name that generic. Deliberately NOT tenant-scoped — sign-in happens before
|
|
78
|
+
/// a tenant is known.
|
|
79
|
+
model AuthPlatformSetting {
|
|
80
|
+
key String @id
|
|
81
|
+
value Json
|
|
82
|
+
/// Who last changed it, by e-mail, for the audit trail the settings screen
|
|
83
|
+
/// shows. A plain string rather than a relation: the person may be an env
|
|
84
|
+
/// allowlist superadmin with no user row of their own.
|
|
85
|
+
updatedBy String? @map("updated_by")
|
|
86
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
87
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
88
|
+
|
|
89
|
+
@@map("auth_platform_settings")
|
|
90
|
+
}
|