@oimlsmart/platform-server 0.2.4 → 0.2.5
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.
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Migration 0024 — the SSO wave-A tail (TODO.identity-sso: RP-initiated
|
|
2
|
+
-- logout + prompt=login): the one-time code carries the consenting
|
|
3
|
+
-- session's authentication instant (sessions.created_at, verbatim), so
|
|
4
|
+
-- the token endpoint emits the ID token's auth_time — the forced
|
|
5
|
+
-- re-authentication's freshness proof the RP verifies. NULL = no
|
|
6
|
+
-- instant recorded (a code minted before this wave).
|
|
7
|
+
-- schema.sql carries the same end state for fresh databases —
|
|
8
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
9
|
+
-- schema.sql's CREATE set.
|
|
10
|
+
ALTER TABLE oidc_codes ADD COLUMN auth_time TEXT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oimlsmart/platform-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "The OIML SMART platform server kernel: the store seam (ServerStore + the D1 and SQLite implementations), the canonical D1 migration set both deployments apply, the instance profile, the mailer, the RBAC map, the OIDC/OAuth client cones, and the shared role/permission vocabulary. Consumed by the smart monorepo (browser/) and the identity service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/store/d1.ts
CHANGED
|
@@ -640,6 +640,11 @@ export class D1ServerStore implements ServerStore {
|
|
|
640
640
|
if (!codeCols.results.some(c => c.name === 'amr')) {
|
|
641
641
|
await this.db.prepare('ALTER TABLE oidc_codes ADD COLUMN amr TEXT').run()
|
|
642
642
|
}
|
|
643
|
+
// TODO.identity-sso (the wave-A tail): the code carries the
|
|
644
|
+
// consenting session's authentication instant (migration 0024).
|
|
645
|
+
if (!codeCols.results.some(c => c.name === 'auth_time')) {
|
|
646
|
+
await this.db.prepare('ALTER TABLE oidc_codes ADD COLUMN auth_time TEXT').run()
|
|
647
|
+
}
|
|
643
648
|
const tokenCols = await this.db.prepare('PRAGMA table_info(oidc_access_tokens)').all<{ name: string }>()
|
|
644
649
|
if (!tokenCols.results.some(c => c.name === 'amr')) {
|
|
645
650
|
await this.db.prepare('ALTER TABLE oidc_access_tokens ADD COLUMN amr TEXT').run()
|
|
@@ -862,11 +867,11 @@ export class D1ServerStore implements ServerStore {
|
|
|
862
867
|
// reassignment takes effect on the next request; deactivation ends
|
|
863
868
|
// the session at once.
|
|
864
869
|
const session = await this.stmt(
|
|
865
|
-
`SELECT s.user_id, s.active_org, s.amr, u.email, u.name, u.role, u.roles, u.org_id, u.avatar_url, u.provider, u.email_verified_at
|
|
870
|
+
`SELECT s.user_id, s.active_org, s.amr, s.created_at, u.email, u.name, u.role, u.roles, u.org_id, u.avatar_url, u.provider, u.email_verified_at
|
|
866
871
|
FROM sessions s JOIN users u ON s.user_id = u.id
|
|
867
872
|
WHERE s.token = ? AND s.expires_at > datetime('now') AND u.active = 1`,
|
|
868
873
|
token,
|
|
869
|
-
).first<{ user_id: string; active_org: string | null; amr: string | null; email: string; name: string; role: string; roles: string | null; org_id: string | null; avatar_url: string | null; provider: string; email_verified_at: string | null }>()
|
|
874
|
+
).first<{ user_id: string; active_org: string | null; amr: string | null; created_at: string; email: string; name: string; role: string; roles: string | null; org_id: string | null; avatar_url: string | null; provider: string; email_verified_at: string | null }>()
|
|
870
875
|
if (!session) return null
|
|
871
876
|
const amr = parseRoles(session.amr)
|
|
872
877
|
const payload: AuthUserPayload = {
|
|
@@ -880,6 +885,9 @@ export class D1ServerStore implements ServerStore {
|
|
|
880
885
|
provider: session.provider,
|
|
881
886
|
emailVerifiedAt: session.email_verified_at ?? null,
|
|
882
887
|
...(amr?.length ? { amr } : {}),
|
|
888
|
+
// TODO.identity-sso (the wave-A tail): the authentication instant —
|
|
889
|
+
// the ID token's auth_time derives from it (the consumer converts).
|
|
890
|
+
sessionCreatedAt: session.created_at,
|
|
883
891
|
}
|
|
884
892
|
// TODO.identity/11: the active-org context (the membership model) —
|
|
885
893
|
// the payload's org/roles follow the session's stamped context. The
|
|
@@ -1295,16 +1303,20 @@ export class D1ServerStore implements ServerStore {
|
|
|
1295
1303
|
* (stored as JSON; the token endpoint emits it as the ID token's
|
|
1296
1304
|
* amr). Absent = no provenance recorded. */
|
|
1297
1305
|
amr?: string[] | null
|
|
1306
|
+
/** TODO.identity-sso (the wave-A tail): the consenting session's
|
|
1307
|
+
* authentication instant (verbatim; absent = none recorded) — the
|
|
1308
|
+
* token endpoint emits it as the ID token's auth_time. */
|
|
1309
|
+
authTime?: string | null
|
|
1298
1310
|
ttlMs: number
|
|
1299
1311
|
}): Promise<void> {
|
|
1300
1312
|
await this.ensureMembershipSupport()
|
|
1301
1313
|
const expiresAt = new Date(Date.now() + input.ttlMs).toISOString()
|
|
1302
1314
|
await this.ensureOidcColumns()
|
|
1303
1315
|
await this.stmt(
|
|
1304
|
-
`INSERT INTO oidc_codes (code, client_id, redirect_uri, scope, nonce, code_challenge, user_id, context_org, amr, expires_at)
|
|
1305
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1316
|
+
`INSERT INTO oidc_codes (code, client_id, redirect_uri, scope, nonce, code_challenge, user_id, context_org, amr, auth_time, expires_at)
|
|
1317
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1306
1318
|
input.code, input.clientId, input.redirectUri, input.scope, input.nonce, input.codeChallenge, input.userId, input.contextOrg ?? null,
|
|
1307
|
-
input.amr?.length ? JSON.stringify(input.amr) : null, expiresAt,
|
|
1319
|
+
input.amr?.length ? JSON.stringify(input.amr) : null, input.authTime ?? null, expiresAt,
|
|
1308
1320
|
).run()
|
|
1309
1321
|
}
|
|
1310
1322
|
|
|
@@ -1330,6 +1342,7 @@ export class D1ServerStore implements ServerStore {
|
|
|
1330
1342
|
userId: row.user_id as string,
|
|
1331
1343
|
contextOrg: (row.context_org as string | null) ?? null,
|
|
1332
1344
|
amr: parseRoles((row.amr as string | null) ?? null) ?? null,
|
|
1345
|
+
authTime: (row.auth_time as string | null) ?? null,
|
|
1333
1346
|
expiresAt: row.expires_at as string,
|
|
1334
1347
|
}
|
|
1335
1348
|
}
|
|
@@ -174,14 +174,18 @@ export function createOidcCode(input: {
|
|
|
174
174
|
* (stored as JSON; the token endpoint emits it as the ID token's
|
|
175
175
|
* amr). Absent = no provenance recorded. */
|
|
176
176
|
amr?: string[] | null
|
|
177
|
+
/** TODO.identity-sso (the wave-A tail): the consenting session's
|
|
178
|
+
* authentication instant (verbatim; absent = none recorded) — the
|
|
179
|
+
* token endpoint emits it as the ID token's auth_time. */
|
|
180
|
+
authTime?: string | null
|
|
177
181
|
ttlMs: number
|
|
178
182
|
}): void {
|
|
179
183
|
const expiresAt = new Date(Date.now() + input.ttlMs).toISOString()
|
|
180
184
|
getDb().prepare(`
|
|
181
|
-
INSERT INTO oidc_codes (code, client_id, redirect_uri, scope, nonce, code_challenge, user_id, context_org, amr, expires_at)
|
|
182
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
185
|
+
INSERT INTO oidc_codes (code, client_id, redirect_uri, scope, nonce, code_challenge, user_id, context_org, amr, auth_time, expires_at)
|
|
186
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
183
187
|
`).run(input.code, input.clientId, input.redirectUri, input.scope, input.nonce, input.codeChallenge, input.userId,
|
|
184
|
-
input.contextOrg ?? null, input.amr?.length ? JSON.stringify(input.amr) : null, expiresAt)
|
|
188
|
+
input.contextOrg ?? null, input.amr?.length ? JSON.stringify(input.amr) : null, input.authTime ?? null, expiresAt)
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
/** Atomically consume the code: the UPDATE flips consumed_at exactly
|
|
@@ -204,6 +208,7 @@ export function consumeOidcCode(code: string): OidcCode | null {
|
|
|
204
208
|
userId: row.user_id as string,
|
|
205
209
|
contextOrg: (row.context_org as string | null) ?? null,
|
|
206
210
|
amr: parseJsonStringList(row.amr),
|
|
211
|
+
authTime: (row.auth_time as string | null) ?? null,
|
|
207
212
|
expiresAt: row.expires_at as string,
|
|
208
213
|
}
|
|
209
214
|
}
|
|
@@ -339,6 +339,10 @@ CREATE TABLE IF NOT EXISTS oidc_codes (
|
|
|
339
339
|
-- TODO.identity-sso/02+03: the consenting session's amr provenance (a
|
|
340
340
|
-- JSON array; NULL = none recorded), carried into the ID token.
|
|
341
341
|
amr TEXT,
|
|
342
|
+
-- TODO.identity-sso (the wave-A tail): the consenting session's
|
|
343
|
+
-- authentication instant (sessions.created_at, verbatim; NULL = none
|
|
344
|
+
-- recorded), carried into the ID token's auth_time.
|
|
345
|
+
auth_time TEXT,
|
|
342
346
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
343
347
|
expires_at TEXT NOT NULL,
|
|
344
348
|
consumed_at TEXT
|
|
@@ -323,6 +323,10 @@ export function getSessionUser(token: string): AuthUserPayload | null {
|
|
|
323
323
|
).run(token)
|
|
324
324
|
// s.* carries the SESSION's id — the payload's id is the USER's.
|
|
325
325
|
const payload = toAuthPayload({ ...session, id: session.user_id })
|
|
326
|
+
// TODO.identity-sso (the wave-A tail): the session's authentication
|
|
327
|
+
// instant (sessions.created_at, verbatim) — the ID token's auth_time
|
|
328
|
+
// derives from it (the consumer converts to the OIDC NumericDate).
|
|
329
|
+
payload.sessionCreatedAt = session.created_at as string
|
|
326
330
|
// TODO.identity/11: the active-org context (the membership model) —
|
|
327
331
|
// the payload's org/roles follow the session's stamped context.
|
|
328
332
|
return applySessionOrgContext(session.active_org ?? null, payload)
|
package/src/store.ts
CHANGED
|
@@ -65,6 +65,13 @@ export interface AuthUserPayload {
|
|
|
65
65
|
* read (getSessionUser) only; ABSENT = no OP-side credential event
|
|
66
66
|
* recorded (an upstream-provider sign-in, a legacy row). */
|
|
67
67
|
amr?: string[]
|
|
68
|
+
/** TODO.identity-sso (the wave-A tail): the SESSION's authentication
|
|
69
|
+
* instant (sessions.created_at, the column verbatim — the sign-in's
|
|
70
|
+
* wall-clock stamp). Projected by the session-backed read
|
|
71
|
+
* (getSessionUser) only. The ID token's auth_time derives from it (the
|
|
72
|
+
* OIDC NumericDate conversion is the consumer's — the column's storage
|
|
73
|
+
* format is the store's own). */
|
|
74
|
+
sessionCreatedAt?: string
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
// ── identity federation (TODO.federation/10) ─────────────────────────
|
|
@@ -425,6 +432,10 @@ export interface OidcCode {
|
|
|
425
432
|
/** TODO.identity-sso/02+03: the consenting session's amr provenance
|
|
426
433
|
* (parsed from the row's JSON; null = none recorded). */
|
|
427
434
|
amr: string[] | null
|
|
435
|
+
/** TODO.identity-sso (the wave-A tail): the consenting session's
|
|
436
|
+
* authentication instant (sessions.created_at, verbatim; null = none
|
|
437
|
+
* recorded) — the token endpoint emits it as the ID token's auth_time. */
|
|
438
|
+
authTime: string | null
|
|
428
439
|
expiresAt: string
|
|
429
440
|
}
|
|
430
441
|
|
|
@@ -1580,6 +1591,11 @@ export interface ServerStore {
|
|
|
1580
1591
|
* (stored as JSON; the token endpoint emits it as the ID token's
|
|
1581
1592
|
* amr). Absent = no provenance recorded. */
|
|
1582
1593
|
amr?: string[] | null
|
|
1594
|
+
/** TODO.identity-sso (the wave-A tail): the consenting session's
|
|
1595
|
+
* authentication instant (sessions.created_at, verbatim; absent =
|
|
1596
|
+
* none recorded) — the token endpoint emits it as the ID token's
|
|
1597
|
+
* auth_time. */
|
|
1598
|
+
authTime?: string | null
|
|
1583
1599
|
ttlMs: number
|
|
1584
1600
|
}): Promise<void>
|
|
1585
1601
|
/** Atomically consume the code: answers the row exactly once (a
|