@oimlsmart/platform-server 0.1.3 → 0.1.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,34 @@
1
+ -- Migration 0019 — the OIML Member category (TODO.identity-features/10,
2
+ -- the taxonomy correction): the org_registry gains the two designation
3
+ -- LINK columns and the CS status facet.
4
+ --
5
+ -- The corrected model: the Utilizer/Associate are DESIGNATED BODIES
6
+ -- (their own organization rows, signing the Declaration per PD-08),
7
+ -- never statuses on a member; "OIML Member" is the category with the
8
+ -- member-state / corresponding-member kinds. The links:
9
+ --
10
+ -- designated_by the designating body: a Utilizer's is its MEMBER
11
+ -- STATE, an Associate's its CORRESPONDING MEMBER, a
12
+ -- Test Laboratory's its associated ISSUING AUTHORITY
13
+ -- (the participants model's designated_by:
14
+ -- issuing_authority);
15
+ -- proposed_by an Issuing Authority's proposing MEMBER STATE (a
16
+ -- member state participates in the OIML-CS by
17
+ -- PROPOSING an IA and DESIGNATING a Utilizer);
18
+ -- cs_status the designated bodies' Declaration standing
19
+ -- (signed-active / suspended / withdrawn — the CS
20
+ -- layer's fact projected onto the identity plane).
21
+ --
22
+ -- Expand-only, and deliberately NULL-defaulted: the kind enforcement of
23
+ -- the links (which kind may point at which) is the PROGRAM's write path
24
+ -- (the store keeps the kind column opaque by doctrine), and a legacy
25
+ -- row — a utilizer/associate curated before this migration — keeps its
26
+ -- home untouched: the links read NULL ("not recorded"), never a
27
+ -- destructive move. The journals' discipline holds.
28
+ -- schema.sql carries the same end state for fresh databases —
29
+ -- test/migrations.test.ts pins the UNION of every migration to
30
+ -- schema.sql's CREATE set.
31
+
32
+ ALTER TABLE org_registry ADD COLUMN designated_by TEXT;
33
+ ALTER TABLE org_registry ADD COLUMN proposed_by TEXT;
34
+ ALTER TABLE org_registry ADD COLUMN cs_status TEXT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimlsmart/platform-server",
3
- "version": "0.1.3",
3
+ "version": "0.1.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
@@ -323,6 +323,16 @@ export class D1ServerStore implements ServerStore {
323
323
  )`,
324
324
  ).run()
325
325
  await this.db.prepare('CREATE INDEX IF NOT EXISTS idx_org_registry_state ON org_registry (state)').run()
326
+ // TODO.identity-features/10 (the OIML Member category): the
327
+ // designation links + the CS status facet arrive with migration
328
+ // 0019 — a dev D1 predating it grows the columns here (the
329
+ // PRAGMA probe + ALTER posture of ensureUserColumns). NULL = not
330
+ // recorded: existing rows keep their posture silently.
331
+ const cols = await this.db.prepare('PRAGMA table_info(org_registry)').all<{ name: string }>()
332
+ const names = new Set(cols.results.map(c => c.name))
333
+ if (!names.has('designated_by')) await this.db.prepare('ALTER TABLE org_registry ADD COLUMN designated_by TEXT').run()
334
+ if (!names.has('proposed_by')) await this.db.prepare('ALTER TABLE org_registry ADD COLUMN proposed_by TEXT').run()
335
+ if (!names.has('cs_status')) await this.db.prepare('ALTER TABLE org_registry ADD COLUMN cs_status TEXT').run()
326
336
  })()
327
337
  }
328
338
  return this.orgRegistrySupportEnsured
@@ -2188,6 +2198,9 @@ export class D1ServerStore implements ServerStore {
2188
2198
  country: (row.country as string | null) ?? null,
2189
2199
  contacts,
2190
2200
  participantRef: (row.participant_ref as string | null) ?? null,
2201
+ designatedBy: (row.designated_by as string | null) ?? null,
2202
+ proposedBy: (row.proposed_by as string | null) ?? null,
2203
+ csStatus: (row.cs_status as string | null) ?? null,
2191
2204
  state: row.state as OrgRegistryState,
2192
2205
  createdAt: row.created_at as string,
2193
2206
  createdBy: (row.created_by as string | null) ?? null,
@@ -2219,14 +2232,18 @@ export class D1ServerStore implements ServerStore {
2219
2232
  country?: string | null
2220
2233
  contacts?: OrgRegistryContact[]
2221
2234
  participantRef?: string | null
2235
+ designatedBy?: string | null
2236
+ proposedBy?: string | null
2237
+ csStatus?: string | null
2222
2238
  createdBy?: string | null
2223
2239
  }): Promise<OrgRegistryOrg | null> {
2224
2240
  await this.ensureOrgRegistrySupport()
2225
2241
  const res = await this.stmt(
2226
- `INSERT OR IGNORE INTO org_registry (id, name, short_name, kind, country, contacts, participant_ref, created_by)
2227
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
2242
+ `INSERT OR IGNORE INTO org_registry (id, name, short_name, kind, country, contacts, participant_ref, designated_by, proposed_by, cs_status, created_by)
2243
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2228
2244
  input.id, input.name, input.shortName ?? null, input.kind ?? null, input.country ?? null,
2229
- JSON.stringify(input.contacts ?? []), input.participantRef ?? null, input.createdBy ?? null,
2245
+ JSON.stringify(input.contacts ?? []), input.participantRef ?? null,
2246
+ input.designatedBy ?? null, input.proposedBy ?? null, input.csStatus ?? null, input.createdBy ?? null,
2230
2247
  ).run()
2231
2248
  if ((res.meta.changes ?? 0) === 0) return null
2232
2249
  return this.getOrgRegistryOrg(input.id)
@@ -2242,6 +2259,9 @@ export class D1ServerStore implements ServerStore {
2242
2259
  country?: string | null
2243
2260
  contacts?: OrgRegistryContact[]
2244
2261
  participantRef?: string | null
2262
+ designatedBy?: string | null
2263
+ proposedBy?: string | null
2264
+ csStatus?: string | null
2245
2265
  },
2246
2266
  actor?: string | null,
2247
2267
  ): Promise<OrgRegistryOrg | null> {
@@ -2254,6 +2274,9 @@ export class D1ServerStore implements ServerStore {
2254
2274
  if (patch.country !== undefined) { sets.push('country = ?'); params.push(patch.country) }
2255
2275
  if (patch.contacts !== undefined) { sets.push('contacts = ?'); params.push(JSON.stringify(patch.contacts)) }
2256
2276
  if (patch.participantRef !== undefined) { sets.push('participant_ref = ?'); params.push(patch.participantRef) }
2277
+ if (patch.designatedBy !== undefined) { sets.push('designated_by = ?'); params.push(patch.designatedBy) }
2278
+ if (patch.proposedBy !== undefined) { sets.push('proposed_by = ?'); params.push(patch.proposedBy) }
2279
+ if (patch.csStatus !== undefined) { sets.push('cs_status = ?'); params.push(patch.csStatus) }
2257
2280
  sets.push("updated_at = datetime('now')", 'updated_by = ?')
2258
2281
  params.push(actor ?? null)
2259
2282
  const res = await this.stmt(`UPDATE org_registry SET ${sets.join(', ')} WHERE id = ?`, ...params, id).run()
@@ -668,7 +668,7 @@ CREATE INDEX IF NOT EXISTS idx_mfa_pending_user ON mfa_pending (user_id);
668
668
  -- registry never merge (the spec's §4), and a membership row's honesty
669
669
  -- (its lifecycle state) never depends on a join.
670
670
  -- The D1 migration set carries the identical end state
671
- -- (0013_org_registry.sql).
671
+ -- (0013_org_registry.sql + 0019_org_member_category.sql).
672
672
  -- ═══════════════════════════════════════════════════════════════════
673
673
  CREATE TABLE IF NOT EXISTS org_registry (
674
674
  id TEXT PRIMARY KEY,
@@ -684,6 +684,17 @@ CREATE TABLE IF NOT EXISTS org_registry (
684
684
  -- The participant-link annotation (which participant record the org
685
685
  -- mirrors); documentation only.
686
686
  participant_ref TEXT,
687
+ -- The designation links + the CS status facet (0019_org_member_category.sql,
688
+ -- TODO.identity-features/10): a Utilizer's designated_by is its MEMBER
689
+ -- STATE, an Associate's its CORRESPONDING MEMBER, a Test Laboratory's
690
+ -- its associated ISSUING AUTHORITY; an Issuing Authority's proposed_by
691
+ -- is its proposing MEMBER STATE; cs_status is the designated bodies'
692
+ -- Declaration standing (signed-active / suspended / withdrawn). All
693
+ -- NULL = not recorded; the kind enforcement is the program's write
694
+ -- path, the store keeps the columns opaque.
695
+ designated_by TEXT,
696
+ proposed_by TEXT,
697
+ cs_status TEXT,
687
698
  state TEXT NOT NULL DEFAULT 'active',
688
699
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
689
700
  created_by TEXT,
@@ -115,6 +115,22 @@ function migrateAuthTables(db: Database.Database): void {
115
115
  if (membershipCols.length && !membershipCols.some(c => c.name === 'cone')) {
116
116
  db.exec('ALTER TABLE org_memberships ADD COLUMN cone TEXT')
117
117
  }
118
+ // TODO.identity-features/10 (the OIML Member category): the
119
+ // designation links + the CS status facet arrive with migration 0019 —
120
+ // a dev file predating it grows the columns here. NULL = not recorded:
121
+ // existing rows keep their posture silently.
122
+ const registryCols = db.prepare('PRAGMA table_info(org_registry)').all() as Array<{ name: string }>
123
+ if (registryCols.length) {
124
+ if (!registryCols.some(c => c.name === 'designated_by')) {
125
+ db.exec('ALTER TABLE org_registry ADD COLUMN designated_by TEXT')
126
+ }
127
+ if (!registryCols.some(c => c.name === 'proposed_by')) {
128
+ db.exec('ALTER TABLE org_registry ADD COLUMN proposed_by TEXT')
129
+ }
130
+ if (!registryCols.some(c => c.name === 'cs_status')) {
131
+ db.exec('ALTER TABLE org_registry ADD COLUMN cs_status TEXT')
132
+ }
133
+ }
118
134
  // TODO.identity-sso/02+03 (the strong-authentication wave): the amr
119
135
  // provenance columns on sessions → codes → access tokens.
120
136
  if (!sessionCols.some(c => c.name === 'amr')) {
@@ -1008,6 +1024,9 @@ interface OrgRegistryRow {
1008
1024
  country: string | null
1009
1025
  contacts: string
1010
1026
  participant_ref: string | null
1027
+ designated_by: string | null
1028
+ proposed_by: string | null
1029
+ cs_status: string | null
1011
1030
  state: OrgRegistryState
1012
1031
  created_at: string
1013
1032
  created_by: string | null
@@ -1042,6 +1061,9 @@ function orgRegistryPayload(row: OrgRegistryRow): OrgRegistryOrg {
1042
1061
  country: row.country,
1043
1062
  contacts: parseOrgContacts(row.contacts),
1044
1063
  participantRef: row.participant_ref,
1064
+ designatedBy: row.designated_by ?? null,
1065
+ proposedBy: row.proposed_by ?? null,
1066
+ csStatus: row.cs_status ?? null,
1045
1067
  state: row.state,
1046
1068
  createdAt: row.created_at,
1047
1069
  createdBy: row.created_by,
@@ -1071,14 +1093,18 @@ export function createOrgRegistryOrg(input: {
1071
1093
  country?: string | null
1072
1094
  contacts?: OrgRegistryContact[]
1073
1095
  participantRef?: string | null
1096
+ designatedBy?: string | null
1097
+ proposedBy?: string | null
1098
+ csStatus?: string | null
1074
1099
  createdBy?: string | null
1075
1100
  }): OrgRegistryOrg | null {
1076
1101
  const res = getDb().prepare(
1077
- `INSERT OR IGNORE INTO org_registry (id, name, short_name, kind, country, contacts, participant_ref, created_by)
1078
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
1102
+ `INSERT OR IGNORE INTO org_registry (id, name, short_name, kind, country, contacts, participant_ref, designated_by, proposed_by, cs_status, created_by)
1103
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1079
1104
  ).run(
1080
1105
  input.id, input.name, input.shortName ?? null, input.kind ?? null, input.country ?? null,
1081
- JSON.stringify(input.contacts ?? []), input.participantRef ?? null, input.createdBy ?? null,
1106
+ JSON.stringify(input.contacts ?? []), input.participantRef ?? null,
1107
+ input.designatedBy ?? null, input.proposedBy ?? null, input.csStatus ?? null, input.createdBy ?? null,
1082
1108
  )
1083
1109
  if (res.changes === 0) return null
1084
1110
  return getOrgRegistryOrg(input.id)
@@ -1095,6 +1121,9 @@ export function updateOrgRegistryOrg(
1095
1121
  country?: string | null
1096
1122
  contacts?: OrgRegistryContact[]
1097
1123
  participantRef?: string | null
1124
+ designatedBy?: string | null
1125
+ proposedBy?: string | null
1126
+ csStatus?: string | null
1098
1127
  },
1099
1128
  actor?: string | null,
1100
1129
  ): OrgRegistryOrg | null {
@@ -1106,6 +1135,9 @@ export function updateOrgRegistryOrg(
1106
1135
  if (patch.country !== undefined) { sets.push('country = ?'); params.push(patch.country) }
1107
1136
  if (patch.contacts !== undefined) { sets.push('contacts = ?'); params.push(JSON.stringify(patch.contacts)) }
1108
1137
  if (patch.participantRef !== undefined) { sets.push('participant_ref = ?'); params.push(patch.participantRef) }
1138
+ if (patch.designatedBy !== undefined) { sets.push('designated_by = ?'); params.push(patch.designatedBy) }
1139
+ if (patch.proposedBy !== undefined) { sets.push('proposed_by = ?'); params.push(patch.proposedBy) }
1140
+ if (patch.csStatus !== undefined) { sets.push('cs_status = ?'); params.push(patch.csStatus) }
1109
1141
  sets.push("updated_at = datetime('now')", 'updated_by = ?')
1110
1142
  params.push(actor ?? null)
1111
1143
  const res = getDb().prepare(`UPDATE org_registry SET ${sets.join(', ')} WHERE id = ?`).run(...params, id)
package/src/store.ts CHANGED
@@ -898,19 +898,40 @@ export interface OrgRegistryContact {
898
898
  * scheme consumer); the program side bounds the assignable per-org
899
899
  * roles by it. `participantRef` is the OPTIONAL annotation documenting
900
900
  * which participant record the org mirrors (the link's documentation,
901
- * never a key the store resolves). */
901
+ * never a key the store resolves).
902
+ *
903
+ * TODO.identity-features/10 (the OIML Member category, the taxonomy
904
+ * correction): the designation links + the CS status facet (migration
905
+ * 0019). `designatedBy` is the designating body (a Utilizer's member
906
+ * state, an Associate's corresponding member, a Test Laboratory's
907
+ * associated issuing authority); `proposedBy` is an Issuing
908
+ * Authority's proposing member state; `csStatus` is the designated
909
+ * bodies' Declaration standing ('signed-active' | 'suspended' |
910
+ * 'withdrawn'). All three are opaque strings to the store — the
911
+ * per-kind link enforcement (which kind may point at which) is the
912
+ * program's write path; NULL reads "not recorded", honestly. */
902
913
  export interface OrgRegistryOrg {
903
914
  id: string
904
915
  /** The display name. */
905
916
  name: string
906
917
  shortName: string | null
907
918
  /** The participant kind ('issuing-authority' | 'test-laboratory' |
908
- * 'utilizer' | 'associate' on the OIML-CS program), NULL for a
919
+ * 'utilizer' | 'associate' on the OIML-CS program, 'member-state' |
920
+ * 'corresponding-member' on the OIML Member category), NULL for a
909
921
  * non-participant org. Opaque to the store. */
910
922
  kind: string | null
911
923
  country: string | null
912
924
  contacts: OrgRegistryContact[]
913
925
  participantRef: string | null
926
+ /** The designating body's org id (TODO.identity-features/10) — the
927
+ * designated-body kinds carry it; NULL = not recorded. */
928
+ designatedBy: string | null
929
+ /** The proposing member state's org id (the issuing-authority kind). */
930
+ proposedBy: string | null
931
+ /** The Declaration's standing on the designated bodies
932
+ * ('signed-active' | 'suspended' | 'withdrawn'); NULL = not
933
+ * recorded. */
934
+ csStatus: string | null
914
935
  state: OrgRegistryState
915
936
  createdAt: string
916
937
  createdBy: string | null
@@ -1702,6 +1723,9 @@ export interface ServerStore {
1702
1723
  country?: string | null
1703
1724
  contacts?: OrgRegistryContact[]
1704
1725
  participantRef?: string | null
1726
+ designatedBy?: string | null
1727
+ proposedBy?: string | null
1728
+ csStatus?: string | null
1705
1729
  createdBy?: string | null
1706
1730
  }): Promise<OrgRegistryOrg | null>
1707
1731
  /** Edit the display data (the id is the stable slug — never editable);
@@ -1716,6 +1740,9 @@ export interface ServerStore {
1716
1740
  country?: string | null
1717
1741
  contacts?: OrgRegistryContact[]
1718
1742
  participantRef?: string | null
1743
+ designatedBy?: string | null
1744
+ proposedBy?: string | null
1745
+ csStatus?: string | null
1719
1746
  },
1720
1747
  actor?: string | null,
1721
1748
  ): Promise<OrgRegistryOrg | null>
@@ -145,6 +145,12 @@ export const TRANSITION_PERMISSIONS: Record<string, readonly ActionPermission[]>
145
145
  // records mode (TODO.adoption/02): the registered-offline entry — the
146
146
  // application's honest hop into ACCEPTED without a fake walked history.
147
147
  'application:register_offline->ACCEPTED': ['records.register'],
148
+ // DEMO_FLOWS wave 1: the lifecycle closure — the IA marks the Evaluation
149
+ // Project COMPLETED once the completion gate's settling conditions all
150
+ // hold (the model's completion_settled guard refuses otherwise); the act
151
+ // is the IA's decision desk on the application, the same family as the
152
+ // accept/reject decisions.
153
+ 'application:ia_marks_completed->COMPLETED': ['application.accept'],
148
154
 
149
155
  // test_request (the dispatch; TODO.adoption/07's quote leg: the
150
156
  // laboratory's quotation response and the IA's commercial decision on it)
@@ -161,6 +167,9 @@ export const TRANSITION_PERMISSIONS: Record<string, readonly ActionPermission[]>
161
167
  // test_report
162
168
  'test_report:lab_submits->SUBMITTED': ['tr.submit'],
163
169
  'test_report:ia_starts_review->UNDER_REVIEW': ['tr.review'],
170
+ // DEMO_FLOWS wave 1: the review period's opening on the test report —
171
+ // the IA's TR-review desk (the consultation rides the review class).
172
+ 'test_report:ia_opens_consultation->CONSULTATION': ['tr.review'],
164
173
  'test_report:ia_accepts->ACCEPTED': ['tr.review'],
165
174
  'test_report:ia_rejects->REJECTED': ['tr.review'],
166
175
  'test_report:lab_withdraws_for_edit->DRAFT': ['tr.submit'],
@@ -184,6 +193,9 @@ export const TRANSITION_PERMISSIONS: Record<string, readonly ActionPermission[]>
184
193
  // Fired by the TR-review service when the last report is determined —
185
194
  // the reviewing officer holds tr.review, the evaluation worker er.review.
186
195
  'evaluation_report:last_tr_determined->ALL_TR_REVIEWED': ['tr.review', 'er.review'],
196
+ // DEMO_FLOWS wave 1: the review period's opening — the IA officer's
197
+ // evaluation-work act (the consultation is the ER's review surface).
198
+ 'evaluation_report:ia_opens_consultation->CONSULTATION': ['er.review'],
187
199
  'evaluation_report:ia_approves->APPROVED': ['er.finalize'],
188
200
  'evaluation_report:ia_rejects->REJECTED': ['er.finalize'],
189
201
  'evaluation_report:ia_approves_with_conditions->CONDITIONALLY_APPROVED': ['er.finalize'],
@@ -217,6 +229,22 @@ export const TRANSITION_PERMISSIONS: Record<string, readonly ActionPermission[]>
217
229
  'certificate:revise->ACTIVE': ['certificate.issue'],
218
230
  'certificate:transfer_ownership->ACTIVE': ['certificate.issue'],
219
231
 
232
+ // certificate_registration (DEMO_FLOWS wave 1 — the register's
233
+ // auto-publish doctrine, the program owner's 2026-08-28 decision): the
234
+ // hub-side registration submission lifecycle — the registrant
235
+ // announcement's shell, the signed package's intake verification, the
236
+ // publication, the recorded refusal. All three edges are SYSTEM acts:
237
+ // the intake automation walks them on the verified signed submission
238
+ // (no human gate between verify and publish — the register's trust is
239
+ // in the signature + the receipt machinery; the human review surfaces
240
+ // stay read postures). The map names the desk authority the automation
241
+ // exercises — the same shape the other engine-fired edges ride (the
242
+ // sample_verification projection's transitions name
243
+ // verification.perform).
244
+ 'certificate_registration:intake_verifies->SUBMITTED': ['certificate.register'],
245
+ 'certificate_registration:auto_publishes->PUBLISHED': ['certificate.register'],
246
+ 'certificate_registration:intake_refuses->REFUSED': ['certificate.register'],
247
+
220
248
  // test_assignment (the laboratory's work items; `omit` rides the IA's
221
249
  // dispatch withdrawal cascade)
222
250
  'test_assignment:lab_accepts->ACCEPTED': ['run.perform'],
@@ -354,6 +382,10 @@ export const STORE_MACHINES: Record<string, string> = {
354
382
  formInstances: 'form_instance',
355
383
  evaluationReports: 'evaluation_report',
356
384
  certificates: 'certificate',
385
+ // DEMO_FLOWS wave 1: the register's registration submissions are
386
+ // machinated too (the intake-verify → auto-publish chain); the generic
387
+ // write path gates status writes on the machine's declared edges.
388
+ certificateRegistrations: 'certificate_registration',
357
389
  testAssignments: 'test_assignment',
358
390
  testRuns: 'test_run',
359
391
  measuringInstrumentSamples: 'measuring_instrument_sample',