@substrat-run/vertical-auth 0.8.1 → 0.10.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/README.md +16 -2
- package/dist/identity-do.d.ts +32 -11
- package/dist/identity-do.d.ts.map +1 -1
- package/dist/identity-do.js +49 -41
- package/dist/identity-do.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/instance-auth.d.ts +102 -0
- package/dist/instance-auth.d.ts.map +1 -0
- package/dist/instance-auth.js +124 -0
- package/dist/instance-auth.js.map +1 -0
- package/dist/owner-claim-link.d.ts +28 -0
- package/dist/owner-claim-link.d.ts.map +1 -0
- package/dist/owner-claim-link.js +34 -0
- package/dist/owner-claim-link.js.map +1 -0
- package/dist/owner-seat.d.ts +106 -0
- package/dist/owner-seat.d.ts.map +1 -0
- package/dist/owner-seat.js +171 -0
- package/dist/owner-seat.js.map +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -48,8 +48,22 @@ leak across tenants. It holds the provider-agnostic `sub → principal` mapping,
|
|
|
48
48
|
`setPendingOwner` / `resolvePrincipal` are used under **every** provider, including the two
|
|
49
49
|
OIDC ones where Better Auth stays dormant.
|
|
50
50
|
|
|
51
|
-
That mapping is what makes first-run ownership work
|
|
52
|
-
the
|
|
51
|
+
That mapping is what makes first-run ownership work. The owner seat is minted **empty** at
|
|
52
|
+
provision (the platform knows the principal it minted, not the login the tenant's issuer will
|
|
53
|
+
emit), and bound later by a verified subject — two ways:
|
|
54
|
+
|
|
55
|
+
- **First sign-in, inside a window.** For `FIRST_SIGN_IN_WINDOW_MS` (15 minutes) after provision,
|
|
56
|
+
the first authenticated subject claims the seat — the install flow, where the installer opens
|
|
57
|
+
the app seconds later. After that a plain sign-in binds nobody: an instance nobody opened (a CI
|
|
58
|
+
deploy, an issuer with open sign-up) is no longer a seat anyone can take, indefinitely.
|
|
59
|
+
- **A claim link.** The platform asks `mintOwnerClaim` under its secret and the dashboard hands
|
|
60
|
+
the installer a short-lived `/?claim=<token>` link; `claimOwner` binds the subject that
|
|
61
|
+
presents it. Only the token's hash is stored, and minting again retires the earlier link.
|
|
62
|
+
`mintOwnerClaimLink` does the token + hash + URL once, so a vertical's hook is a one-liner.
|
|
63
|
+
|
|
64
|
+
A closed window is not a lost desk: the seat stays pending (`needsSetup` keeps saying so, and
|
|
65
|
+
`ownerSeat` says *why* — open, closed, or already claimed) until a claim binds it. A re-provision
|
|
66
|
+
keeps the window it has and never re-opens a claimed seat. Access is invite-only afterwards.
|
|
53
67
|
|
|
54
68
|
Also exported: `resolveCookieDomain`, which refuses to set a session cookie on a public suffix
|
|
55
69
|
(guarded by [`@substrat-run/psl`](https://npmjs.com/package/@substrat-run/psl)).
|
package/dist/identity-do.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DurableObject } from 'cloudflare:workers';
|
|
2
2
|
import type { AuthProvider } from './provider.js';
|
|
3
3
|
import { type SiteRow } from './site-registry.js';
|
|
4
|
+
import { type OwnerSeat } from './owner-seat.js';
|
|
4
5
|
export type IdentityDoEnv = Record<string, never>;
|
|
5
6
|
export declare class IdentityDO extends DurableObject<IdentityDoEnv> {
|
|
6
7
|
/** This tenant's Better Auth signing secret — generated in this DO, never shared, never a worker binding. */
|
|
@@ -56,10 +57,10 @@ export declare class IdentityDO extends DurableObject<IdentityDoEnv> {
|
|
|
56
57
|
/** The DO's SQLite as the registry's minimal `exec` seam (see site-registry.ts). */
|
|
57
58
|
private get registrySql();
|
|
58
59
|
/**
|
|
59
|
-
* Record the owner seat at provision
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* Record the owner seat at provision (#925): the transient `pending_owner`, bounded by the
|
|
61
|
+
* first-sign-in window, and the durable `owner_of_record` (#332). Idempotent the way the
|
|
62
|
+
* platform needs — a re-run (reconcile, retry) keeps the seat's existing window and never
|
|
63
|
+
* re-opens a seat already claimed.
|
|
63
64
|
*/
|
|
64
65
|
setPendingOwner(scopeId: string, principal: string): Promise<void>;
|
|
65
66
|
/**
|
|
@@ -69,19 +70,34 @@ export declare class IdentityDO extends DurableObject<IdentityDoEnv> {
|
|
|
69
70
|
*/
|
|
70
71
|
getOwnerOfRecord(scopeId: string): Promise<string | null>;
|
|
71
72
|
/**
|
|
72
|
-
* Is this scope awaiting first-run setup? True while its owner seat is unclaimed —
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* sign-up allowed only during this window (after the owner claims, sign-up is invite-only).
|
|
73
|
+
* Is this scope awaiting first-run setup? True while its owner seat is unclaimed — whether or
|
|
74
|
+
* not a plain first sign-in can still claim it (`ownerSeat` says which). The worker uses this
|
|
75
|
+
* to answer `needs-setup` instead of a bare 401, so the SPA can say what to do.
|
|
76
76
|
*/
|
|
77
77
|
needsSetup(scopeId: string): Promise<boolean>;
|
|
78
|
+
/** The seat as the platform sees it: claimed / unclaimed (window open or closed, link live or not) / unknown. */
|
|
79
|
+
ownerSeat(scopeId: string): Promise<OwnerSeat>;
|
|
78
80
|
/**
|
|
79
81
|
* Map a verified subject to a PrincipalId in this scope. If already bound, return it. If
|
|
80
|
-
* not, and the scope's owner seat is unclaimed
|
|
81
|
-
*
|
|
82
|
-
*
|
|
82
|
+
* not, and the scope's owner seat is unclaimed with its first-sign-in window still open,
|
|
83
|
+
* CLAIM it. Otherwise null — a valid login with no seat has no access, and after the
|
|
84
|
+
* window the seat is reached by claim link only. Provider-agnostic: the subject may come
|
|
85
|
+
* from Better Auth or an OIDC issuer.
|
|
83
86
|
*/
|
|
84
87
|
resolvePrincipal(scopeId: string, sub: string): Promise<string | null>;
|
|
88
|
+
/**
|
|
89
|
+
* Mint a claim link for an unclaimed seat: the platform asks for this under its secret and
|
|
90
|
+
* the dashboard hands the installer the link. Only `tokenHash` reaches the DO; a new mint
|
|
91
|
+
* replaces the previous link. Null ⇒ the seat is already claimed (nothing to mint).
|
|
92
|
+
*/
|
|
93
|
+
mintOwnerClaim(scopeId: string, tokenHash: string): Promise<{
|
|
94
|
+
expiresAt: string;
|
|
95
|
+
} | null>;
|
|
96
|
+
/**
|
|
97
|
+
* Claim the seat by link: bind this verified subject to the owner principal if the token's
|
|
98
|
+
* hash matches the live claim. Consumes the seat and the link. Null ⇒ invalid or expired.
|
|
99
|
+
*/
|
|
100
|
+
claimOwner(scopeId: string, sub: string, tokenHash: string): Promise<string | null>;
|
|
85
101
|
/**
|
|
86
102
|
* Record a member invite: a pre-minted `principal` + `roleKey` the caller has already
|
|
87
103
|
* granted at scope level, claimable by whoever presents the token whose hash is `tokenHash`.
|
|
@@ -135,7 +151,12 @@ export type IdentityStub = {
|
|
|
135
151
|
setPendingOwner(scopeId: string, principal: string): Promise<void>;
|
|
136
152
|
getOwnerOfRecord(scopeId: string): Promise<string | null>;
|
|
137
153
|
needsSetup(scopeId: string): Promise<boolean>;
|
|
154
|
+
ownerSeat(scopeId: string): Promise<OwnerSeat>;
|
|
138
155
|
resolvePrincipal(scopeId: string, sub: string): Promise<string | null>;
|
|
156
|
+
mintOwnerClaim(scopeId: string, tokenHash: string): Promise<{
|
|
157
|
+
expiresAt: string;
|
|
158
|
+
} | null>;
|
|
159
|
+
claimOwner(scopeId: string, sub: string, tokenHash: string): Promise<string | null>;
|
|
139
160
|
createInvite(scopeId: string, principal: string, roleKey: string, email: string | null, tokenHash: string): Promise<void>;
|
|
140
161
|
listInvites(scopeId: string): Promise<Array<{
|
|
141
162
|
principal: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-do.d.ts","sourceRoot":"","sources":["../src/identity-do.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAKnD,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,eAAe,CAAC;AAE/D,OAAO,EAOL,KAAK,OAAO,EACb,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"identity-do.d.ts","sourceRoot":"","sources":["../src/identity-do.ts"],"names":[],"mappings":"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"}
|
package/dist/identity-do.js
CHANGED
|
@@ -5,6 +5,7 @@ import { drizzle } from 'drizzle-orm/durable-sqlite';
|
|
|
5
5
|
import * as schema from './auth-schema.js';
|
|
6
6
|
import { resolveCookieDomain } from './cookie-domain.js';
|
|
7
7
|
import { SITE_REGISTRY_DDL, recordSite as recordSiteRow, listSites as listSiteRows, resolveSiteScope as resolveSiteScopeRow, forgetSite as forgetSiteRow, } from './site-registry.js';
|
|
8
|
+
import { OWNER_SEAT_DDL, migrateOwnerSeat, recordOwnerSeat, ownerOfRecord as ownerOfRecordRow, needsSetup as needsSetupRow, ownerSeat as ownerSeatRow, resolvePrincipal as resolvePrincipalRow, mintOwnerClaim as mintOwnerClaimRow, claimOwner as claimOwnerRow, } from './owner-seat.js';
|
|
8
9
|
/**
|
|
9
10
|
* The per-tenant IDENTITY Durable Object — one per tenant, running its OWN Better Auth
|
|
10
11
|
* over its OWN SQLite (Drizzle's Durable-Object driver, not a shared D1), and holding the
|
|
@@ -18,8 +19,9 @@ import { SITE_REGISTRY_DDL, recordSite as recordSiteRow, listSites as listSiteRo
|
|
|
18
19
|
* Two roles, either or both used per deployment:
|
|
19
20
|
* - `doAuthProvider` (below) exposes Better Auth here as an `AuthProvider` (the
|
|
20
21
|
* `better-auth-do` config). With an OIDC provider instead, Better Auth stays dormant.
|
|
21
|
-
* - `setPendingOwner` / `resolvePrincipal` are the identity directory —
|
|
22
|
-
* provider, since the subject → principal mapping is provider-independent.
|
|
22
|
+
* - `setPendingOwner` / `resolvePrincipal` / `claimOwner` are the identity directory —
|
|
23
|
+
* used under EVERY provider, since the subject → principal mapping is provider-independent.
|
|
24
|
+
* The owner seat's rules (the bounded first sign-in, the claim link) are in owner-seat.ts.
|
|
23
25
|
*/
|
|
24
26
|
const SCHEMA_STATEMENTS = [
|
|
25
27
|
`CREATE TABLE IF NOT EXISTS user (
|
|
@@ -46,19 +48,15 @@ const SCHEMA_STATEMENTS = [
|
|
|
46
48
|
created_at INTEGER NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)),
|
|
47
49
|
updated_at INTEGER NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)))`,
|
|
48
50
|
`CREATE INDEX IF NOT EXISTS verification_identifier_idx ON verification (identifier)`,
|
|
49
|
-
// The PROVIDER-AGNOSTIC identity directory
|
|
50
|
-
// PrincipalId it maps to, per scope
|
|
51
|
-
//
|
|
52
|
-
`
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
// (a different DO from the scope's data DO), so it survives a scope-DO storage wipe. A builder-
|
|
59
|
-
// triggered reconcile reads it to re-establish the owner's role grant without the platform
|
|
60
|
-
// secret and without the CP having to persist an owner it doesn't otherwise track.
|
|
61
|
-
`CREATE TABLE IF NOT EXISTS owner_of_record (scope_id TEXT PRIMARY KEY, principal TEXT NOT NULL)`,
|
|
51
|
+
// The PROVIDER-AGNOSTIC identity directory (`identity`: a verified `sub` → the Substrat
|
|
52
|
+
// PrincipalId it maps to, per scope — K-22, the same login is a different principal in each
|
|
53
|
+
// scope, independent of WHICH provider verified it) and the OWNER SEAT around it —
|
|
54
|
+
// `pending_owner` (claimed and consumed), `owner_of_record` (#332, never consumed: the
|
|
55
|
+
// vertical's own durable memory of the owner, in this per-tenant DO rather than the scope's
|
|
56
|
+
// data DO, so it survives a scope-DO wipe and a reconcile can re-grant from it) and
|
|
57
|
+
// `owner_claim` (the claim link's hash, #925). The tables and the rules over them live in
|
|
58
|
+
// `owner-seat.ts` so they are unit-tested without a DO; the methods below delegate there.
|
|
59
|
+
...OWNER_SEAT_DDL,
|
|
62
60
|
// Outstanding member invites (the post-setup join path). Each is a pre-minted principal +
|
|
63
61
|
// role the admin already granted at scope level, waiting for a login to claim it by token.
|
|
64
62
|
// Only the token's HASH is stored — the token itself lives in the accept link, never here.
|
|
@@ -89,6 +87,8 @@ export class IdentityDO extends DurableObject {
|
|
|
89
87
|
ctx.blockConcurrencyWhile(async () => {
|
|
90
88
|
for (const stmt of SCHEMA_STATEMENTS)
|
|
91
89
|
ctx.storage.sql.exec(stmt);
|
|
90
|
+
// Columns the DDL cannot add to a table that already exists (#925's `claim_until`).
|
|
91
|
+
migrateOwnerSeat(ctx.storage.sql);
|
|
92
92
|
// Load-or-generate this tenant's OWN signing secret. Each IdentityDO mints its own on
|
|
93
93
|
// first init and persists it here, so the secret is per-tenant, never leaves the DO,
|
|
94
94
|
// and needs no `wrangler secret put` (which would be one value shared across every
|
|
@@ -177,14 +177,13 @@ export class IdentityDO extends DurableObject {
|
|
|
177
177
|
return this.ctx.storage.sql;
|
|
178
178
|
}
|
|
179
179
|
/**
|
|
180
|
-
* Record the owner seat at provision
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
180
|
+
* Record the owner seat at provision (#925): the transient `pending_owner`, bounded by the
|
|
181
|
+
* first-sign-in window, and the durable `owner_of_record` (#332). Idempotent the way the
|
|
182
|
+
* platform needs — a re-run (reconcile, retry) keeps the seat's existing window and never
|
|
183
|
+
* re-opens a seat already claimed.
|
|
184
184
|
*/
|
|
185
185
|
async setPendingOwner(scopeId, principal) {
|
|
186
|
-
this.
|
|
187
|
-
this.ctx.storage.sql.exec('INSERT OR REPLACE INTO owner_of_record (scope_id, principal) VALUES (?, ?)', scopeId, principal);
|
|
186
|
+
recordOwnerSeat(this.registrySql, scopeId, principal, Date.now());
|
|
188
187
|
}
|
|
189
188
|
/**
|
|
190
189
|
* The scope's durable owner of record, or null if this scope was never provisioned through this
|
|
@@ -192,35 +191,44 @@ export class IdentityDO extends DurableObject {
|
|
|
192
191
|
* owner their role after a scope-DO wipe left the scope enforcing against an empty tuple table.
|
|
193
192
|
*/
|
|
194
193
|
async getOwnerOfRecord(scopeId) {
|
|
195
|
-
|
|
196
|
-
return row?.principal ?? null;
|
|
194
|
+
return ownerOfRecordRow(this.registrySql, scopeId);
|
|
197
195
|
}
|
|
198
196
|
/**
|
|
199
|
-
* Is this scope awaiting first-run setup? True while its owner seat is unclaimed —
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* sign-up allowed only during this window (after the owner claims, sign-up is invite-only).
|
|
197
|
+
* Is this scope awaiting first-run setup? True while its owner seat is unclaimed — whether or
|
|
198
|
+
* not a plain first sign-in can still claim it (`ownerSeat` says which). The worker uses this
|
|
199
|
+
* to answer `needs-setup` instead of a bare 401, so the SPA can say what to do.
|
|
203
200
|
*/
|
|
204
201
|
async needsSetup(scopeId) {
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
return needsSetupRow(this.registrySql, scopeId);
|
|
203
|
+
}
|
|
204
|
+
/** The seat as the platform sees it: claimed / unclaimed (window open or closed, link live or not) / unknown. */
|
|
205
|
+
async ownerSeat(scopeId) {
|
|
206
|
+
return ownerSeatRow(this.registrySql, scopeId, Date.now());
|
|
207
207
|
}
|
|
208
208
|
/**
|
|
209
209
|
* Map a verified subject to a PrincipalId in this scope. If already bound, return it. If
|
|
210
|
-
* not, and the scope's owner seat is unclaimed
|
|
211
|
-
*
|
|
212
|
-
*
|
|
210
|
+
* not, and the scope's owner seat is unclaimed with its first-sign-in window still open,
|
|
211
|
+
* CLAIM it. Otherwise null — a valid login with no seat has no access, and after the
|
|
212
|
+
* window the seat is reached by claim link only. Provider-agnostic: the subject may come
|
|
213
|
+
* from Better Auth or an OIDC issuer.
|
|
213
214
|
*/
|
|
214
215
|
async resolvePrincipal(scopeId, sub) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return
|
|
216
|
+
return resolvePrincipalRow(this.registrySql, scopeId, sub, Date.now());
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Mint a claim link for an unclaimed seat: the platform asks for this under its secret and
|
|
220
|
+
* the dashboard hands the installer the link. Only `tokenHash` reaches the DO; a new mint
|
|
221
|
+
* replaces the previous link. Null ⇒ the seat is already claimed (nothing to mint).
|
|
222
|
+
*/
|
|
223
|
+
async mintOwnerClaim(scopeId, tokenHash) {
|
|
224
|
+
return mintOwnerClaimRow(this.registrySql, scopeId, tokenHash, Date.now());
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Claim the seat by link: bind this verified subject to the owner principal if the token's
|
|
228
|
+
* hash matches the live claim. Consumes the seat and the link. Null ⇒ invalid or expired.
|
|
229
|
+
*/
|
|
230
|
+
async claimOwner(scopeId, sub, tokenHash) {
|
|
231
|
+
return claimOwnerRow(this.registrySql, scopeId, sub, tokenHash, Date.now());
|
|
224
232
|
}
|
|
225
233
|
/**
|
|
226
234
|
* Record a member invite: a pre-minted `principal` + `roleKey` the caller has already
|
package/dist/identity-do.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-do.js","sourceRoot":"","sources":["../src/identity-do.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAE3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EACL,iBAAiB,EACjB,UAAU,IAAI,aAAa,EAC3B,SAAS,IAAI,YAAY,EACzB,gBAAgB,IAAI,mBAAmB,EACvC,UAAU,IAAI,aAAa,GAG5B,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"identity-do.js","sourceRoot":"","sources":["../src/identity-do.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,7 @@ export { oidcAuthProvider, type OidcConfig } from './oidc.js';
|
|
|
3
3
|
export { oidcRpAuthProvider, type OidcRpConfig } from './oidc-rp-provider.js';
|
|
4
4
|
export { IdentityDO, doAuthProvider, type IdentityDoEnv, type IdentityStub } from './identity-do.js';
|
|
5
5
|
export { resolveCookieDomain } from './cookie-domain.js';
|
|
6
|
+
export { FIRST_SIGN_IN_WINDOW_MS, OWNER_CLAIM_TTL_MS, type OwnerSeat } from './owner-seat.js';
|
|
7
|
+
export { mintOwnerClaimLink, ownerClaimPath, sha256Hex } from './owner-claim-link.js';
|
|
8
|
+
export { AUTH_CONFIG_KEY, AuthConfigError, authChoice, instanceAuthFor, parseAuthChoice, selectAuthProvider, type AuthChoice, type InstanceAuth, } from './instance-auth.js';
|
|
6
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrG,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrG,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC9F,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EACL,eAAe,EACf,eAAe,EACf,UAAU,EACV,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,KAAK,UAAU,EACf,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,7 @@ export { oidcAuthProvider } from './oidc.js';
|
|
|
2
2
|
export { oidcRpAuthProvider } from './oidc-rp-provider.js';
|
|
3
3
|
export { IdentityDO, doAuthProvider } from './identity-do.js';
|
|
4
4
|
export { resolveCookieDomain } from './cookie-domain.js';
|
|
5
|
+
export { FIRST_SIGN_IN_WINDOW_MS, OWNER_CLAIM_TTL_MS } from './owner-seat.js';
|
|
6
|
+
export { mintOwnerClaimLink, ownerClaimPath, sha256Hex } from './owner-claim-link.js';
|
|
7
|
+
export { AUTH_CONFIG_KEY, AuthConfigError, authChoice, instanceAuthFor, parseAuthChoice, selectAuthProvider, } from './instance-auth.js';
|
|
5
8
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAmB,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAqB,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,cAAc,EAAyC,MAAM,kBAAkB,CAAC;AACrG,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAmB,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAqB,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,cAAc,EAAyC,MAAM,kBAAkB,CAAC;AACrG,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAkB,MAAM,iBAAiB,CAAC;AAC9F,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EACL,eAAe,EACf,eAAe,EACf,UAAU,EACV,eAAe,EACf,eAAe,EACf,kBAAkB,GAGnB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z, type EnvVarSpec } from '@substrat-run/contracts';
|
|
2
|
+
import type { IdentityStub } from './identity-do.js';
|
|
3
|
+
import type { AuthProvider } from './provider.js';
|
|
4
|
+
/**
|
|
5
|
+
* ONE instance's auth wiring — the composition every OIDC-only vertical was writing
|
|
6
|
+
* for itself (#972).
|
|
7
|
+
*
|
|
8
|
+
* Four demos carried a near-identical forty-line copy of "read the delivered config,
|
|
9
|
+
* parse `substrat:auth`, resolve the declared settings, pick a provider", and they had
|
|
10
|
+
* already drifted: manyfold's copy skipped `resolveScopedEnvSpec` entirely and read
|
|
11
|
+
* `env.OIDC_ISSUER`, so a per-instance issuer saved in the dashboard never reached it
|
|
12
|
+
* (the silent-defaults bug, #374/#398); meridian's fail-closed check on an unset issuer
|
|
13
|
+
* existed only in meridian. A rule that lives in four places is four chances to be
|
|
14
|
+
* wrong about it once. It lives here now.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The scope's DELIVERED auth choice (vertical-auth-detach.md §2.2/§2.3) — the
|
|
18
|
+
* `substrat:auth` entry the dashboard configured at install or in Settings.
|
|
19
|
+
*
|
|
20
|
+
* OIDC-only (oidc-only-demos.md): `oidc` is the only supported mode, so a `builtin` or
|
|
21
|
+
* malformed entry fails this parse and reads as "nothing delivered". That leniency is
|
|
22
|
+
* deliberate — a bad delivery falls back to the deployment default rather than locking
|
|
23
|
+
* an instance out of its own login.
|
|
24
|
+
*/
|
|
25
|
+
export declare const authChoice: z.ZodObject<{
|
|
26
|
+
mode: z.ZodLiteral<"oidc">;
|
|
27
|
+
issuer: z.ZodOptional<z.ZodString>;
|
|
28
|
+
clientId: z.ZodOptional<z.ZodString>;
|
|
29
|
+
clientSecret: z.ZodOptional<z.ZodString>;
|
|
30
|
+
audience: z.ZodOptional<z.ZodString>;
|
|
31
|
+
cookieDomain: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export type AuthChoice = z.infer<typeof authChoice>;
|
|
34
|
+
/** The delivered-config key the dashboard writes the identity choice to. */
|
|
35
|
+
export declare const AUTH_CONFIG_KEY = "substrat:auth";
|
|
36
|
+
/**
|
|
37
|
+
* An instance that cannot be given a provider, with the status to answer.
|
|
38
|
+
*
|
|
39
|
+
* A plain `Error` would arrive at a vertical's error envelope as a 500, which is the
|
|
40
|
+
* wrong answer for "nobody has configured this yet" — so the status travels with the
|
|
41
|
+
* throw and each worker re-raises it in its own framework's exception.
|
|
42
|
+
*/
|
|
43
|
+
export declare class AuthConfigError extends Error {
|
|
44
|
+
readonly status: 500 | 503;
|
|
45
|
+
constructor(status: 500 | 503, message: string);
|
|
46
|
+
}
|
|
47
|
+
export interface InstanceAuth {
|
|
48
|
+
/** The parsed `substrat:auth` choice, or null when nothing usable was delivered. */
|
|
49
|
+
identity: AuthChoice | null;
|
|
50
|
+
/** The tenant's DO-minted session-signing secret. */
|
|
51
|
+
sessionSecret: string;
|
|
52
|
+
/** The declared settings, resolved delivered > binding > manifest default (#398). */
|
|
53
|
+
settings: Record<string, string | undefined>;
|
|
54
|
+
/** The whole delivered map — for a vertical's own non-declared keys. */
|
|
55
|
+
config: Record<string, string>;
|
|
56
|
+
/**
|
|
57
|
+
* The `AuthProvider` this instance's configuration selects. A function, not a field:
|
|
58
|
+
* a route that only wants `settings` must not fail because nobody has configured a
|
|
59
|
+
* login yet. Throws `AuthConfigError` when there is nothing to select.
|
|
60
|
+
*/
|
|
61
|
+
provider(): AuthProvider;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Everything an instance was configured with, in ONE DO hop.
|
|
65
|
+
*
|
|
66
|
+
* `settings` comes from `resolveScopedEnvSpec` over the SAME delivered map the auth
|
|
67
|
+
* choice rides in — no second round-trip, and per-scope Env-tab value > worker binding >
|
|
68
|
+
* manifest default. Reading `env` directly instead is the bug this replaces: a spec
|
|
69
|
+
* `default` rides as a binding shared by every install of one serving script, so
|
|
70
|
+
* `env.OIDC_ISSUER` is the same string for every tenant no matter what any of them saved.
|
|
71
|
+
*/
|
|
72
|
+
export declare function instanceAuthFor(opts: {
|
|
73
|
+
/** The tenant's identity DO stub. */
|
|
74
|
+
directory: Pick<IdentityStub, 'authWiring'>;
|
|
75
|
+
scopeId: string;
|
|
76
|
+
/** The vertical's declared env spec (`CALLOUT_ENV`, `TICKET0_ENV`, …). */
|
|
77
|
+
envSpec: EnvVarSpec[];
|
|
78
|
+
/** The worker's own environment — the binding layer under the delivered map. */
|
|
79
|
+
env: Record<string, unknown>;
|
|
80
|
+
}): Promise<InstanceAuth>;
|
|
81
|
+
/** The delivered `substrat:auth` entry, parsed leniently: anything unusable is null. */
|
|
82
|
+
export declare function parseAuthChoice(raw: string | undefined): AuthChoice | null;
|
|
83
|
+
/**
|
|
84
|
+
* The provider an instance's configuration selects — the whole point of the contract.
|
|
85
|
+
*
|
|
86
|
+
* Per-SCOPE first (hosted): a delivered `substrat:auth` builds the full relying-party
|
|
87
|
+
* provider (browser login at the issuer, cookie sessions signed with the tenant's
|
|
88
|
+
* DO-minted secret, bearer fallback for API clients) — one script, many issuers.
|
|
89
|
+
* Absent that, the DEPLOYMENT default: `AUTH_PROVIDER=oidc` verifies bearer tokens
|
|
90
|
+
* against a fixed issuer (standalone deploys). Anything else is unconfigured — fail
|
|
91
|
+
* closed. The app never learns which; it only ever holds an `AuthProvider`.
|
|
92
|
+
*
|
|
93
|
+
* `AUTH_PROVIDER=oidc` with no `OIDC_ISSUER` is a 500, not a 503: the deployment asked
|
|
94
|
+
* for a provider it did not finish configuring, which is an operator's mistake, not a
|
|
95
|
+
* tenant's missing choice.
|
|
96
|
+
*/
|
|
97
|
+
export declare function selectAuthProvider(opts: {
|
|
98
|
+
identity: AuthChoice | null;
|
|
99
|
+
sessionSecret: string;
|
|
100
|
+
settings: Record<string, string | undefined>;
|
|
101
|
+
}): AuthProvider;
|
|
102
|
+
//# sourceMappingURL=instance-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-auth.d.ts","sourceRoot":"","sources":["../src/instance-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,CAAC,EAAE,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAGnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD;;;;;;;;;;;GAWG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU;;;;;;;iBAQrB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,4EAA4E;AAC5E,eAAO,MAAM,eAAe,kBAAkB,CAAC;AAE/C;;;;;;GAMG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IAEtC,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG;IAD5B,YACW,MAAM,EAAE,GAAG,GAAG,GAAG,EAC1B,OAAO,EAAE,MAAM,EAIhB;CACF;AAED,MAAM,WAAW,YAAY;IAC3B,oFAAoF;IACpF,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5B,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,IAAI,YAAY,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,qCAAqC;IACrC,SAAS,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B,GAAG,OAAO,CAAC,YAAY,CAAC,CAWxB;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,IAAI,CAQ1E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC9C,GAAG,YAAY,CA+Bf"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { resolveScopedEnvSpec, z } from '@substrat-run/contracts';
|
|
2
|
+
import { oidcAuthProvider } from './oidc.js';
|
|
3
|
+
import { oidcRpAuthProvider } from './oidc-rp-provider.js';
|
|
4
|
+
/**
|
|
5
|
+
* ONE instance's auth wiring — the composition every OIDC-only vertical was writing
|
|
6
|
+
* for itself (#972).
|
|
7
|
+
*
|
|
8
|
+
* Four demos carried a near-identical forty-line copy of "read the delivered config,
|
|
9
|
+
* parse `substrat:auth`, resolve the declared settings, pick a provider", and they had
|
|
10
|
+
* already drifted: manyfold's copy skipped `resolveScopedEnvSpec` entirely and read
|
|
11
|
+
* `env.OIDC_ISSUER`, so a per-instance issuer saved in the dashboard never reached it
|
|
12
|
+
* (the silent-defaults bug, #374/#398); meridian's fail-closed check on an unset issuer
|
|
13
|
+
* existed only in meridian. A rule that lives in four places is four chances to be
|
|
14
|
+
* wrong about it once. It lives here now.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The scope's DELIVERED auth choice (vertical-auth-detach.md §2.2/§2.3) — the
|
|
18
|
+
* `substrat:auth` entry the dashboard configured at install or in Settings.
|
|
19
|
+
*
|
|
20
|
+
* OIDC-only (oidc-only-demos.md): `oidc` is the only supported mode, so a `builtin` or
|
|
21
|
+
* malformed entry fails this parse and reads as "nothing delivered". That leniency is
|
|
22
|
+
* deliberate — a bad delivery falls back to the deployment default rather than locking
|
|
23
|
+
* an instance out of its own login.
|
|
24
|
+
*/
|
|
25
|
+
export const authChoice = z.object({
|
|
26
|
+
mode: z.literal('oidc'),
|
|
27
|
+
issuer: z.string().url().optional(),
|
|
28
|
+
clientId: z.string().min(1).optional(),
|
|
29
|
+
clientSecret: z.string().optional(),
|
|
30
|
+
audience: z.string().optional(),
|
|
31
|
+
/** Share the login across every surface under this parent domain (K-26 multi-surface). */
|
|
32
|
+
cookieDomain: z.string().min(1).optional(),
|
|
33
|
+
});
|
|
34
|
+
/** The delivered-config key the dashboard writes the identity choice to. */
|
|
35
|
+
export const AUTH_CONFIG_KEY = 'substrat:auth';
|
|
36
|
+
/**
|
|
37
|
+
* An instance that cannot be given a provider, with the status to answer.
|
|
38
|
+
*
|
|
39
|
+
* A plain `Error` would arrive at a vertical's error envelope as a 500, which is the
|
|
40
|
+
* wrong answer for "nobody has configured this yet" — so the status travels with the
|
|
41
|
+
* throw and each worker re-raises it in its own framework's exception.
|
|
42
|
+
*/
|
|
43
|
+
export class AuthConfigError extends Error {
|
|
44
|
+
status;
|
|
45
|
+
constructor(status, message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.status = status;
|
|
48
|
+
this.name = 'AuthConfigError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Everything an instance was configured with, in ONE DO hop.
|
|
53
|
+
*
|
|
54
|
+
* `settings` comes from `resolveScopedEnvSpec` over the SAME delivered map the auth
|
|
55
|
+
* choice rides in — no second round-trip, and per-scope Env-tab value > worker binding >
|
|
56
|
+
* manifest default. Reading `env` directly instead is the bug this replaces: a spec
|
|
57
|
+
* `default` rides as a binding shared by every install of one serving script, so
|
|
58
|
+
* `env.OIDC_ISSUER` is the same string for every tenant no matter what any of them saved.
|
|
59
|
+
*/
|
|
60
|
+
export async function instanceAuthFor(opts) {
|
|
61
|
+
const wiring = await opts.directory.authWiring(opts.scopeId);
|
|
62
|
+
const identity = parseAuthChoice(wiring.config[AUTH_CONFIG_KEY]);
|
|
63
|
+
const settings = resolveScopedEnvSpec(opts.envSpec, opts.env, wiring.config).values;
|
|
64
|
+
return {
|
|
65
|
+
identity,
|
|
66
|
+
sessionSecret: wiring.sessionSecret,
|
|
67
|
+
settings,
|
|
68
|
+
config: wiring.config,
|
|
69
|
+
provider: () => selectAuthProvider({ identity, sessionSecret: wiring.sessionSecret, settings }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** The delivered `substrat:auth` entry, parsed leniently: anything unusable is null. */
|
|
73
|
+
export function parseAuthChoice(raw) {
|
|
74
|
+
if (!raw)
|
|
75
|
+
return null;
|
|
76
|
+
try {
|
|
77
|
+
const parsed = authChoice.safeParse(JSON.parse(raw));
|
|
78
|
+
return parsed.success ? parsed.data : null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The provider an instance's configuration selects — the whole point of the contract.
|
|
86
|
+
*
|
|
87
|
+
* Per-SCOPE first (hosted): a delivered `substrat:auth` builds the full relying-party
|
|
88
|
+
* provider (browser login at the issuer, cookie sessions signed with the tenant's
|
|
89
|
+
* DO-minted secret, bearer fallback for API clients) — one script, many issuers.
|
|
90
|
+
* Absent that, the DEPLOYMENT default: `AUTH_PROVIDER=oidc` verifies bearer tokens
|
|
91
|
+
* against a fixed issuer (standalone deploys). Anything else is unconfigured — fail
|
|
92
|
+
* closed. The app never learns which; it only ever holds an `AuthProvider`.
|
|
93
|
+
*
|
|
94
|
+
* `AUTH_PROVIDER=oidc` with no `OIDC_ISSUER` is a 500, not a 503: the deployment asked
|
|
95
|
+
* for a provider it did not finish configuring, which is an operator's mistake, not a
|
|
96
|
+
* tenant's missing choice.
|
|
97
|
+
*/
|
|
98
|
+
export function selectAuthProvider(opts) {
|
|
99
|
+
const { identity, settings } = opts;
|
|
100
|
+
if (identity?.mode === 'oidc') {
|
|
101
|
+
if (!identity.issuer || !identity.clientId) {
|
|
102
|
+
throw new AuthConfigError(503, "this instance's OIDC configuration is incomplete — set issuer and clientId");
|
|
103
|
+
}
|
|
104
|
+
return oidcRpAuthProvider({
|
|
105
|
+
issuer: identity.issuer,
|
|
106
|
+
clientId: identity.clientId,
|
|
107
|
+
clientSecret: identity.clientSecret ?? '',
|
|
108
|
+
sessionSecret: opts.sessionSecret,
|
|
109
|
+
...(identity.audience ? { audience: identity.audience } : {}),
|
|
110
|
+
...(identity.cookieDomain ? { cookieDomain: identity.cookieDomain } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (settings.AUTH_PROVIDER === 'oidc') {
|
|
114
|
+
if (!settings.OIDC_ISSUER) {
|
|
115
|
+
throw new AuthConfigError(500, 'AUTH_PROVIDER=oidc but OIDC_ISSUER is unset');
|
|
116
|
+
}
|
|
117
|
+
return oidcAuthProvider({
|
|
118
|
+
issuer: settings.OIDC_ISSUER,
|
|
119
|
+
...(settings.OIDC_AUDIENCE ? { audience: settings.OIDC_AUDIENCE } : {}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
throw new AuthConfigError(503, "this instance has no identity provider configured — deliver substrat:auth with mode 'oidc', or set AUTH_PROVIDER=oidc and OIDC_ISSUER");
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=instance-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-auth.js","sourceRoot":"","sources":["../src/instance-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The claim-link half of the owner seat (#925), from the worker's side: mint a token,
|
|
3
|
+
* hand its HASH to the identity directory, and return the URL the installer opens. Written
|
|
4
|
+
* once here so each vertical's `mintOwnerClaim` hook is a one-liner, and so the token shape,
|
|
5
|
+
* the hash and the URL convention (`/?claim=<token>`, the SPA's counterpart to `?invite=`)
|
|
6
|
+
* are one fact rather than four copies.
|
|
7
|
+
*/
|
|
8
|
+
/** SHA-256 hex (Web Crypto — the same call in workerd, node and browsers). */
|
|
9
|
+
export declare function sha256Hex(s: string): Promise<string>;
|
|
10
|
+
/** A long, URL-safe token: two UUIDs = 256 bits of entropy. Only its hash is ever stored. */
|
|
11
|
+
export declare function claimToken(): string;
|
|
12
|
+
/** The path a claim token rides on — what the SPA reads back as `?claim=`. */
|
|
13
|
+
export declare function ownerClaimPath(token: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Mint a claim link for a scope's unclaimed owner seat. `origin` is the instance's public
|
|
16
|
+
* origin — supplied by the platform, which owns the hostname directory; a `/internal` call
|
|
17
|
+
* reaches the worker through the dispatcher and carries no usable host of its own. Null ⇒
|
|
18
|
+
* the seat is already claimed (or unknown), and there is nothing to mint for.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mintOwnerClaimLink(directory: {
|
|
21
|
+
mintOwnerClaim(scopeId: string, tokenHash: string): Promise<{
|
|
22
|
+
expiresAt: string;
|
|
23
|
+
} | null>;
|
|
24
|
+
}, scopeId: string, origin: string): Promise<{
|
|
25
|
+
claimUrl: string;
|
|
26
|
+
expiresAt: string;
|
|
27
|
+
} | null>;
|
|
28
|
+
//# sourceMappingURL=owner-claim-link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-claim-link.d.ts","sourceRoot":"","sources":["../src/owner-claim-link.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,8EAA8E;AAC9E,wBAAsB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG1D;AAED,6FAA6F;AAC7F,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE;IAAE,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;CAAE,EACxG,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAKzD"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The claim-link half of the owner seat (#925), from the worker's side: mint a token,
|
|
3
|
+
* hand its HASH to the identity directory, and return the URL the installer opens. Written
|
|
4
|
+
* once here so each vertical's `mintOwnerClaim` hook is a one-liner, and so the token shape,
|
|
5
|
+
* the hash and the URL convention (`/?claim=<token>`, the SPA's counterpart to `?invite=`)
|
|
6
|
+
* are one fact rather than four copies.
|
|
7
|
+
*/
|
|
8
|
+
/** SHA-256 hex (Web Crypto — the same call in workerd, node and browsers). */
|
|
9
|
+
export async function sha256Hex(s) {
|
|
10
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
|
|
11
|
+
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
12
|
+
}
|
|
13
|
+
/** A long, URL-safe token: two UUIDs = 256 bits of entropy. Only its hash is ever stored. */
|
|
14
|
+
export function claimToken() {
|
|
15
|
+
return (crypto.randomUUID() + crypto.randomUUID()).replace(/-/g, '');
|
|
16
|
+
}
|
|
17
|
+
/** The path a claim token rides on — what the SPA reads back as `?claim=`. */
|
|
18
|
+
export function ownerClaimPath(token) {
|
|
19
|
+
return `/?claim=${encodeURIComponent(token)}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Mint a claim link for a scope's unclaimed owner seat. `origin` is the instance's public
|
|
23
|
+
* origin — supplied by the platform, which owns the hostname directory; a `/internal` call
|
|
24
|
+
* reaches the worker through the dispatcher and carries no usable host of its own. Null ⇒
|
|
25
|
+
* the seat is already claimed (or unknown), and there is nothing to mint for.
|
|
26
|
+
*/
|
|
27
|
+
export async function mintOwnerClaimLink(directory, scopeId, origin) {
|
|
28
|
+
const token = claimToken();
|
|
29
|
+
const minted = await directory.mintOwnerClaim(scopeId, await sha256Hex(token));
|
|
30
|
+
if (!minted)
|
|
31
|
+
return null;
|
|
32
|
+
return { claimUrl: `${origin.replace(/\/$/, '')}${ownerClaimPath(token)}`, expiresAt: minted.expiresAt };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=owner-claim-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-claim-link.js","sourceRoot":"","sources":["../src/owner-claim-link.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,CAAS;IACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,OAAO,CAAC,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACvF,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,UAAU;IACxB,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACvE,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,WAAW,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAAwG,EACxG,OAAe,EACf,MAAc;IAEd,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/E,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC;AAC3G,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OWNER SEAT — how a provisioned scope gets its first human (#925).
|
|
3
|
+
*
|
|
4
|
+
* At provision the platform mints a principal for the installer and hands it to the
|
|
5
|
+
* vertical as `owner`. It cannot hand over the login: the vertical authenticates at
|
|
6
|
+
* whatever issuer the tenant bound, and the platform does not know what `sub` that
|
|
7
|
+
* issuer will emit for this person. So the seat is minted EMPTY and bound later, by a
|
|
8
|
+
* verified subject arriving to claim it. Three ways in, and the first two are what
|
|
9
|
+
* closes the window this used to leave open:
|
|
10
|
+
*
|
|
11
|
+
* 1. **First sign-in, inside a window.** The install flow is "provision, then the
|
|
12
|
+
* installer opens the app and signs in" — seconds apart. That trust-on-first-use
|
|
13
|
+
* claim stays, but only for `FIRST_SIGN_IN_WINDOW_MS` after provision. Before, it
|
|
14
|
+
* was unbounded in time and audience: a CI-deployed instance whose issuer had open
|
|
15
|
+
* sign-up was a seat anyone could take, indefinitely, and nothing said so.
|
|
16
|
+
* 2. **A claim link.** After the window (or instead of it), the platform asks the
|
|
17
|
+
* vertical for a short-lived claim token under the platform secret, and the
|
|
18
|
+
* dashboard hands the installer the link. Only the token's HASH is stored here;
|
|
19
|
+
* the token rides one HTTP exchange and is never persisted anywhere.
|
|
20
|
+
* 3. **Reconcile never re-opens.** A re-provision (the platform's reconciliation
|
|
21
|
+
* sweep, a retry) keeps whatever window the seat already has, and a seat already
|
|
22
|
+
* claimed is left claimed — before, `INSERT OR REPLACE` re-minted the pending seat
|
|
23
|
+
* on every re-provision, so a sweep could hand a claimed desk's ownership to the
|
|
24
|
+
* next stranger to sign in.
|
|
25
|
+
*
|
|
26
|
+
* A CLOSED window is not a lost desk: `pending_owner` stays until a claim binds it, so
|
|
27
|
+
* `needsSetup` keeps reporting the truth and a claim link always works. What a closed
|
|
28
|
+
* window refuses is exactly the unbounded part — a stranger's plain sign-in.
|
|
29
|
+
*
|
|
30
|
+
* Plain functions over a minimal SQLite `exec` seam (the same shape as site-registry.ts)
|
|
31
|
+
* so the rules are unit-tested against a real SQLite without standing up a Durable Object.
|
|
32
|
+
* The IdentityDO's owner-seat methods delegate here.
|
|
33
|
+
*/
|
|
34
|
+
import type { RegistrySql } from './site-registry.js';
|
|
35
|
+
/** How long after provision a plain first sign-in still claims the seat. */
|
|
36
|
+
export declare const FIRST_SIGN_IN_WINDOW_MS: number;
|
|
37
|
+
/** How long a minted claim link stays valid. */
|
|
38
|
+
export declare const OWNER_CLAIM_TTL_MS: number;
|
|
39
|
+
/**
|
|
40
|
+
* The tables. `identity` is the provider-agnostic directory (a verified `sub` → the
|
|
41
|
+
* PrincipalId it maps to, per scope — K-22, the same login is a different principal in
|
|
42
|
+
* each scope); it is also written by invites, which is why it lives here beside the seat
|
|
43
|
+
* rather than in the DO alone: claiming IS writing it.
|
|
44
|
+
*/
|
|
45
|
+
export declare const OWNER_SEAT_DDL: readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* Bring a `pending_owner` table from before `claim_until` up to date. `CREATE TABLE IF NOT
|
|
48
|
+
* EXISTS` leaves an existing table alone, so a DO whose storage predates the column needs the
|
|
49
|
+
* one `ALTER`. Idempotent — run it after the DDL on every construction.
|
|
50
|
+
*/
|
|
51
|
+
export declare function migrateOwnerSeat(sql: RegistrySql): void;
|
|
52
|
+
/** What the platform (and the vertical's own first-run screen) can see of the seat. */
|
|
53
|
+
export interface OwnerSeat {
|
|
54
|
+
/** `unknown` ⇒ this scope was never provisioned through this directory. */
|
|
55
|
+
state: 'claimed' | 'unclaimed' | 'unknown';
|
|
56
|
+
/** The owner of record — the principal the seat binds to. Null when unknown. */
|
|
57
|
+
owner: string | null;
|
|
58
|
+
/** While unclaimed: whether a plain first sign-in still claims it, and until when (ISO). */
|
|
59
|
+
firstSignIn: {
|
|
60
|
+
open: boolean;
|
|
61
|
+
until: string | null;
|
|
62
|
+
} | null;
|
|
63
|
+
/** While unclaimed: the outstanding claim link's expiry (ISO), or null when none is live. */
|
|
64
|
+
claimLink: {
|
|
65
|
+
expiresAt: string;
|
|
66
|
+
} | null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Record the seat at provision — both the transient `pending_owner` and the durable
|
|
70
|
+
* `owner_of_record`. The FIRST write wins: a scope that already has an owner of record is
|
|
71
|
+
* left exactly as it is, whatever principal a re-run names. The platform re-runs provision
|
|
72
|
+
* on reconcile and retry with the owner it minted, so a same-owner re-run changes nothing
|
|
73
|
+
* either way; a DIFFERENT owner reaching a seat that is already recorded would otherwise
|
|
74
|
+
* re-point it — and re-open a claimed one for a stranger — which is the hole this closes.
|
|
75
|
+
* A pending seat keeps the window it was given rather than getting a fresh one per re-run.
|
|
76
|
+
*/
|
|
77
|
+
export declare function recordOwnerSeat(sql: RegistrySql, scopeId: string, principal: string, now: number, windowMs?: number): void;
|
|
78
|
+
/** The scope's durable owner of record, or null if never provisioned through this directory. */
|
|
79
|
+
export declare function ownerOfRecord(sql: RegistrySql, scopeId: string): string | null;
|
|
80
|
+
/** Is the seat unclaimed? True whatever the window says — a closed window is still an empty seat. */
|
|
81
|
+
export declare function needsSetup(sql: RegistrySql, scopeId: string): boolean;
|
|
82
|
+
/** The seat as the platform sees it. */
|
|
83
|
+
export declare function ownerSeat(sql: RegistrySql, scopeId: string, now: number): OwnerSeat;
|
|
84
|
+
/**
|
|
85
|
+
* Map a verified subject to a principal in this scope. Bound ⇒ that principal. Unbound with
|
|
86
|
+
* the seat pending AND the first-sign-in window open ⇒ claim it. Otherwise null — a valid
|
|
87
|
+
* login with no seat has no access, and a closed window is "no seat" for THIS path (the
|
|
88
|
+
* claim link is the other). Provider-agnostic: the subject may come from Better Auth or an
|
|
89
|
+
* OIDC issuer.
|
|
90
|
+
*/
|
|
91
|
+
export declare function resolvePrincipal(sql: RegistrySql, scopeId: string, sub: string, now: number): string | null;
|
|
92
|
+
/**
|
|
93
|
+
* Mint a claim link for a pending seat: store the token's hash with an expiry, replacing any
|
|
94
|
+
* earlier link (so minting again is also how one is revoked). Null ⇒ the seat is not pending
|
|
95
|
+
* — already claimed, or never provisioned here — and there is nothing to mint for.
|
|
96
|
+
*/
|
|
97
|
+
export declare function mintOwnerClaim(sql: RegistrySql, scopeId: string, tokenHash: string, now: number, ttlMs?: number): {
|
|
98
|
+
expiresAt: string;
|
|
99
|
+
} | null;
|
|
100
|
+
/**
|
|
101
|
+
* Claim the seat by link: the presented token's hash must match the live claim, and the seat
|
|
102
|
+
* must still be pending. Binds the subject, consumes the seat and the link. Null ⇒ invalid,
|
|
103
|
+
* expired, already used, or nothing to claim — one answer, so a probe learns nothing.
|
|
104
|
+
*/
|
|
105
|
+
export declare function claimOwner(sql: RegistrySql, scopeId: string, sub: string, tokenHash: string, now: number): string | null;
|
|
106
|
+
//# sourceMappingURL=owner-seat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-seat.d.ts","sourceRoot":"","sources":["../src/owner-seat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,4EAA4E;AAC5E,eAAO,MAAM,uBAAuB,QAAc,CAAC;AACnD,gDAAgD;AAChD,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAE9C;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAa3C,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAKvD;AAED,uFAAuF;AACvF,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,KAAK,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;IAC3C,gFAAgF;IAChF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,4FAA4F;IAC5F,WAAW,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5D,6FAA6F;IAC7F,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACzC;AAID;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAgC,GACzC,IAAI,CASN;AAED,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK9E;AAED,qGAAqG;AACrG,wBAAgB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE;AAuBD,wCAAwC;AACxC,wBAAgB,SAAS,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAenF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS3G;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,GAAE,MAA2B,GACjC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAU9B;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMxH"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OWNER SEAT — how a provisioned scope gets its first human (#925).
|
|
3
|
+
*
|
|
4
|
+
* At provision the platform mints a principal for the installer and hands it to the
|
|
5
|
+
* vertical as `owner`. It cannot hand over the login: the vertical authenticates at
|
|
6
|
+
* whatever issuer the tenant bound, and the platform does not know what `sub` that
|
|
7
|
+
* issuer will emit for this person. So the seat is minted EMPTY and bound later, by a
|
|
8
|
+
* verified subject arriving to claim it. Three ways in, and the first two are what
|
|
9
|
+
* closes the window this used to leave open:
|
|
10
|
+
*
|
|
11
|
+
* 1. **First sign-in, inside a window.** The install flow is "provision, then the
|
|
12
|
+
* installer opens the app and signs in" — seconds apart. That trust-on-first-use
|
|
13
|
+
* claim stays, but only for `FIRST_SIGN_IN_WINDOW_MS` after provision. Before, it
|
|
14
|
+
* was unbounded in time and audience: a CI-deployed instance whose issuer had open
|
|
15
|
+
* sign-up was a seat anyone could take, indefinitely, and nothing said so.
|
|
16
|
+
* 2. **A claim link.** After the window (or instead of it), the platform asks the
|
|
17
|
+
* vertical for a short-lived claim token under the platform secret, and the
|
|
18
|
+
* dashboard hands the installer the link. Only the token's HASH is stored here;
|
|
19
|
+
* the token rides one HTTP exchange and is never persisted anywhere.
|
|
20
|
+
* 3. **Reconcile never re-opens.** A re-provision (the platform's reconciliation
|
|
21
|
+
* sweep, a retry) keeps whatever window the seat already has, and a seat already
|
|
22
|
+
* claimed is left claimed — before, `INSERT OR REPLACE` re-minted the pending seat
|
|
23
|
+
* on every re-provision, so a sweep could hand a claimed desk's ownership to the
|
|
24
|
+
* next stranger to sign in.
|
|
25
|
+
*
|
|
26
|
+
* A CLOSED window is not a lost desk: `pending_owner` stays until a claim binds it, so
|
|
27
|
+
* `needsSetup` keeps reporting the truth and a claim link always works. What a closed
|
|
28
|
+
* window refuses is exactly the unbounded part — a stranger's plain sign-in.
|
|
29
|
+
*
|
|
30
|
+
* Plain functions over a minimal SQLite `exec` seam (the same shape as site-registry.ts)
|
|
31
|
+
* so the rules are unit-tested against a real SQLite without standing up a Durable Object.
|
|
32
|
+
* The IdentityDO's owner-seat methods delegate here.
|
|
33
|
+
*/
|
|
34
|
+
/** How long after provision a plain first sign-in still claims the seat. */
|
|
35
|
+
export const FIRST_SIGN_IN_WINDOW_MS = 15 * 60_000;
|
|
36
|
+
/** How long a minted claim link stays valid. */
|
|
37
|
+
export const OWNER_CLAIM_TTL_MS = 15 * 60_000;
|
|
38
|
+
/**
|
|
39
|
+
* The tables. `identity` is the provider-agnostic directory (a verified `sub` → the
|
|
40
|
+
* PrincipalId it maps to, per scope — K-22, the same login is a different principal in
|
|
41
|
+
* each scope); it is also written by invites, which is why it lives here beside the seat
|
|
42
|
+
* rather than in the DO alone: claiming IS writing it.
|
|
43
|
+
*/
|
|
44
|
+
export const OWNER_SEAT_DDL = [
|
|
45
|
+
`CREATE TABLE IF NOT EXISTS identity (scope_id TEXT NOT NULL, sub TEXT NOT NULL, principal TEXT NOT NULL, PRIMARY KEY (scope_id, sub))`,
|
|
46
|
+
// The owner seat waiting to be claimed: set at provision, consumed by the claim. `claim_until`
|
|
47
|
+
// bounds the plain first-sign-in path (ms epoch); NULL — a row from before the column existed —
|
|
48
|
+
// reads as CLOSED, since a seat that sat unclaimed across an upgrade is exactly the case.
|
|
49
|
+
`CREATE TABLE IF NOT EXISTS pending_owner (scope_id TEXT PRIMARY KEY, principal TEXT NOT NULL, claim_until INTEGER)`,
|
|
50
|
+
// The DURABLE owner of record: also set at provision, but NEVER consumed (#332). `pending_owner`
|
|
51
|
+
// is gone the moment the owner claims, so it can't answer "who owns this scope" after that.
|
|
52
|
+
// This can — it survives a scope-DO storage wipe, and the reconcile path re-grants from it.
|
|
53
|
+
`CREATE TABLE IF NOT EXISTS owner_of_record (scope_id TEXT PRIMARY KEY, principal TEXT NOT NULL)`,
|
|
54
|
+
// One outstanding claim link per scope — the hash of its token and when it stops working.
|
|
55
|
+
// Minting again replaces it, so a leaked link is retired by minting a fresh one.
|
|
56
|
+
`CREATE TABLE IF NOT EXISTS owner_claim (scope_id TEXT PRIMARY KEY, token_hash TEXT NOT NULL, expires_at INTEGER NOT NULL)`,
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Bring a `pending_owner` table from before `claim_until` up to date. `CREATE TABLE IF NOT
|
|
60
|
+
* EXISTS` leaves an existing table alone, so a DO whose storage predates the column needs the
|
|
61
|
+
* one `ALTER`. Idempotent — run it after the DDL on every construction.
|
|
62
|
+
*/
|
|
63
|
+
export function migrateOwnerSeat(sql) {
|
|
64
|
+
const columns = [...sql.exec('PRAGMA table_info(pending_owner)')].map((r) => r.name);
|
|
65
|
+
if (!columns.includes('claim_until')) {
|
|
66
|
+
sql.exec('ALTER TABLE pending_owner ADD COLUMN claim_until INTEGER');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
70
|
+
/**
|
|
71
|
+
* Record the seat at provision — both the transient `pending_owner` and the durable
|
|
72
|
+
* `owner_of_record`. The FIRST write wins: a scope that already has an owner of record is
|
|
73
|
+
* left exactly as it is, whatever principal a re-run names. The platform re-runs provision
|
|
74
|
+
* on reconcile and retry with the owner it minted, so a same-owner re-run changes nothing
|
|
75
|
+
* either way; a DIFFERENT owner reaching a seat that is already recorded would otherwise
|
|
76
|
+
* re-point it — and re-open a claimed one for a stranger — which is the hole this closes.
|
|
77
|
+
* A pending seat keeps the window it was given rather than getting a fresh one per re-run.
|
|
78
|
+
*/
|
|
79
|
+
export function recordOwnerSeat(sql, scopeId, principal, now, windowMs = FIRST_SIGN_IN_WINDOW_MS) {
|
|
80
|
+
if (ownerOfRecord(sql, scopeId) !== null)
|
|
81
|
+
return;
|
|
82
|
+
sql.exec('INSERT INTO owner_of_record (scope_id, principal) VALUES (?, ?)', scopeId, principal);
|
|
83
|
+
sql.exec('INSERT INTO pending_owner (scope_id, principal, claim_until) VALUES (?, ?, ?)', scopeId, principal, now + windowMs);
|
|
84
|
+
}
|
|
85
|
+
/** The scope's durable owner of record, or null if never provisioned through this directory. */
|
|
86
|
+
export function ownerOfRecord(sql, scopeId) {
|
|
87
|
+
const row = [...sql.exec('SELECT principal FROM owner_of_record WHERE scope_id = ?', scopeId)][0];
|
|
88
|
+
return row?.principal ?? null;
|
|
89
|
+
}
|
|
90
|
+
/** Is the seat unclaimed? True whatever the window says — a closed window is still an empty seat. */
|
|
91
|
+
export function needsSetup(sql, scopeId) {
|
|
92
|
+
return [...sql.exec('SELECT 1 FROM pending_owner WHERE scope_id = ?', scopeId)][0] !== undefined;
|
|
93
|
+
}
|
|
94
|
+
function pendingRow(sql, scopeId) {
|
|
95
|
+
return [...sql.exec('SELECT principal, claim_until FROM pending_owner WHERE scope_id = ?', scopeId)][0];
|
|
96
|
+
}
|
|
97
|
+
function liveClaim(sql, scopeId, now) {
|
|
98
|
+
const row = [...sql.exec('SELECT token_hash, expires_at FROM owner_claim WHERE scope_id = ?', scopeId)][0];
|
|
99
|
+
return row && row.expires_at > now ? row : undefined;
|
|
100
|
+
}
|
|
101
|
+
/** Bind a subject to the pending seat and consume it — the one write every claim path shares. */
|
|
102
|
+
function bindSeat(sql, scopeId, sub, principal) {
|
|
103
|
+
sql.exec('INSERT OR REPLACE INTO identity (scope_id, sub, principal) VALUES (?, ?, ?)', scopeId, sub, principal);
|
|
104
|
+
sql.exec('DELETE FROM pending_owner WHERE scope_id = ?', scopeId);
|
|
105
|
+
sql.exec('DELETE FROM owner_claim WHERE scope_id = ?', scopeId);
|
|
106
|
+
return principal;
|
|
107
|
+
}
|
|
108
|
+
/** The seat as the platform sees it. */
|
|
109
|
+
export function ownerSeat(sql, scopeId, now) {
|
|
110
|
+
const owner = ownerOfRecord(sql, scopeId);
|
|
111
|
+
if (!owner)
|
|
112
|
+
return { state: 'unknown', owner: null, firstSignIn: null, claimLink: null };
|
|
113
|
+
const pending = pendingRow(sql, scopeId);
|
|
114
|
+
if (!pending)
|
|
115
|
+
return { state: 'claimed', owner, firstSignIn: null, claimLink: null };
|
|
116
|
+
const claim = liveClaim(sql, scopeId, now);
|
|
117
|
+
return {
|
|
118
|
+
state: 'unclaimed',
|
|
119
|
+
owner,
|
|
120
|
+
firstSignIn: {
|
|
121
|
+
open: pending.claim_until !== null && pending.claim_until > now,
|
|
122
|
+
until: pending.claim_until === null ? null : iso(pending.claim_until),
|
|
123
|
+
},
|
|
124
|
+
claimLink: claim ? { expiresAt: iso(claim.expires_at) } : null,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Map a verified subject to a principal in this scope. Bound ⇒ that principal. Unbound with
|
|
129
|
+
* the seat pending AND the first-sign-in window open ⇒ claim it. Otherwise null — a valid
|
|
130
|
+
* login with no seat has no access, and a closed window is "no seat" for THIS path (the
|
|
131
|
+
* claim link is the other). Provider-agnostic: the subject may come from Better Auth or an
|
|
132
|
+
* OIDC issuer.
|
|
133
|
+
*/
|
|
134
|
+
export function resolvePrincipal(sql, scopeId, sub, now) {
|
|
135
|
+
const bound = [...sql.exec('SELECT principal FROM identity WHERE scope_id = ? AND sub = ?', scopeId, sub)][0];
|
|
136
|
+
if (bound)
|
|
137
|
+
return bound.principal;
|
|
138
|
+
const pending = pendingRow(sql, scopeId);
|
|
139
|
+
if (!pending)
|
|
140
|
+
return null;
|
|
141
|
+
if (pending.claim_until === null || pending.claim_until <= now)
|
|
142
|
+
return null;
|
|
143
|
+
return bindSeat(sql, scopeId, sub, pending.principal);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Mint a claim link for a pending seat: store the token's hash with an expiry, replacing any
|
|
147
|
+
* earlier link (so minting again is also how one is revoked). Null ⇒ the seat is not pending
|
|
148
|
+
* — already claimed, or never provisioned here — and there is nothing to mint for.
|
|
149
|
+
*/
|
|
150
|
+
export function mintOwnerClaim(sql, scopeId, tokenHash, now, ttlMs = OWNER_CLAIM_TTL_MS) {
|
|
151
|
+
if (!pendingRow(sql, scopeId))
|
|
152
|
+
return null;
|
|
153
|
+
const expiresAt = now + ttlMs;
|
|
154
|
+
sql.exec('INSERT OR REPLACE INTO owner_claim (scope_id, token_hash, expires_at) VALUES (?, ?, ?)', scopeId, tokenHash, expiresAt);
|
|
155
|
+
return { expiresAt: iso(expiresAt) };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Claim the seat by link: the presented token's hash must match the live claim, and the seat
|
|
159
|
+
* must still be pending. Binds the subject, consumes the seat and the link. Null ⇒ invalid,
|
|
160
|
+
* expired, already used, or nothing to claim — one answer, so a probe learns nothing.
|
|
161
|
+
*/
|
|
162
|
+
export function claimOwner(sql, scopeId, sub, tokenHash, now) {
|
|
163
|
+
const pending = pendingRow(sql, scopeId);
|
|
164
|
+
if (!pending)
|
|
165
|
+
return null;
|
|
166
|
+
const claim = liveClaim(sql, scopeId, now);
|
|
167
|
+
if (!claim || claim.token_hash !== tokenHash)
|
|
168
|
+
return null;
|
|
169
|
+
return bindSeat(sql, scopeId, sub, pending.principal);
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=owner-seat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-seat.js","sourceRoot":"","sources":["../src/owner-seat.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@substrat-run/vertical-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Pluggable auth for Substrat verticals — the AuthProvider contract, an OIDC provider (Supabase/Auth0/AuthHero/Keycloak), a full relying-party login flow, and a per-tenant identity Durable Object (Better Auth + sub→principal directory). Workers-targeted; the vertical worker bundles it.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"drizzle-orm": "^0.45.2",
|
|
38
38
|
"jose": "^6.2.3",
|
|
39
39
|
"@substrat-run/oidc-rp": "^0.5.1",
|
|
40
|
-
"@substrat-run/
|
|
40
|
+
"@substrat-run/contracts": "^0.94.0",
|
|
41
|
+
"@substrat-run/psl": "^0.2.4"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@cloudflare/workers-types": "^4.20250109.0",
|