@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.4",
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