@oimlsmart/platform-server 0.2.8 → 0.2.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimlsmart/platform-server",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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
@@ -1456,6 +1456,16 @@ export class D1ServerStore implements ServerStore {
1456
1456
  return (res.meta.changes ?? 0) > 0
1457
1457
  }
1458
1458
 
1459
+ /** The governance view's population read: the client's LIVE access-token
1460
+ * count (unexpired — the row's absence IS the revocation). */
1461
+ async countOidcAccessTokensForClient(clientId: string): Promise<number> {
1462
+ await this.ensureMembershipSupport()
1463
+ const row = await this.stmt(
1464
+ "SELECT COUNT(*) AS n FROM oidc_access_tokens WHERE client_id = ? AND datetime(expires_at) > datetime('now')", clientId,
1465
+ ).first<{ n: number }>()
1466
+ return row?.n ?? 0
1467
+ }
1468
+
1459
1469
  private static toRefreshToken(row: Record<string, unknown>): OidcRefreshToken {
1460
1470
  return {
1461
1471
  token: row.token as string,
@@ -1546,6 +1556,18 @@ export class D1ServerStore implements ServerStore {
1546
1556
  return res.meta.changes ?? 0
1547
1557
  }
1548
1558
 
1559
+ /** The governance view's population read: the client's LIVE refresh-token
1560
+ * count (unconsumed AND unexpired — a revoked family is deleted
1561
+ * wholesale, so a live row's presence is the offline grant's standing). */
1562
+ async countOidcRefreshTokensForClient(clientId: string): Promise<number> {
1563
+ await this.ensureMembershipSupport()
1564
+ await this.ensureOidcRefreshTokenSupport()
1565
+ const row = await this.stmt(
1566
+ "SELECT COUNT(*) AS n FROM oidc_refresh_tokens WHERE client_id = ? AND consumed_at IS NULL AND datetime(expires_at) > datetime('now')", clientId,
1567
+ ).first<{ n: number }>()
1568
+ return row?.n ?? 0
1569
+ }
1570
+
1549
1571
  async listOidcKeys(): Promise<OidcKeyRow[]> {
1550
1572
  const res = await this.stmt('SELECT * FROM oidc_keys ORDER BY created_at, kid').all<Record<string, unknown>>()
1551
1573
  return res.results.map(row => ({
@@ -1639,6 +1661,17 @@ export class D1ServerStore implements ServerStore {
1639
1661
  return (res.meta.changes ?? 0) > 0
1640
1662
  }
1641
1663
 
1664
+ /** The client-registry governance view's per-client read: EVERY grant row
1665
+ * the client holds — live AND revoked, newest first. */
1666
+ async listOidcConsentGrantsForClient(clientId: string): Promise<OidcConsentGrant[]> {
1667
+ await this.ensureConsentGrantSupport()
1668
+ const res = await this.stmt(
1669
+ 'SELECT * FROM oidc_consent_grants WHERE client_id = ? ORDER BY created_at DESC, rowid DESC',
1670
+ clientId,
1671
+ ).all<Record<string, unknown>>()
1672
+ return res.results.map(D1ServerStore.toConsentGrant)
1673
+ }
1674
+
1642
1675
  // ── the upstream providers (TODO.identity/08) ─────────────────────
1643
1676
  // The provider + link rows port directly (D1 is SQLite).
1644
1677
 
@@ -77,6 +77,15 @@ export function listConsentGrants(userId: string): OidcConsentGrant[] {
77
77
  return rows.map(toConsentGrant)
78
78
  }
79
79
 
80
+ /** The client-registry governance view's per-client read: EVERY grant row
81
+ * the client holds — live AND revoked, newest first. */
82
+ export function listOidcConsentGrantsForClient(clientId: string): OidcConsentGrant[] {
83
+ const rows = getDb().prepare(
84
+ 'SELECT * FROM oidc_consent_grants WHERE client_id = ? ORDER BY created_at DESC, rowid DESC',
85
+ ).all(clientId) as Array<Record<string, unknown>>
86
+ return rows.map(toConsentGrant)
87
+ }
88
+
80
89
  /** The guarded revoke: the owner's LIVE row flips, once. */
81
90
  export function revokeConsentGrant(id: string, userId: string): boolean {
82
91
  return getDb().prepare(
@@ -275,6 +275,15 @@ export function deleteOidcAccessToken(token: string, clientId: string): boolean
275
275
  return res.changes > 0
276
276
  }
277
277
 
278
+ /** The governance view's population read: the client's LIVE access-token
279
+ * count (unexpired — the row's absence IS the revocation). */
280
+ export function countOidcAccessTokensForClient(clientId: string): number {
281
+ const row = getDb().prepare(
282
+ "SELECT COUNT(*) AS n FROM oidc_access_tokens WHERE client_id = ? AND datetime(expires_at) > datetime('now')",
283
+ ).get(clientId) as { n: number }
284
+ return row.n
285
+ }
286
+
278
287
  function toOidcRefreshToken(row: Record<string, unknown>): OidcRefreshToken {
279
288
  return {
280
289
  token: row.token as string,
@@ -354,6 +363,16 @@ export function deleteOidcRefreshTokensForUserClient(userId: string, clientId: s
354
363
  return getDb().prepare('DELETE FROM oidc_refresh_tokens WHERE user_id = ? AND client_id = ?').run(userId, clientId).changes
355
364
  }
356
365
 
366
+ /** The governance view's population read: the client's LIVE refresh-token
367
+ * count (unconsumed AND unexpired — a revoked family is deleted wholesale,
368
+ * so a live row's presence is the offline grant's standing). */
369
+ export function countOidcRefreshTokensForClient(clientId: string): number {
370
+ const row = getDb().prepare(
371
+ "SELECT COUNT(*) AS n FROM oidc_refresh_tokens WHERE client_id = ? AND consumed_at IS NULL AND datetime(expires_at) > datetime('now')",
372
+ ).get(clientId) as { n: number }
373
+ return row.n
374
+ }
375
+
357
376
  /** The amr column's honest parse (a JSON array of strings, else null —
358
377
  * the provenance is absent on rows that predate the wave). */
359
378
  function parseJsonStringList(raw: unknown): string[] | null {
@@ -120,6 +120,8 @@ import {
120
120
  import {
121
121
  consumeOidcCode,
122
122
  consumeOidcRefreshToken,
123
+ countOidcAccessTokensForClient,
124
+ countOidcRefreshTokensForClient,
123
125
  createOidcAccessToken,
124
126
  createOidcAuthorization,
125
127
  createOidcCode,
@@ -223,6 +225,7 @@ import {
223
225
  import {
224
226
  getConsentGrant,
225
227
  listConsentGrants,
228
+ listOidcConsentGrantsForClient,
226
229
  recordConsentGrant,
227
230
  revokeConsentGrant,
228
231
  } from './sqlite/consent-grants-store'
@@ -672,6 +675,9 @@ export function createSqliteServerStore(): ServerStore {
672
675
  async deleteOidcAccessToken(token: string, clientId: string): Promise<boolean> {
673
676
  return deleteOidcAccessToken(token, clientId)
674
677
  },
678
+ async countOidcAccessTokensForClient(clientId: string): Promise<number> {
679
+ return countOidcAccessTokensForClient(clientId)
680
+ },
675
681
  async createOidcRefreshToken(input: {
676
682
  token: string
677
683
  userId: string
@@ -694,6 +700,9 @@ export function createSqliteServerStore(): ServerStore {
694
700
  async deleteOidcRefreshTokensForUserClient(userId: string, clientId: string): Promise<number> {
695
701
  return deleteOidcRefreshTokensForUserClient(userId, clientId)
696
702
  },
703
+ async countOidcRefreshTokensForClient(clientId: string): Promise<number> {
704
+ return countOidcRefreshTokensForClient(clientId)
705
+ },
697
706
  async listOidcKeys(): Promise<OidcKeyRow[]> {
698
707
  return listOidcKeys()
699
708
  },
@@ -717,6 +726,9 @@ export function createSqliteServerStore(): ServerStore {
717
726
  async revokeConsentGrant(id: string, userId: string): Promise<boolean> {
718
727
  return revokeConsentGrant(id, userId)
719
728
  },
729
+ async listOidcConsentGrantsForClient(clientId: string): Promise<OidcConsentGrant[]> {
730
+ return listOidcConsentGrantsForClient(clientId)
731
+ },
720
732
 
721
733
  // ── the upstream providers (TODO.identity/08) ──
722
734
  async listIdentityProviders(): Promise<IdentityProvider[]> {
package/src/store.ts CHANGED
@@ -1684,6 +1684,12 @@ export interface ServerStore {
1684
1684
  * client answers false). An absent row answers false too (the endpoint
1685
1685
  * masks both behind its 200). */
1686
1686
  deleteOidcAccessToken(token: string, clientId: string): Promise<boolean>
1687
+ /** The client-registry governance view's population read: the count of
1688
+ * the client's LIVE access tokens (unexpired — the row's absence IS the
1689
+ * revocation, so presence + liveness is standing). A count, never the
1690
+ * rows: the governance console answers "how many sessions stand behind
1691
+ * this client", never a token value. */
1692
+ countOidcAccessTokensForClient(clientId: string): Promise<number>
1687
1693
  /** The refresh tokens (migration 0025 — the SSO wave-C token surface).
1688
1694
  * The mint: token is the opaque value the route generated, familyId the
1689
1695
  * rotation lineage (a fresh id at the code exchange's first mint, the
@@ -1717,6 +1723,12 @@ export interface ServerStore {
1717
1723
  * (account, client) pair goes — the "Revoke access" act ends the
1718
1724
  * offline half with the remembered consent. Answers the count. */
1719
1725
  deleteOidcRefreshTokensForUserClient(userId: string, clientId: string): Promise<number>
1726
+ /** The client-registry governance view's population read: the count of
1727
+ * the client's LIVE refresh tokens (unconsumed AND unexpired — a
1728
+ * revoked family is DELETED wholesale, so a live row's presence is the
1729
+ * grant's standing). A count, never the rows: the console answers "how
1730
+ * many offline grants stand", never a token value. */
1731
+ countOidcRefreshTokensForClient(clientId: string): Promise<number>
1720
1732
  /** The key rotation history (public halves). */
1721
1733
  listOidcKeys(): Promise<OidcKeyRow[]>
1722
1734
  upsertOidcKey(input: { kid: string; publicJwk: string }): Promise<void>
@@ -1743,6 +1755,13 @@ export interface ServerStore {
1743
1755
  * live row — a second revoke or another account's row answers false
1744
1756
  * (the PAT guard's posture). The row STAYS. */
1745
1757
  revokeConsentGrant(id: string, userId: string): Promise<boolean>
1758
+ /** The client-registry governance view's per-client read: EVERY grant
1759
+ * row the client holds — live AND revoked (the account console's
1760
+ * listConsentGrants hides the revoked half; the governance view shows
1761
+ * the history the audit chain would otherwise carry alone), newest
1762
+ * first. Read-only: the acts stay on the account console and the
1763
+ * dashboard's revoke routes. */
1764
+ listOidcConsentGrantsForClient(clientId: string): Promise<OidcConsentGrant[]>
1746
1765
 
1747
1766
  // ── the upstream providers (TODO.identity/08) ──
1748
1767
  /** The upstream registry (admin-managed; OP_UPSTREAM_SEED bootstraps).
@@ -428,3 +428,87 @@ export const CREATION_PERMISSIONS: Record<string, readonly ActionPermission[]> =
428
428
  export const FIELD_GUARD_PERMISSIONS: Record<string, Record<string, readonly ActionPermission[]>> = {
429
429
  testReports: { signature_acknowledgment: ['tr.sign'] },
430
430
  }
431
+
432
+ // ── The store-wide write gate (the 2026-09-07 security cone audit's
433
+ // F2) ────────────────────────────────────────────────────────────
434
+ // ORG_FIELDS (store.ts) cones the org-fielded workflow stores and
435
+ // STORE_MACHINES + CREATION/FIELD_GUARD gate the machinated ones — but
436
+ // both leave holes the audit named: a store with NO org fields and NO
437
+ // machine answered a generic PUT/DELETE from ANY authenticated role
438
+ // (viewer included), and a machinated store gated only its status moves
439
+ // — a field update without a status change, or a creation the CREATION
440
+ // map does not name, computed NO requirement at all. The worst
441
+ // amplification: organizations carries the submission_public_keys the
442
+ // intake's signature check trusts.
443
+ //
444
+ // THIS map is the write side's store-class declaration for the stores
445
+ // the org cone cannot carry: EVERY generic write to a listed store
446
+ // (create, in-place update, delete — status change or not) requires the
447
+ // actor to hold ANY of the named permissions, on top of every other
448
+ // gate that fires (the machine's transitions, the creation acts, the
449
+ // field guards). The consumer's entities router composes the classes:
450
+ // org-fielded → the org cone; catalog → the catalog write gate;
451
+ // machinated → the machine's edges; listed here → this gate; NONE of
452
+ // those → the generic write refuses outright (deny-by-default). The
453
+ // gate is uniform over roles: the platform roles pass because their map
454
+ // grants the permission, never because the gate looks away.
455
+
456
+ /** The store-wide write requirements (store → any-of permission set).
457
+ * The assignments follow the app's actual write callers, audited
458
+ * 2026-09-07:
459
+ *
460
+ * - the participants register (PD-03/PD-04/PD-08/PD-09): the Executive
461
+ * Secretary administers (participants.manage), the RC assesses
462
+ * (participants.review), the MC votes (participants.decide);
463
+ * - the scheme-operations records (B 18 §7 — appeals, complaints,
464
+ * misuse, deregistrations) are the operations console's;
465
+ * - the §15.8 registered copies record at the registration act (the
466
+ * IA's local register-with-BIML fallback holds certificate.register)
467
+ * and at the operations console's corrections;
468
+ * - the ANR validation letters record at issuance (the issuing
469
+ * officer's certificate.issue) and at the operations console;
470
+ * - the ANR declarations: the Utilizer/Associate staffer declares
471
+ * (anr.declare), the registry moderates (anr.review) — the DRAFT's
472
+ * in-place edits ride the declarer's permission;
473
+ * - the instrument register's parties (organizations, manufacturers)
474
+ * are instance-settings/registry records — organizations in
475
+ * particular carries submission_public_keys, the intake signature
476
+ * trust root, so its writes are the instance's own administration;
477
+ * the manufacturer org rows are created at application intake (the
478
+ * wizard's quick-create, the IA desk's white-gloves entry);
479
+ * - the per-model determination records are the evaluation desk's
480
+ * (er.review / the TR review that stamps them);
481
+ * - the lab's work records (form instances, verification records, the
482
+ * module-B markings/sealings/calibration records) ride their work
483
+ * permissions; the IA's review desks write form instances on the
484
+ * review/reopen legs;
485
+ * - the certificate's document lists and annexes are the issuers'. */
486
+ export const STORE_WRITE_PERMISSIONS: Record<string, readonly ActionPermission[]> = {
487
+ organizations: ['instance.settings'],
488
+ manufacturers: ['application.submit', 'application.review', 'instance.settings'],
489
+ experts: ['participants.manage'],
490
+ utilizers: ['participants.manage'],
491
+ associates: ['participants.manage'],
492
+ categorySchemes: ['participants.manage'],
493
+ approvalVotes: ['participants.decide'],
494
+ competenceEvidence: ['participants.review', 'participants.manage'],
495
+ participantApplications: ['participants.review', 'participants.manage'],
496
+ participantDeclarations: ['participants.manage'],
497
+ operatedSchemes: ['instance.settings'],
498
+ certificateDocumentLists: ['certificate.issue'],
499
+ certificateAnnexes: ['certificate.issue', 'certificate.manage'],
500
+ registeredCopies: ['certificate.register', 'operations.manage'],
501
+ validationLetters: ['certificate.issue', 'operations.manage'],
502
+ deregistrations: ['operations.manage'],
503
+ appeals: ['operations.manage'],
504
+ complaints: ['operations.manage'],
505
+ misuseCases: ['operations.manage'],
506
+ anrDeclarations: ['anr.declare', 'anr.review'],
507
+ testReportDeterminations: ['tr.review', 'er.review'],
508
+ formInstances: ['run.perform', 'tr.review', 'er.review'],
509
+ verificationRecords: ['verification.perform'],
510
+ certificateRegistrations: ['certificate.register'],
511
+ markings: ['markings.manage'],
512
+ sealings: ['markings.manage'],
513
+ calibrationRecords: ['markings.manage'],
514
+ }