@oimlsmart/platform-server 0.1.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.
Files changed (48) hide show
  1. package/README.md +92 -0
  2. package/migrations/0001_init.sql +69 -0
  3. package/migrations/0002_identity.sql +24 -0
  4. package/migrations/0003_federation_peers.sql +21 -0
  5. package/migrations/0003_users_rbac.sql +8 -0
  6. package/migrations/0004_oidc_op.sql +61 -0
  7. package/migrations/0005_upstream_providers.sql +34 -0
  8. package/migrations/0006_op_accounts.sql +28 -0
  9. package/migrations/0007_org_join_requests.sql +26 -0
  10. package/migrations/0008_op_client_roles.sql +19 -0
  11. package/migrations/0009_account_console.sql +32 -0
  12. package/migrations/0009_sso_states.sql +13 -0
  13. package/migrations/0010_notify_events.sql +23 -0
  14. package/migrations/0011_op_launch.sql +18 -0
  15. package/migrations/0011_org_memberships.sql +62 -0
  16. package/migrations/0012_notify_subscriptions.sql +57 -0
  17. package/migrations/0012_strong_auth.sql +109 -0
  18. package/migrations/0013_org_registry.sql +53 -0
  19. package/migrations/0014_notify_inbox.sql +34 -0
  20. package/migrations/0015_certificate_holder_attribution.sql +63 -0
  21. package/migrations/0016_instrument_registrations.sql +78 -0
  22. package/package.json +52 -0
  23. package/src/client-info.ts +25 -0
  24. package/src/context.ts +31 -0
  25. package/src/github.ts +284 -0
  26. package/src/mailer.ts +309 -0
  27. package/src/oidc.ts +369 -0
  28. package/src/profile/node.ts +83 -0
  29. package/src/profile.ts +582 -0
  30. package/src/rbac/node.ts +42 -0
  31. package/src/rbac.ts +53 -0
  32. package/src/session.ts +45 -0
  33. package/src/store/d1.ts +2850 -0
  34. package/src/store/sqlite/entities.ts +71 -0
  35. package/src/store/sqlite/events.ts +82 -0
  36. package/src/store/sqlite/factors-store.ts +348 -0
  37. package/src/store/sqlite/notify.ts +247 -0
  38. package/src/store/sqlite/op-accounts-store.ts +470 -0
  39. package/src/store/sqlite/op-store.ts +280 -0
  40. package/src/store/sqlite/schema.sql +745 -0
  41. package/src/store/sqlite/store.ts +1390 -0
  42. package/src/store/sqlite/upstream-store.ts +148 -0
  43. package/src/store/sqlite.ts +1027 -0
  44. package/src/store.ts +1826 -0
  45. package/src/vocab/index.ts +12 -0
  46. package/src/vocab/permissions.ts +398 -0
  47. package/src/vocab/rbac.ts +281 -0
  48. package/src/vocab/roles.ts +162 -0
