@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 +1 -1
- package/src/store/d1.ts +33 -0
- package/src/store/sqlite/consent-grants-store.ts +9 -0
- package/src/store/sqlite/op-store.ts +19 -0
- package/src/store/sqlite.ts +12 -0
- package/src/store.ts +19 -0
- package/src/vocab/permissions.ts +84 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oimlsmart/platform-server",
|
|
3
|
-
"version": "0.2.
|
|
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 {
|
package/src/store/sqlite.ts
CHANGED
|
@@ -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).
|
package/src/vocab/permissions.ts
CHANGED
|
@@ -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
|
+
}
|