@@ -0,0 +1,1027 @@
1
+ // ═══════════════════════════════════════════════════════════════════
2
+ // The SQLite ServerStore (TODO.cs-e2e/14): the node/self-hosted half
3
+ // of the backend seam — the SAME sync modules the server always used
4
+ // (store.ts + entities.ts, better-sqlite3) exposed through the async
5
+ // ServerStore contract the routes consume. Zero behavior change: every
6
+ // method delegates one-for-one.
7
+ //
8
+ // NODE-ONLY: this module imports better-sqlite3 through store.ts. The
9
+ // Worker bundle never sees it (the worker entry installs the D1 store
10
+ // instead) — the audit the wrangler config documents.
11
+ // ═══════════════════════════════════════════════════════════════════
12
+
13
+ import { fileURLToPath } from 'url'
14
+ import {
15
+ attributeCertificateHolderOrg,
16
+ authenticateDemo,
17
+ cleanExpiredSessions,
18
+ consumeSsoState,
19
+ createCertificateHolderClaim,
20
+ createLocalUser,
21
+ createOrgJoinRequest,
22
+ createOrgMembership,
23
+ createOrgRegistryOrg,
24
+ createInstrumentRegistration,
25
+ createSession,
26
+ decideCertificateHolderClaim,
27
+ decideIdentityApproval,
28
+ decideOrgJoinRequest,
29
+ deleteOrgMembership,
30
+ deleteOrgRegistryOrg,
31
+ deleteSession,
32
+ findOrCreateOAuthUser,
33
+ findPendingCertificateHolderClaim,
34
+ findPendingOrgJoinRequestByEmail,
35
+ findUserByEmail,
36
+ findUserByProvider,
37
+ getCertificateHolderClaim,
38
+ getCertificateHolderOrg,
39
+ getDb,
40
+ getFederationPeer,
41
+ getIdentityApproval,
42
+ getInstrumentRegistration,
43
+ getOrgJoinRequest,
44
+ getOrgMembership,
45
+ getOrgRegistryOrg,
46
+ getSessionActiveOrg,
47
+ getSessionIdTokenHint,
48
+ getSessionUser,
49
+ getUserById,
50
+ linkProviderIdentity,
51
+ listCertificateHolderClaims,
52
+ listCertificateHolderOrgs,
53
+ listFederationPeers,
54
+ listIdentityApprovals,
55
+ listInstrumentRegistrations,
56
+ listInstrumentRegistrationsForCertificate,
57
+ listInstrumentRegistrationsForHolder,
58
+ listOrgJoinRequests,
59
+ listOrgMembers,
60
+ listOrgMemberships,
61
+ listOrgRegistryOrgs,
62
+ listUsers,
63
+ provisionSsoUser,
64
+ putSsoState,
65
+ revokeFederationPeer,
66
+ seedDemoAccounts,
67
+ setInstrumentRegistrationLifecycle,
68
+ setOrgMembershipRoles,
69
+ setOrgMembershipState,
70
+ setOrgRegistryOrgState,
71
+ setSessionActiveOrg,
72
+ setUserActive,
73
+ setUserRoles,
74
+ touchLastLogin,
75
+ updateOrgRegistryOrg,
76
+ updateUserRoleOrg,
77
+ upsertFederationPeer,
78
+ upsertIdentityApproval,
79
+ } from './sqlite/store'
80
+ import {
81
+ changesAfter,
82
+ deleteEntity,
83
+ getEntity,
84
+ latestChangeSeq,
85
+ listEntities,
86
+ putEntity,
87
+ } from './sqlite/entities'
88
+ import {
89
+ appendEvent,
90
+ eventsAfter,
91
+ eventsMatching,
92
+ getEvent,
93
+ latestEventSeq,
94
+ } from './sqlite/events'
95
+ import {
96
+ deleteNotifyEntityMute,
97
+ deleteNotifyRule,
98
+ getNotifyPreferences,
99
+ listNotifyEntityMutes,
100
+ listNotifyInboxStates,
101
+ listNotifyRules,
102
+ notifyEntityMutesForEvent,
103
+ notifyRulesForEvent,
104
+ putNotifyEntityMute,
105
+ putNotifyInboxState,
106
+ putNotifyPreferences,
107
+ putNotifyRule,
108
+ } from './sqlite/notify'
109
+ import {
110
+ consumeOidcCode,
111
+ createOidcAccessToken,
112
+ createOidcAuthorization,
113
+ createOidcCode,
114
+ decideOidcAuthorization,
115
+ getOidcAccessToken,
116
+ getOidcAuthorization,
117
+ getOidcClient,
118
+ listOidcClients,
119
+ listOidcKeys,
120
+ retireOidcKey,
121
+ setOidcClientStatus,
122
+ setOidcClientLaunch,
123
+ upsertOidcClient,
124
+ upsertOidcKey,
125
+ } from './sqlite/op-store'
126
+ import {
127
+ createIdentityLink,
128
+ deleteIdentityLink,
129
+ deleteIdentityProvider,
130
+ findIdentityLink,
131
+ getIdentityProvider,
132
+ listIdentityLinks,
133
+ listIdentityProviders,
134
+ setIdentityProviderEnabled,
135
+ upsertIdentityProvider,
136
+ } from './sqlite/upstream-store'
137
+ import {
138
+ completeEmailChange,
139
+ completeEnrollment,
140
+ countSignInMethods,
141
+ createEmailChangeToken,
142
+ createEnrollmentToken,
143
+ createOpAccount,
144
+ deleteAllUserSessions,
145
+ deleteOpClientRoles,
146
+ deleteOtherSessions,
147
+ deletePasswordHash,
148
+ deleteSessionById,
149
+ eraseOpAccount,
150
+ getEmailChangeToken,
151
+ getEnrollmentToken,
152
+ getOpClientRoles,
153
+ getPasswordLogin,
154
+ getPendingEmailChange,
155
+ lastAccountSignIns,
156
+ listAllOpClientRoles,
157
+ listOpClientRoles,
158
+ listOpLiveSessions,
159
+ listUserSessions,
160
+ revokeOpUserCredentials,
161
+ setOpClientRoles,
162
+ setPasswordHash,
163
+ setUserAvatar,
164
+ updateOpAccount,
165
+ updateUserName,
166
+ } from './sqlite/op-accounts-store'
167
+ import { installStore, type AuthUserPayload, type CertificateHolderClaim, type CertificateHolderOrg, type CompleteEmailChangeResult, type CompleteEnrollmentResult, type EmailChangeToken, type EnrollmentToken, type EntityChange, type EntityRow, type EventKeyFilter, type FederationPeer, type IdentityApproval, type IdentityLink, type IdentityProvider, type InstrumentRegistration, type InstrumentRegistrationLifecycle, type InstrumentRegistrationScopeStatus, type NotifyEntityMute, type NotifyInboxState, type NotifyPreferences, type NotifyRule, type OAuthInitialAssignment, type OidcAccessToken, type OidcAuthorization, type OidcClient, type OidcClientLaunch, type OidcCode, type OidcKeyRow, type OpAccountErasure, type OpClientRoleAssignment, type OpLiveSession, type OrgJoinRequest, type OrgMembership, type OrgMembershipState, type OrgRegistryContact, type OrgRegistryOrg, type OrgRegistryState, type PlatformEvent, type ServerStore, type SessionView, type SsoSignInState, type UserAdminRow, type AdvanceCounterResult, type MfaPending, type RecoveryCodeState, type TotpSecret, type WebauthnChallenge, type WebauthnCredential } from '../store'
168
+ import {
169
+ advanceWebauthnCounter,
170
+ consumeMfaPending,
171
+ consumeRecoveryCode,
172
+ consumeWebauthnChallenge,
173
+ createMfaPending,
174
+ createTotpSecret,
175
+ createWebauthnChallenge,
176
+ createWebauthnCredential,
177
+ deleteTotpSecret,
178
+ deleteWebauthnCredential,
179
+ getMfaPending,
180
+ getTotpSecret,
181
+ getWebauthnCredential,
182
+ listTotpSecrets,
183
+ listWebauthnCredentials,
184
+ markTotpSecretUsed,
185
+ markTotpSecretVerified,
186
+ recordMfaPendingFailure,
187
+ recordTotpEnrollFailure,
188
+ replaceRecoveryCodes,
189
+ recoveryCodeState,
190
+ } from './sqlite/factors-store'
191
+
192
+
193
+ /** The workflow tables the reset wipe covers (the D1 store's
194
+ * WIPE_TABLES twin — one rule, two backends): ordinary rowid tables
195
+ * all, so ranged rounds address rows by a stable rowid window. The
196
+ * TODO.notify/01 event store wipes with them — its rows reference the
197
+ * workflow entities a reset removes (the feed's read-time visibility
198
+ * gate would drop the orphans anyway; wiping keeps the demo honest).
199
+ * TODO.register/03: the instrument register wipes too — the e2e
200
+ * isolation contract resets it with the rest of the mutable workflow
201
+ * state. */
202
+ const WIPE_TABLES = ['entity_changes', 'evidence_records', 'entities', 'events', 'instrument_registrations'] as const
203
+
204
+ export function createSqliteServerStore(): ServerStore {
205
+ return {
206
+ // ── users / sessions ──
207
+ async seedDemoAccounts(): Promise<void> {
208
+ seedDemoAccounts()
209
+ },
210
+ async authenticateDemo(email: string, password: string): Promise<AuthUserPayload | null> {
211
+ return authenticateDemo(email, password)
212
+ },
213
+ async findOrCreateOAuthUser(
214
+ provider: string,
215
+ providerAccountId: string,
216
+ email: string,
217
+ name: string,
218
+ avatarUrl?: string,
219
+ initial?: OAuthInitialAssignment,
220
+ ): Promise<AuthUserPayload> {
221
+ return findOrCreateOAuthUser(provider, providerAccountId, email, name, avatarUrl, initial)
222
+ },
223
+ async createSession(
224
+ userId: string,
225
+ opts?: { idTokenHint?: string | null; userAgent?: string | null; ip?: string | null; amr?: string[] | null },
226
+ ): Promise<string> {
227
+ return createSession(userId, opts)
228
+ },
229
+ async touchLastLogin(userId: string): Promise<void> {
230
+ touchLastLogin(userId)
231
+ },
232
+ async getSessionUser(token: string): Promise<AuthUserPayload | null> {
233
+ return getSessionUser(token)
234
+ },
235
+ async getSessionIdTokenHint(token: string): Promise<string | null> {
236
+ return getSessionIdTokenHint(token)
237
+ },
238
+ async deleteSession(token: string): Promise<void> {
239
+ deleteSession(token)
240
+ },
241
+ async cleanExpiredSessions(): Promise<void> {
242
+ cleanExpiredSessions()
243
+ },
244
+ async listDemoAccounts(): Promise<Array<{ email: string; name: string; role: string }>> {
245
+ return getDb()
246
+ .prepare("SELECT email, name, role FROM users WHERE provider = 'demo' ORDER BY role, name")
247
+ .all() as Array<{ email: string; name: string; role: string }>
248
+ },
249
+
250
+ // ── identity federation (TODO.federation/10) ──
251
+ async findUserByEmail(email: string): Promise<AuthUserPayload | null> {
252
+ return findUserByEmail(email)
253
+ },
254
+ async getUserById(id: string): Promise<AuthUserPayload | null> {
255
+ return getUserById(id)
256
+ },
257
+ async findUserByProvider(provider: string, providerAccountId: string): Promise<AuthUserPayload | null> {
258
+ return findUserByProvider(provider, providerAccountId)
259
+ },
260
+ async provisionSsoUser(input: {
261
+ email: string
262
+ name: string
263
+ provider: string
264
+ providerAccountId: string
265
+ role: string
266
+ orgId: string | null
267
+ }): Promise<AuthUserPayload> {
268
+ return provisionSsoUser(input)
269
+ },
270
+ async linkProviderIdentity(userId: string, provider: string, providerAccountId: string): Promise<void> {
271
+ linkProviderIdentity(userId, provider, providerAccountId)
272
+ },
273
+ async updateUserRoleOrg(userId: string, role: string, orgId: string | null): Promise<void> {
274
+ updateUserRoleOrg(userId, role, orgId)
275
+ },
276
+ async upsertIdentityApproval(input: {
277
+ email: string
278
+ name: string
279
+ issuer: string
280
+ sub: string
281
+ claimsJson: string | null
282
+ }): Promise<IdentityApproval> {
283
+ return upsertIdentityApproval(input)
284
+ },
285
+ async getIdentityApproval(issuer: string, sub: string): Promise<IdentityApproval | null> {
286
+ return getIdentityApproval(issuer, sub)
287
+ },
288
+ async listIdentityApprovals(status?: IdentityApproval['status']): Promise<IdentityApproval[]> {
289
+ return listIdentityApprovals(status)
290
+ },
291
+ async decideIdentityApproval(
292
+ id: string,
293
+ decision: { status: 'approved' | 'rejected'; role?: string; orgId?: string | null; decidedBy: string },
294
+ ): Promise<IdentityApproval | null> {
295
+ return decideIdentityApproval(id, decision)
296
+ },
297
+
298
+ // ── the SSO sign-in state jar (TODO.identity/04) ──
299
+ async putSsoState(input: { state: string; nonce: string; verifier: string; ttlMs: number }): Promise<void> {
300
+ putSsoState(input)
301
+ },
302
+ async consumeSsoState(state: string): Promise<SsoSignInState | null> {
303
+ return consumeSsoState(state)
304
+ },
305
+
306
+ // ── federation peers (TODO.federation/04) ──
307
+ async listFederationPeers(status?: FederationPeer['status']): Promise<FederationPeer[]> {
308
+ return listFederationPeers(status)
309
+ },
310
+ async getFederationPeer(id: string): Promise<FederationPeer | null> {
311
+ return getFederationPeer(id)
312
+ },
313
+ async upsertFederationPeer(input: {
314
+ id: string
315
+ name: string
316
+ roles: string
317
+ descriptorUrl: string | null
318
+ descriptorJson: string
319
+ pinnedVia: FederationPeer['pinnedVia']
320
+ connectivity: FederationPeer['connectivity']
321
+ addedBy: string | null
322
+ }): Promise<FederationPeer> {
323
+ return upsertFederationPeer(input)
324
+ },
325
+ async revokeFederationPeer(id: string, revokedBy: string): Promise<FederationPeer | null> {
326
+ return revokeFederationPeer(id, revokedBy)
327
+ },
328
+
329
+ // ── user administration (TODO.federation/12) ──
330
+ async listUsers(): Promise<UserAdminRow[]> {
331
+ return listUsers()
332
+ },
333
+ async createLocalUser(input: {
334
+ email: string
335
+ name: string
336
+ role: string
337
+ roles?: string[]
338
+ orgId?: string | null
339
+ }): Promise<UserAdminRow> {
340
+ return createLocalUser(input)
341
+ },
342
+ async setUserRoles(id: string, role: string, roles: string[]): Promise<boolean> {
343
+ return setUserRoles(id, role, roles)
344
+ },
345
+ async setUserActive(id: string, active: boolean): Promise<boolean> {
346
+ return setUserActive(id, active)
347
+ },
348
+
349
+ // ── organization administration (TODO.identity/10) ──
350
+ async createOrgJoinRequest(input: {
351
+ name: string
352
+ email: string
353
+ orgId: string | null
354
+ orgNameText: string | null
355
+ requestedRole: string
356
+ note?: string | null
357
+ }): Promise<OrgJoinRequest> {
358
+ return createOrgJoinRequest(input)
359
+ },
360
+ async getOrgJoinRequest(id: string): Promise<OrgJoinRequest | null> {
361
+ return getOrgJoinRequest(id)
362
+ },
363
+ async listOrgJoinRequests(filter?: {
364
+ scope?: 'org' | 'unregistered' | 'all'
365
+ orgId?: string
366
+ status?: OrgJoinRequest['status']
367
+ }): Promise<OrgJoinRequest[]> {
368
+ return listOrgJoinRequests(filter)
369
+ },
370
+ async decideOrgJoinRequest(
371
+ id: string,
372
+ decision: {
373
+ status: 'approved' | 'refused'
374
+ decidedBy: string
375
+ refusalReason?: string | null
376
+ invitedUserId?: string | null
377
+ },
378
+ ): Promise<OrgJoinRequest | null> {
379
+ return decideOrgJoinRequest(id, decision)
380
+ },
381
+ async findPendingOrgJoinRequestByEmail(email: string): Promise<OrgJoinRequest | null> {
382
+ return findPendingOrgJoinRequestByEmail(email)
383
+ },
384
+
385
+ // ── organization memberships (TODO.identity/11) ──
386
+ async listOrgMemberships(userId: string): Promise<OrgMembership[]> {
387
+ return listOrgMemberships(userId)
388
+ },
389
+ async listOrgMembers(orgId: string): Promise<OrgMembership[]> {
390
+ return listOrgMembers(orgId)
391
+ },
392
+ async getOrgMembership(userId: string, orgId: string): Promise<OrgMembership | null> {
393
+ return getOrgMembership(userId, orgId)
394
+ },
395
+ async createOrgMembership(input: {
396
+ userId: string
397
+ orgId: string
398
+ roles: string[]
399
+ state: OrgMembershipState
400
+ invitedBy?: string | null
401
+ }): Promise<OrgMembership | null> {
402
+ return createOrgMembership(input)
403
+ },
404
+ async setOrgMembershipRoles(userId: string, orgId: string, roles: string[]): Promise<boolean> {
405
+ return setOrgMembershipRoles(userId, orgId, roles)
406
+ },
407
+ async setOrgMembershipState(
408
+ userId: string,
409
+ orgId: string,
410
+ state: OrgMembershipState,
411
+ actor?: string | null,
412
+ ): Promise<OrgMembership | null> {
413
+ return setOrgMembershipState(userId, orgId, state, actor)
414
+ },
415
+ async deleteOrgMembership(userId: string, orgId: string): Promise<boolean> {
416
+ return deleteOrgMembership(userId, orgId)
417
+ },
418
+ async getSessionActiveOrg(token: string): Promise<string | null> {
419
+ return getSessionActiveOrg(token)
420
+ },
421
+ async setSessionActiveOrg(token: string, orgId: string | null): Promise<boolean> {
422
+ return setSessionActiveOrg(token, orgId)
423
+ },
424
+
425
+ // ── the organization registry (TODO.identity-features/05) ──
426
+ async listOrgRegistryOrgs(): Promise<OrgRegistryOrg[]> {
427
+ return listOrgRegistryOrgs()
428
+ },
429
+ async getOrgRegistryOrg(id: string): Promise<OrgRegistryOrg | null> {
430
+ return getOrgRegistryOrg(id)
431
+ },
432
+ async createOrgRegistryOrg(input: {
433
+ id: string
434
+ name: string
435
+ shortName?: string | null
436
+ kind?: string | null
437
+ country?: string | null
438
+ contacts?: OrgRegistryContact[]
439
+ participantRef?: string | null
440
+ createdBy?: string | null
441
+ }): Promise<OrgRegistryOrg | null> {
442
+ return createOrgRegistryOrg(input)
443
+ },
444
+ async updateOrgRegistryOrg(
445
+ id: string,
446
+ patch: {
447
+ name?: string
448
+ shortName?: string | null
449
+ kind?: string | null
450
+ country?: string | null
451
+ contacts?: OrgRegistryContact[]
452
+ participantRef?: string | null
453
+ },
454
+ actor?: string | null,
455
+ ): Promise<OrgRegistryOrg | null> {
456
+ return updateOrgRegistryOrg(id, patch, actor)
457
+ },
458
+ async setOrgRegistryOrgState(id: string, state: OrgRegistryState, actor?: string | null): Promise<OrgRegistryOrg | null> {
459
+ return setOrgRegistryOrgState(id, state, actor)
460
+ },
461
+ async deleteOrgRegistryOrg(id: string): Promise<boolean> {
462
+ return deleteOrgRegistryOrg(id)
463
+ },
464
+
465
+ // ── the register's holder-org attribution (TODO.register/02) ──
466
+ async attributeCertificateHolderOrg(input: {
467
+ certificateId: string
468
+ orgId: string
469
+ orgName: string
470
+ source: CertificateHolderOrg['source']
471
+ attributedAt: string
472
+ attributedBy?: string | null
473
+ claimId?: string | null
474
+ }): Promise<CertificateHolderOrg | null> {
475
+ return attributeCertificateHolderOrg(input)
476
+ },
477
+ async getCertificateHolderOrg(certificateId: string): Promise<CertificateHolderOrg | null> {
478
+ return getCertificateHolderOrg(certificateId)
479
+ },
480
+ async listCertificateHolderOrgs(filter?: { orgId?: string }): Promise<CertificateHolderOrg[]> {
481
+ return listCertificateHolderOrgs(filter)
482
+ },
483
+ async createCertificateHolderClaim(input: {
484
+ certificateId: string
485
+ claimantOrgId: string
486
+ claimantOrgName: string
487
+ matchedHolderName: string
488
+ claimedBy: string
489
+ }): Promise<CertificateHolderClaim> {
490
+ return createCertificateHolderClaim(input)
491
+ },
492
+ async getCertificateHolderClaim(id: string): Promise<CertificateHolderClaim | null> {
493
+ return getCertificateHolderClaim(id)
494
+ },
495
+ async listCertificateHolderClaims(filter?: {
496
+ state?: CertificateHolderClaim['state']
497
+ claimantOrgId?: string
498
+ certificateId?: string
499
+ }): Promise<CertificateHolderClaim[]> {
500
+ return listCertificateHolderClaims(filter)
501
+ },
502
+ async decideCertificateHolderClaim(
503
+ id: string,
504
+ decision: { status: 'confirmed' | 'refused'; decidedBy: string; refusalReason?: string | null },
505
+ ): Promise<CertificateHolderClaim | null> {
506
+ return decideCertificateHolderClaim(id, decision)
507
+ },
508
+ async findPendingCertificateHolderClaim(certificateId: string): Promise<CertificateHolderClaim | null> {
509
+ return findPendingCertificateHolderClaim(certificateId)
510
+ },
511
+
512
+ // ── the instrument register (TODO.register/03) ──
513
+ async listInstrumentRegistrations(): Promise<InstrumentRegistration[]> {
514
+ return listInstrumentRegistrations()
515
+ },
516
+ async listInstrumentRegistrationsForCertificate(certificateId: string): Promise<InstrumentRegistration[]> {
517
+ return listInstrumentRegistrationsForCertificate(certificateId)
518
+ },
519
+ async listInstrumentRegistrationsForHolder(holderOrgId: string): Promise<InstrumentRegistration[]> {
520
+ return listInstrumentRegistrationsForHolder(holderOrgId)
521
+ },
522
+ async getInstrumentRegistration(id: string): Promise<InstrumentRegistration | null> {
523
+ return getInstrumentRegistration(id)
524
+ },
525
+ async createInstrumentRegistration(input: {
526
+ id: string
527
+ certificateId: string
528
+ holderOrgId: string
529
+ standardId: string
530
+ serialNumber: string
531
+ manufactureDate?: string | null
532
+ designations?: Record<string, unknown>
533
+ scopeStatus: InstrumentRegistrationScopeStatus
534
+ scopeDetail?: string | null
535
+ registeredBy?: string | null
536
+ }): Promise<InstrumentRegistration | null> {
537
+ return createInstrumentRegistration(input)
538
+ },
539
+ async setInstrumentRegistrationLifecycle(
540
+ id: string,
541
+ lifecycle: InstrumentRegistrationLifecycle,
542
+ actor?: string | null,
543
+ ): Promise<InstrumentRegistration | null> {
544
+ return setInstrumentRegistrationLifecycle(id, lifecycle, actor)
545
+ },
546
+
547
+ // ── the OIDC Provider (TODO.identity/01) ──
548
+ async getOidcClient(clientId: string): Promise<OidcClient | null> {
549
+ return getOidcClient(clientId)
550
+ },
551
+ async listOidcClients(): Promise<OidcClient[]> {
552
+ return listOidcClients()
553
+ },
554
+ async upsertOidcClient(input: {
555
+ clientId: string
556
+ name: string
557
+ secretHash: string | null
558
+ redirectUris: string[]
559
+ claimsPolicy: { claims: string[] } | null
560
+ createdBy?: string | null
561
+ }): Promise<OidcClient> {
562
+ return upsertOidcClient(input)
563
+ },
564
+ async setOidcClientStatus(clientId: string, status: OidcClient['status']): Promise<OidcClient | null> {
565
+ return setOidcClientStatus(clientId, status)
566
+ },
567
+ async setOidcClientLaunch(clientId: string, launch: OidcClientLaunch | null): Promise<OidcClient | null> {
568
+ return setOidcClientLaunch(clientId, launch)
569
+ },
570
+ async createOidcAuthorization(input: {
571
+ id: string
572
+ clientId: string
573
+ redirectUri: string
574
+ scope: string
575
+ state: string
576
+ nonce: string | null
577
+ codeChallenge: string
578
+ userId: string | null
579
+ ttlMs: number
580
+ }): Promise<OidcAuthorization> {
581
+ return createOidcAuthorization(input)
582
+ },
583
+ async getOidcAuthorization(id: string): Promise<OidcAuthorization | null> {
584
+ return getOidcAuthorization(id)
585
+ },
586
+ async decideOidcAuthorization(
587
+ id: string,
588
+ decision: { userId: string; decision: 'allow' | 'deny' },
589
+ ): Promise<OidcAuthorization | null> {
590
+ return decideOidcAuthorization(id, decision)
591
+ },
592
+ async createOidcCode(input: {
593
+ code: string
594
+ clientId: string
595
+ redirectUri: string
596
+ scope: string
597
+ nonce: string | null
598
+ codeChallenge: string
599
+ userId: string
600
+ contextOrg?: string | null
601
+ amr?: string[] | null
602
+ ttlMs: number
603
+ }): Promise<void> {
604
+ createOidcCode(input)
605
+ },
606
+ async consumeOidcCode(code: string): Promise<OidcCode | null> {
607
+ return consumeOidcCode(code)
608
+ },
609
+ async createOidcAccessToken(input: {
610
+ token: string
611
+ userId: string
612
+ clientId: string
613
+ scope: string
614
+ contextOrg?: string | null
615
+ amr?: string[] | null
616
+ ttlMs: number
617
+ }): Promise<void> {
618
+ createOidcAccessToken(input)
619
+ },
620
+ async getOidcAccessToken(token: string): Promise<OidcAccessToken | null> {
621
+ return getOidcAccessToken(token)
622
+ },
623
+ async listOidcKeys(): Promise<OidcKeyRow[]> {
624
+ return listOidcKeys()
625
+ },
626
+ async upsertOidcKey(input: { kid: string; publicJwk: string }): Promise<void> {
627
+ upsertOidcKey(input)
628
+ },
629
+ async retireOidcKey(kid: string): Promise<void> {
630
+ retireOidcKey(kid)
631
+ },
632
+
633
+ // ── the upstream providers (TODO.identity/08) ──
634
+ async listIdentityProviders(): Promise<IdentityProvider[]> {
635
+ return listIdentityProviders()
636
+ },
637
+ async getIdentityProvider(id: string): Promise<IdentityProvider | null> {
638
+ return getIdentityProvider(id)
639
+ },
640
+ async upsertIdentityProvider(input: {
641
+ id: string
642
+ kind: IdentityProvider['kind']
643
+ displayName: string
644
+ brandMark?: string | null
645
+ issuer?: string | null
646
+ clientId: string
647
+ clientSecretRef?: string | null
648
+ scopes?: string | null
649
+ enabled?: boolean
650
+ createdBy?: string | null
651
+ }): Promise<IdentityProvider> {
652
+ return upsertIdentityProvider(input)
653
+ },
654
+ async setIdentityProviderEnabled(id: string, enabled: boolean): Promise<IdentityProvider | null> {
655
+ return setIdentityProviderEnabled(id, enabled)
656
+ },
657
+ async deleteIdentityProvider(id: string): Promise<boolean> {
658
+ return deleteIdentityProvider(id)
659
+ },
660
+
661
+ // ── the linked identities (TODO.identity/02's shape, 08's flows) ──
662
+ async listIdentityLinks(userId: string): Promise<IdentityLink[]> {
663
+ return listIdentityLinks(userId)
664
+ },
665
+ async findIdentityLink(provider: string, providerAccountId: string): Promise<IdentityLink | null> {
666
+ return findIdentityLink(provider, providerAccountId)
667
+ },
668
+ async createIdentityLink(input: {
669
+ userId: string
670
+ provider: string
671
+ providerAccountId: string
672
+ linkedBy?: string | null
673
+ }): Promise<IdentityLink | null> {
674
+ return createIdentityLink(input)
675
+ },
676
+ async deleteIdentityLink(userId: string, provider: string): Promise<boolean> {
677
+ return deleteIdentityLink(userId, provider)
678
+ },
679
+
680
+ // ── the OP's account model (TODO.identity/02) ──
681
+ async createOpAccount(input: {
682
+ email: string
683
+ name: string
684
+ role: string
685
+ createdBy?: string | null
686
+ }): Promise<UserAdminRow | null> {
687
+ return createOpAccount(input)
688
+ },
689
+ async getPasswordLogin(email: string): Promise<{ userId: string; hash: string; active: boolean } | null> {
690
+ return getPasswordLogin(email)
691
+ },
692
+ async setPasswordHash(userId: string, hash: string, setBy?: string | null): Promise<void> {
693
+ setPasswordHash(userId, hash, setBy)
694
+ },
695
+ async countSignInMethods(userId: string): Promise<{ password: boolean; links: number; passkeys: number }> {
696
+ return countSignInMethods(userId)
697
+ },
698
+ async createEnrollmentToken(input: {
699
+ token: string
700
+ userId: string
701
+ createdBy?: string | null
702
+ ttlMs: number
703
+ }): Promise<EnrollmentToken> {
704
+ return createEnrollmentToken(input)
705
+ },
706
+ async getEnrollmentToken(token: string): Promise<EnrollmentToken | null> {
707
+ return getEnrollmentToken(token)
708
+ },
709
+ async completeEnrollment(token: string, passwordHash: string, setBy?: string | null): Promise<CompleteEnrollmentResult> {
710
+ return completeEnrollment(token, passwordHash, setBy)
711
+ },
712
+ async listUserSessions(userId: string, currentToken?: string): Promise<SessionView[]> {
713
+ return listUserSessions(userId, currentToken)
714
+ },
715
+ async deleteSessionById(userId: string, sessionId: string): Promise<boolean> {
716
+ return deleteSessionById(userId, sessionId)
717
+ },
718
+ async listOpLiveSessions(currentToken?: string): Promise<OpLiveSession[]> {
719
+ return listOpLiveSessions(currentToken)
720
+ },
721
+ async deleteAllUserSessions(userId: string): Promise<number> {
722
+ return deleteAllUserSessions(userId)
723
+ },
724
+ // ── the account console (TODO.identity/06) ──
725
+ async updateUserName(userId: string, name: string): Promise<boolean> {
726
+ return updateUserName(userId, name)
727
+ },
728
+ async setUserAvatar(userId: string, avatarUrl: string | null): Promise<boolean> {
729
+ return setUserAvatar(userId, avatarUrl)
730
+ },
731
+ async deletePasswordHash(userId: string): Promise<boolean> {
732
+ return deletePasswordHash(userId)
733
+ },
734
+ async deleteOtherSessions(userId: string, keepToken: string): Promise<number> {
735
+ return deleteOtherSessions(userId, keepToken)
736
+ },
737
+ async createEmailChangeToken(input: {
738
+ token: string
739
+ userId: string
740
+ newEmail: string
741
+ deliveredBy: 'mailer' | 'shown'
742
+ ttlMs: number
743
+ }): Promise<EmailChangeToken> {
744
+ return createEmailChangeToken(input)
745
+ },
746
+ async getEmailChangeToken(token: string): Promise<EmailChangeToken | null> {
747
+ return getEmailChangeToken(token)
748
+ },
749
+ async getPendingEmailChange(userId: string): Promise<EmailChangeToken | null> {
750
+ return getPendingEmailChange(userId)
751
+ },
752
+ async completeEmailChange(token: string): Promise<CompleteEmailChangeResult> {
753
+ return completeEmailChange(token)
754
+ },
755
+
756
+ // ── strong authentication: the factor registry (TODO.identity-sso/02 + /03) ──
757
+ async createWebauthnChallenge(input: {
758
+ challenge: string
759
+ userId: string | null
760
+ kind: WebauthnChallenge['kind']
761
+ ttlMs: number
762
+ }): Promise<void> {
763
+ createWebauthnChallenge(input)
764
+ },
765
+ async consumeWebauthnChallenge(challenge: string): Promise<WebauthnChallenge | null> {
766
+ return consumeWebauthnChallenge(challenge)
767
+ },
768
+ async createWebauthnCredential(input: {
769
+ credentialId: string
770
+ userId: string
771
+ name: string
772
+ publicKeyCose: string
773
+ signCount: number
774
+ aaguid: string | null
775
+ transports: string[]
776
+ ip?: string | null
777
+ }): Promise<WebauthnCredential | null> {
778
+ return createWebauthnCredential(input)
779
+ },
780
+ async listWebauthnCredentials(userId: string): Promise<WebauthnCredential[]> {
781
+ return listWebauthnCredentials(userId)
782
+ },
783
+ async getWebauthnCredential(credentialId: string): Promise<WebauthnCredential | null> {
784
+ return getWebauthnCredential(credentialId)
785
+ },
786
+ async deleteWebauthnCredential(userId: string, credentialId: string): Promise<boolean> {
787
+ return deleteWebauthnCredential(userId, credentialId)
788
+ },
789
+ async advanceWebauthnCounter(credentialId: string, newCount: number, opts?: { ip?: string | null }): Promise<AdvanceCounterResult> {
790
+ return advanceWebauthnCounter(credentialId, newCount, opts)
791
+ },
792
+ async createTotpSecret(input: { id: string; userId: string; name: string; secret: string }): Promise<TotpSecret> {
793
+ return createTotpSecret(input)
794
+ },
795
+ async listTotpSecrets(userId: string): Promise<TotpSecret[]> {
796
+ return listTotpSecrets(userId)
797
+ },
798
+ async getTotpSecret(id: string): Promise<TotpSecret | null> {
799
+ return getTotpSecret(id)
800
+ },
801
+ async markTotpSecretVerified(id: string, userId: string, name: string): Promise<boolean> {
802
+ return markTotpSecretVerified(id, userId, name)
803
+ },
804
+ async recordTotpEnrollFailure(id: string, userId: string): Promise<number> {
805
+ return recordTotpEnrollFailure(id, userId)
806
+ },
807
+ async markTotpSecretUsed(id: string, opts?: { ip?: string | null }): Promise<void> {
808
+ markTotpSecretUsed(id, opts)
809
+ },
810
+ async deleteTotpSecret(userId: string, id: string): Promise<boolean> {
811
+ return deleteTotpSecret(userId, id)
812
+ },
813
+ async replaceRecoveryCodes(userId: string, batch: string, codeHashes: string[]): Promise<void> {
814
+ replaceRecoveryCodes(userId, batch, codeHashes)
815
+ },
816
+ async recoveryCodeState(userId: string): Promise<RecoveryCodeState> {
817
+ return recoveryCodeState(userId)
818
+ },
819
+ async consumeRecoveryCode(userId: string, codeHash: string): Promise<boolean> {
820
+ return consumeRecoveryCode(userId, codeHash)
821
+ },
822
+ async createMfaPending(input: { token: string; userId: string; amr: string[]; ttlMs: number }): Promise<void> {
823
+ createMfaPending(input)
824
+ },
825
+ async getMfaPending(token: string): Promise<MfaPending | null> {
826
+ return getMfaPending(token)
827
+ },
828
+ async consumeMfaPending(token: string): Promise<MfaPending | null> {
829
+ return consumeMfaPending(token)
830
+ },
831
+ async recordMfaPendingFailure(token: string): Promise<MfaPending | null> {
832
+ return recordMfaPendingFailure(token)
833
+ },
834
+
835
+ // ── the central user registry (TODO.identity/03) ──
836
+ async listOpClientRoles(userId: string): Promise<OpClientRoleAssignment[]> {
837
+ return listOpClientRoles(userId)
838
+ },
839
+ async listAllOpClientRoles(): Promise<OpClientRoleAssignment[]> {
840
+ return listAllOpClientRoles()
841
+ },
842
+ async getOpClientRoles(userId: string, clientId: string): Promise<string[] | null> {
843
+ return getOpClientRoles(userId, clientId)
844
+ },
845
+ async setOpClientRoles(userId: string, clientId: string, roles: string[], assignedBy: string | null): Promise<void> {
846
+ setOpClientRoles(userId, clientId, roles, assignedBy)
847
+ },
848
+ async deleteOpClientRoles(userId: string, clientId: string): Promise<boolean> {
849
+ return deleteOpClientRoles(userId, clientId)
850
+ },
851
+ async revokeOpUserCredentials(userId: string): Promise<{ sessions: number; accessTokens: number; codes: number; authorizations: number }> {
852
+ return revokeOpUserCredentials(userId)
853
+ },
854
+ async eraseOpAccount(userId: string): Promise<OpAccountErasure | null> {
855
+ return eraseOpAccount(userId)
856
+ },
857
+ async updateOpAccount(id: string, input: { name?: string; email?: string }): Promise<boolean> {
858
+ return updateOpAccount(id, input)
859
+ },
860
+ async lastAccountSignIns(): Promise<Record<string, string>> {
861
+ return lastAccountSignIns()
862
+ },
863
+
864
+ // ── the workflow entity store + change journal ──
865
+ async listEntities(store: string): Promise<EntityRow[]> {
866
+ return listEntities(store)
867
+ },
868
+ async getEntity(store: string, id: string): Promise<EntityRow | undefined> {
869
+ return getEntity(store, id)
870
+ },
871
+ async putEntity(store: string, id: string, orgId: string | null, data: string): Promise<void> {
872
+ putEntity(store, id, orgId, data)
873
+ },
874
+ async deleteEntity(store: string, id: string): Promise<boolean> {
875
+ return deleteEntity(store, id)
876
+ },
877
+ async changesAfter(seq: number, limit = 500): Promise<EntityChange[]> {
878
+ return changesAfter(seq, limit)
879
+ },
880
+ async latestChangeSeq(): Promise<number> {
881
+ return latestChangeSeq()
882
+ },
883
+
884
+ // ── the platform event store (TODO.notify/01) ──
885
+ async appendEvent(input: {
886
+ id: string
887
+ domain: string
888
+ entityId: string
889
+ action: string
890
+ payload: string
891
+ }): Promise<PlatformEvent> {
892
+ return appendEvent(input)
893
+ },
894
+ async eventsAfter(seq: number, limit = 500): Promise<PlatformEvent[]> {
895
+ return eventsAfter(seq, limit)
896
+ },
897
+ async latestEventSeq(): Promise<number> {
898
+ return latestEventSeq()
899
+ },
900
+ async getEvent(id: string): Promise<PlatformEvent | null> {
901
+ return getEvent(id)
902
+ },
903
+ async eventsMatching(filter: EventKeyFilter, limit = 500): Promise<PlatformEvent[]> {
904
+ return eventsMatching(filter, limit)
905
+ },
906
+
907
+ // ── the notification subscriptions store (TODO.notify/02) ──
908
+ async listNotifyRules(userId: string): Promise<NotifyRule[]> {
909
+ return listNotifyRules(userId)
910
+ },
911
+ async putNotifyRule(input: {
912
+ id: string
913
+ userId: string
914
+ pattern: string
915
+ domain: string
916
+ entityId: string | null
917
+ action: string | null
918
+ mode: NotifyRule['mode']
919
+ channelOverrides: string | null
920
+ }): Promise<NotifyRule> {
921
+ return putNotifyRule(input)
922
+ },
923
+ async deleteNotifyRule(userId: string, pattern: string): Promise<boolean> {
924
+ return deleteNotifyRule(userId, pattern)
925
+ },
926
+ async notifyRulesForEvent(filter: { domain: string; entityId: string; action: string }): Promise<NotifyRule[]> {
927
+ return notifyRulesForEvent(filter)
928
+ },
929
+ async listNotifyEntityMutes(userId: string): Promise<NotifyEntityMute[]> {
930
+ return listNotifyEntityMutes(userId)
931
+ },
932
+ async putNotifyEntityMute(input: { id: string; userId: string; domain: string; entityId: string }): Promise<NotifyEntityMute> {
933
+ return putNotifyEntityMute(input)
934
+ },
935
+ async deleteNotifyEntityMute(userId: string, domain: string, entityId: string): Promise<boolean> {
936
+ return deleteNotifyEntityMute(userId, domain, entityId)
937
+ },
938
+ async notifyEntityMutesForEvent(domain: string, entityId: string): Promise<NotifyEntityMute[]> {
939
+ return notifyEntityMutesForEvent(domain, entityId)
940
+ },
941
+ async getNotifyPreferences(userId: string): Promise<NotifyPreferences | null> {
942
+ return getNotifyPreferences(userId)
943
+ },
944
+ async putNotifyPreferences(userId: string, channels: string): Promise<NotifyPreferences> {
945
+ return putNotifyPreferences(userId, channels)
946
+ },
947
+
948
+ // ── the inbox state (TODO.notify/03) ──
949
+ async listNotifyInboxStates(userId: string): Promise<NotifyInboxState[]> {
950
+ return listNotifyInboxStates(userId)
951
+ },
952
+ async putNotifyInboxState(input: {
953
+ userId: string
954
+ eventId: string
955
+ read?: boolean
956
+ done?: boolean
957
+ }): Promise<NotifyInboxState> {
958
+ return putNotifyInboxState(input)
959
+ },
960
+
961
+ // ── provisioning / dev support ──
962
+ async wipeWorkflowStores(range?: { after: number; through: number }): Promise<number> {
963
+ const db = getDb()
964
+ let rows = 0
965
+ // One transaction, the same atomicity the D1 batch gives.
966
+ db.transaction(() => {
967
+ for (const table of WIPE_TABLES) {
968
+ rows += range
969
+ ? db.prepare(`DELETE FROM ${table} WHERE rowid > ? AND rowid <= ?`).run(range.after, range.through).changes
970
+ : db.prepare(`DELETE FROM ${table}`).run().changes
971
+ }
972
+ })()
973
+ return rows
974
+ },
975
+ async workflowStoreRowCeiling(): Promise<number> {
976
+ const row = getDb().prepare(
977
+ `SELECT MAX(ceiling) AS ceiling FROM (
978
+ SELECT MAX(rowid) AS ceiling FROM entity_changes
979
+ UNION ALL SELECT MAX(rowid) FROM evidence_records
980
+ UNION ALL SELECT MAX(rowid) FROM entities
981
+ UNION ALL SELECT MAX(rowid) FROM events
982
+ UNION ALL SELECT MAX(rowid) FROM instrument_registrations)`,
983
+ ).get() as { ceiling: number | null }
984
+ return row.ceiling ?? 0
985
+ },
986
+ async countEntities(): Promise<number> {
987
+ return (getDb().prepare('SELECT COUNT(*) AS n FROM entities').get() as { n: number }).n
988
+ },
989
+ }
990
+ }
991
+
992
+ /** The node composition root's one-liner: the SQLite store becomes THE
993
+ * store (server/index.ts, the node scripts, the route-level tests). */
994
+ export function installSqliteStore(): ServerStore {
995
+ const store = createSqliteServerStore()
996
+ installStore(store)
997
+ return store
998
+ }
999
+
1000
+ // ── The extraction surface (TODO.identity-extract/01) ───────────────
1001
+ // This subpath IS the SQLite cone (the map, PROGRESS/41 §2.1): the six
1002
+ // pre-extraction modules (the composer above + store/entities/
1003
+ // op-store/op-accounts-store/upstream-store under ./sqlite/) behind one
1004
+ // specifier, so the cone's direct consumers (the dev-reset seam's
1005
+ // getDb, the evidence adapter, the SQLite repository, the route-level
1006
+ // tests) import unchanged names from the package.
1007
+ export * from './sqlite/store'
1008
+ export * from './sqlite/entities'
1009
+ export * from './sqlite/events'
1010
+ export * from './sqlite/op-store'
1011
+ export * from './sqlite/op-accounts-store'
1012
+ export * from './sqlite/factors-store'
1013
+ export * from './sqlite/upstream-store'
1014
+
1015
+ /** The SQLite store's own DDL, shipped with the package. Consumers that
1016
+ * replay the schema (reset-db, the tripwires) read it from this path
1017
+ * instead of guessing the package's installed location. */
1018
+ export const SQLITE_SCHEMA_PATH = fileURLToPath(new URL('./sqlite/schema.sql', import.meta.url))
1019
+
1020
+ /** The canonical D1 migration set, shipped with the package
1021
+ * (migrations/): the ONE set both deployments apply. Wrangler's
1022
+ * migrations_dir points at this directory in the consumer's
1023
+ * node_modules, and the deploys' D1 journals key the bookkeeping on
1024
+ * the FILENAMES. Expand-only, never renumber (AGENTS.md, the
1025
+ * migration contract). The migrations test pins this set's end state
1026
+ * to schema.sql in lockstep. */
1027
+ export const MIGRATIONS_DIR = fileURLToPath(new URL('../../migrations', import.meta.url))