@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,12 @@
1
+ // ═══════════════════════════════════════════════════════════════════
2
+ // The shared identity vocabulary (TODO.identity-extract/01, the map:
3
+ // PROGRESS/41 §2): the role model, the role → permission map, and the
4
+ // action-permission catalog — the three plain-TS modules the browser
5
+ // bundle, the node server and the Cloudflare Worker all import.
6
+ //
7
+ // ISOMORPHIC: no Vue, no node built-ins, no side effects.
8
+ // ═══════════════════════════════════════════════════════════════════
9
+
10
+ export * from './roles'
11
+ export * from './rbac'
12
+ export * from './permissions'
@@ -0,0 +1,398 @@
1
+ // ═══════════════════════════════════════════════════════════════════
2
+ // The action-permission catalog (TODO.federation/12).
3
+ //
4
+ // roles.ts gates SECTIONS (which console a role may enter); THIS module
5
+ // gates ACTIONS (what a role may DO once inside): accept an application,
6
+ // issue a dispatch, sign a test report, finalize an evaluation report,
7
+ // issue a certificate, manage users, …
8
+ //
9
+ // The catalog is enumerated ONCE here and referenced from three places:
10
+ //
11
+ // 1. the transition → permission map below (every state-machine edge
12
+ // names the permission(s) its actor must hold — the machines are
13
+ // GENERATED from PRL (primmel-packages/oiml-smart-core/
14
+ // evaluation/state-machines.prl → data/core/evaluation/
15
+ // state-machines.yaml), so the permission annotation lives here in
16
+ // code, keyed by transition identity, NOT in the generated YAML;
17
+ // editing the PRL to carry it would push a platform concern into
18
+ // the program's normative source);
19
+ // 2. the server write gate (browser/server/routes/entities.ts refuses
20
+ // a transition write whose actor lacks its permission — honest 403
21
+ // naming the missing permission);
22
+ // 3. the UI (v-can / usePermissions disables a denied action and
23
+ // explains which roles hold it).
24
+ //
25
+ // This module is PLAIN TypeScript: no Vue, no node built-ins — the
26
+ // browser bundle, the node server and the Cloudflare Worker all import
27
+ // it.
28
+ // ═══════════════════════════════════════════════════════════════════
29
+
30
+ /** The catalog: permission id → a one-line description (the admin UI,
31
+ * the denial hints and docs/deployment/rbac.md all render from this). */
32
+ export const PERMISSION_INFO = {
33
+ // ── the application phase (PD-05) ──
34
+ 'application.submit': { label: 'Submit, resubmit or withdraw an application' },
35
+ 'application.review': { label: 'Review an application (start the review, request changes)' },
36
+ 'application.accept': { label: 'Decide an application (accept / reject)' },
37
+ // ── samples (PD-05 §4.2 custody chain) ──
38
+ 'samples.request': { label: 'Issue the sample request' },
39
+ 'samples.register': { label: 'Register sample receipts and returns' },
40
+ 'samples.manage': { label: 'Manage sample custody (dispatch, recall, return, retain, destroy, dispose)' },
41
+ 'samples.ship': { label: 'Record the shipment of samples (the manufacturer)' },
42
+ 'samples.custody': { label: 'Laboratory sample custody (acknowledge receipt, start and complete testing)' },
43
+ // ── dispatch + the laboratory's work (PD-05 §4.3) ──
44
+ 'dispatch.issue': { label: 'Issue or withdraw a test request (the dispatch)' },
45
+ 'dispatch.respond': { label: 'Accept or decline a test request (the laboratory)' },
46
+ 'run.perform': { label: 'Perform test runs, fill test forms, work test assignments' },
47
+ 'tr.sign': { label: 'Sign a test report (the pinned signature acknowledgment)' },
48
+ 'tr.submit': { label: 'Submit or recall a test report' },
49
+ 'tr.review': { label: 'Review and decide test reports (accept / reject / return)' },
50
+ // ── the evaluation report + the certificate (PD-06/PD-08) ──
51
+ 'er.review': { label: 'Work the evaluation report (open it, record determinations)' },
52
+ 'er.finalize': { label: 'Finalize the evaluation report (the type-evaluation decision)' },
53
+ 'certificate.issue': { label: 'Issue a certificate (create, submit for registration, renew / revise / transfer)' },
54
+ 'certificate.register': { label: 'Register a certificate (the BIML registration act)' },
55
+ 'certificate.manage': { label: 'Manage the certificate lifecycle (suspend, reinstate, withdraw, expire, investigate, clear)' },
56
+ // ── the engagement phase (TODO.federation/02 — inquiry → quotation →
57
+ // agreement → conversion; ISO/IEC 17065 7.2/7.3) ──
58
+ 'engagement.manage': { label: 'Drive the pre-application engagement (issue the quotation, convert to application, decline)' },
59
+ 'engagement.respond': { label: 'Respond to an engagement (sign the agreement, decline, withdraw — the applicant)' },
60
+ // ── the generalized negotiation (TODO.adoption/07 — the engagement
61
+ // module's kinds: ia_applicant rides the engagement.* pair above;
62
+ // ia_tl and tl_applicant bring the laboratory in as the quoting
63
+ // provider, and the dispatch's quote leg lets a laboratory answer a
64
+ // test request with a quotation) ──
65
+ 'negotiation.quote': { label: 'Quote for work (the provider side: issue a quotation on a negotiation or a dispatched test request — the laboratory)' },
66
+ 'negotiation.accept': { label: 'Accept or decline a quotation for work (the requester side: the Issuing Authority on the dispatch quote leg and on ia_tl negotiations)' },
67
+ // ── records mode (TODO.adoption/02 — the Excel bridge): registering an
68
+ // offline-produced evaluation / test report / certificate with honest
69
+ // "registered offline" provenance; the act walks the machines'
70
+ // declared register_offline entry legs, never a fake history ──
71
+ 'records.register': { label: 'Register an offline-produced evaluation, test report or certificate (records mode)' },
72
+ // ── the scheme's organs + operations (B 18 §5–§7) ──
73
+ 'participants.review': { label: 'Assess participant applications (evidence, RC referral and recommendation)' },
74
+ 'participants.decide': { label: 'Decide participant applications (the MC vote, suspension, reinstatement)' },
75
+ 'participants.manage': { label: 'Manage participant declarations and withdrawals' },
76
+ 'operations.manage': { label: 'Run scheme operations (appeals, complaints, misuse cases)' },
77
+ // ── the ANR lifecycle (TODO.adoption/11 — the OIML-CS utilizer page:
78
+ // Utilizers/Associates specify Additional National Requirements with
79
+ // their test procedures; the registry moderates before they go live) ──
80
+ 'anr.declare': { label: 'Declare Additional National Requirements for your country (Utilizer/Associate staff)' },
81
+ 'anr.review': { label: 'Review and decide ANR declarations (the registry moderation)' },
82
+ // ── the payment records (TODO.adoption/09 — money moves outside the
83
+ // platform; the evidential record lives inside: the IA→OIML-CS
84
+ // certificate fee raised at issuance (PD-05 §6.2) and the IA↔TL
85
+ // arrangement invoices; never a payment processor) ──
86
+ 'payment.invoice': { label: 'Raise a payment record (the IA↔TL arrangement invoice; the evidential record only)' },
87
+ 'payment.mark_paid': { label: 'Mark a payment record paid with its settlement reference and evidence' },
88
+ 'markings.manage': { label: 'Manage markings, sealings and calibration records' },
89
+ 'verification.perform': { label: 'Perform verification activities (VIML 2.09/2.12–2.14)' },
90
+ // ── the instrument register (TODO.register/03 — the per-serial
91
+ // registration under a certificate's scope; the executing scope at
92
+ // the instrument level: the scope check refuses out-of-scope
93
+ // declarations with the reason) ──
94
+ 'serial.register': { label: 'Register instrument serial numbers under a certificate your organization holds, and mark their lifecycle (out of service, withdrawn)' },
95
+ // ── the instance itself (TODO.federation) ──
96
+ 'peers.manage': { label: 'Manage federation peers (TODO.federation/04)' },
97
+ 'instance.settings': { label: 'Change the instance settings (TODO.federation/01)' },
98
+ 'users.manage': { label: 'Manage the instance’s users and their role assignments' },
99
+ // ── delegated organization administration (TODO.identity/10) ──
100
+ // The ORG-SCOPED twin of users.manage: the organization administrator
101
+ // manages only their own organization's people (server-enforced on the
102
+ // account's org binding — never a wider slice). BIML creates one org
103
+ // admin per REGISTERED participant org; the org admin does the typing.
104
+ 'org.users.manage': { label: 'Manage the users of your own organization (the delegated org admin)' },
105
+ } as const
106
+
107
+ export type ActionPermission = keyof typeof PERMISSION_INFO
108
+
109
+ /** The enumerated catalog (the integrity test pins map values against it). */
110
+ export const ACTION_PERMISSIONS = Object.keys(PERMISSION_INFO) as ActionPermission[]
111
+
112
+ export function isActionPermission(value: string): value is ActionPermission {
113
+ return value in PERMISSION_INFO
114
+ }
115
+
116
+ // ── The transition → permission map ─────────────────────────────────
117
+ // Keyed by transition identity `${machineEntity}:${action}->${to}` —
118
+ // the (action, to) pair is the walker's disambiguator (an action name
119
+ // alone is not unique across a machine, cf. the certificate's three
120
+ // `withdraw` edges), and the permission is an actor property of the
121
+ // edge, so same-(action,to) edges from different from-states share one
122
+ // entry.
123
+ //
124
+ // The value is a SET: the write passes when the actor holds ANY listed
125
+ // permission. Singletons are the rule; a set appears only where one
126
+ // edge has genuinely different drivers — cascade-ridden edges whose
127
+ // firing transition belongs to different actors (e.g. a form instance's
128
+ // recall rides the laboratory's withdrawal AND the IA's return-to-lab).
129
+
130
+ export function transitionPermissionKey(machineEntity: string, action: string, to: string): string {
131
+ return `${machineEntity}:${action}->${to}`
132
+ }
133
+
134
+ export const TRANSITION_PERMISSIONS: Record<string, readonly ActionPermission[]> = {
135
+ // application (the applicant's acts vs the IA's desk)
136
+ 'application:submit->SUBMITTED': ['application.submit'],
137
+ 'application:ia_requests_samples->SAMPLES_REQUESTED': ['samples.request'],
138
+ 'application:ia_logs_samples->SAMPLES_RECEIVED': ['samples.register'],
139
+ 'application:ia_starts_review->UNDER_REVIEW': ['application.review'],
140
+ 'application:ia_accepts->ACCEPTED': ['application.accept'],
141
+ 'application:ia_rejects->REJECTED': ['application.accept'],
142
+ 'application:ia_requests_changes->CHANGES_REQUESTED': ['application.review'],
143
+ 'application:applicant_resubmits->UNDER_REVIEW': ['application.submit'],
144
+ 'application:applicant_withdraws->WITHDRAWN': ['application.submit'],
145
+ // records mode (TODO.adoption/02): the registered-offline entry — the
146
+ // application's honest hop into ACCEPTED without a fake walked history.
147
+ 'application:register_offline->ACCEPTED': ['records.register'],
148
+
149
+ // test_request (the dispatch; TODO.adoption/07's quote leg: the
150
+ // laboratory's quotation response and the IA's commercial decision on it)
151
+ 'test_request:ia_issues->ISSUED': ['dispatch.issue'],
152
+ 'test_request:lab_accepts->ACCEPTED_BY_LAB': ['dispatch.respond'],
153
+ 'test_request:lab_quotes->QUOTED': ['negotiation.quote'],
154
+ 'test_request:ia_accepts_quotation->ACCEPTED_BY_LAB': ['negotiation.accept'],
155
+ 'test_request:ia_declines_quotation->REJECTED_BY_LAB': ['negotiation.accept'],
156
+ 'test_request:lab_declines->REJECTED_BY_LAB': ['dispatch.respond'],
157
+ 'test_request:lab_starts_testing->IN_PROGRESS': ['run.perform'],
158
+ 'test_request:lab_issues_test_report->COMPLETED': ['tr.submit'],
159
+ 'test_request:ia_cancels->WITHDRAWN': ['dispatch.issue'],
160
+
161
+ // test_report
162
+ 'test_report:lab_submits->SUBMITTED': ['tr.submit'],
163
+ 'test_report:ia_starts_review->UNDER_REVIEW': ['tr.review'],
164
+ 'test_report:ia_accepts->ACCEPTED': ['tr.review'],
165
+ 'test_report:ia_rejects->REJECTED': ['tr.review'],
166
+ 'test_report:lab_withdraws_for_edit->DRAFT': ['tr.submit'],
167
+ 'test_report:ia_returns_to_lab->DRAFT': ['tr.review'],
168
+ // records mode (TODO.adoption/02): the laboratory registers a report it
169
+ // produced offline — enters SUBMITTED, the IA's review stays a real act.
170
+ 'test_report:register_offline->SUBMITTED': ['records.register'],
171
+
172
+ // form_instance (largely cascade-ridden — the driver sets the entry)
173
+ 'form_instance:lab_starts_filling->IN_PROGRESS': ['run.perform'],
174
+ 'form_instance:lab_submits_with_report->SUBMITTED': ['run.perform'],
175
+ 'form_instance:test_report_issued->LOCKED': ['tr.submit'],
176
+ 'form_instance:test_report_recalled->SUBMITTED': ['tr.submit', 'tr.review'],
177
+ 'form_instance:ia_accepts->ACCEPTED': ['tr.review'],
178
+ 'form_instance:ia_rejects->REJECTED': ['tr.review'],
179
+ 'form_instance:ia_reopens->LOCKED': ['tr.review'],
180
+ 'form_instance:run_redone->DRAFT': ['run.perform'],
181
+
182
+ // evaluation_report
183
+ 'evaluation_report:ia_opens_first_tr->IN_REVIEW': ['er.review'],
184
+ // Fired by the TR-review service when the last report is determined —
185
+ // the reviewing officer holds tr.review, the evaluation worker er.review.
186
+ 'evaluation_report:last_tr_determined->ALL_TR_REVIEWED': ['tr.review', 'er.review'],
187
+ 'evaluation_report:ia_approves->APPROVED': ['er.finalize'],
188
+ 'evaluation_report:ia_rejects->REJECTED': ['er.finalize'],
189
+ 'evaluation_report:ia_approves_with_conditions->CONDITIONALLY_APPROVED': ['er.finalize'],
190
+ 'evaluation_report:ia_reopens->IN_REVIEW': ['er.finalize'],
191
+ // records mode (TODO.adoption/02): the registered-offline entry — one
192
+ // edge per registered verdict (the walker's action-only lookup needs the
193
+ // distinct names), straight from the machine's initial; each edge's
194
+ // cascade SETS the matching judgment (the D37 doctrine).
195
+ 'evaluation_report:register_offline_approved->APPROVED': ['records.register'],
196
+ 'evaluation_report:register_offline_rejected->REJECTED': ['records.register'],
197
+ 'evaluation_report:register_offline_conditionally_approved->CONDITIONALLY_APPROVED': ['records.register'],
198
+
199
+ // certificate
200
+ 'certificate:submit->PENDING_REGISTRATION': ['certificate.issue'],
201
+ 'certificate:register->ACTIVE': ['certificate.register'],
202
+ 'certificate:expire->EXPIRED': ['certificate.manage'],
203
+ 'certificate:withdraw->WITHDRAWN': ['certificate.manage'],
204
+ 'certificate:suspend->SUSPENDED': ['certificate.manage'],
205
+ 'certificate:reinstate->ACTIVE': ['certificate.manage'],
206
+ 'certificate:investigate->UNDER_INVESTIGATION': ['certificate.manage'],
207
+ 'certificate:clear->ACTIVE': ['certificate.manage'],
208
+ 'certificate:renew->PENDING_REGISTRATION': ['certificate.issue'],
209
+ 'certificate:revise->PENDING_REGISTRATION': ['certificate.issue'],
210
+ 'certificate:transfer_ownership->PENDING_REGISTRATION': ['certificate.issue'],
211
+ // TODO.adoption/05: the non-OIML-CS legs. A certificate under an
212
+ // instance-operated scheme has no BIML registration; the issue act
213
+ // walks it into force, and its renew/revise/transfer conclude at the
214
+ // body (back to ACTIVE).
215
+ 'certificate:issue->ACTIVE': ['certificate.issue'],
216
+ 'certificate:renew->ACTIVE': ['certificate.issue'],
217
+ 'certificate:revise->ACTIVE': ['certificate.issue'],
218
+ 'certificate:transfer_ownership->ACTIVE': ['certificate.issue'],
219
+
220
+ // test_assignment (the laboratory's work items; `omit` rides the IA's
221
+ // dispatch withdrawal cascade)
222
+ 'test_assignment:lab_accepts->ACCEPTED': ['run.perform'],
223
+ 'test_assignment:start->IN_PROGRESS': ['run.perform'],
224
+ 'test_assignment:complete->COMPLETED': ['run.perform'],
225
+ 'test_assignment:invalidate->FAILED': ['run.perform'],
226
+ 'test_assignment:redo->IN_PROGRESS': ['run.perform'],
227
+ 'test_assignment:omit->OMITTED': ['dispatch.issue'],
228
+
229
+ // test_run
230
+ 'test_run:record_evidence->IN_PROGRESS': ['run.perform'],
231
+ 'test_run:complete->COMPLETED': ['run.perform'],
232
+ 'test_run:invalidate->INVALIDATED': ['run.perform'],
233
+ 'test_run:redo->PLANNED': ['run.perform'],
234
+
235
+ // measuring_instrument_sample (the custody chain)
236
+ 'measuring_instrument_sample:manufacturer_ships->IN_TRANSIT': ['samples.ship'],
237
+ 'measuring_instrument_sample:ia_logs_receipt->RECEIVED': ['samples.register'],
238
+ 'measuring_instrument_sample:ia_dispatches_to_lab->IN_TRANSIT': ['samples.manage'],
239
+ 'measuring_instrument_sample:lab_acknowledges_receipt->RECEIVED': ['samples.custody'],
240
+ 'measuring_instrument_sample:lab_starts_test->IN_TESTING': ['samples.custody'],
241
+ 'measuring_instrument_sample:lab_test_complete->TESTING_COMPLETE': ['samples.custody'],
242
+ 'measuring_instrument_sample:ia_recalls->IN_TRANSIT': ['samples.manage'],
243
+ 'measuring_instrument_sample:ia_logs_return->RECEIVED': ['samples.register'],
244
+ 'measuring_instrument_sample:ia_returns_to_manufacturer->RETURNED': ['samples.manage'],
245
+ 'measuring_instrument_sample:ia_retains->RETAINED': ['samples.manage'],
246
+ 'measuring_instrument_sample:ia_destroys->DESTROYED': ['samples.manage'],
247
+ 'measuring_instrument_sample:ia_disposes->DISPOSED': ['samples.manage'],
248
+
249
+ // verification_record + sample_verification (the verification pathways;
250
+ // sample_verification is a PROJECTION machine — no same-named store —
251
+ // its transitions fire inside the trigger engine, never over the wire,
252
+ // but the catalog covers it so every declared edge names a permission)
253
+ 'verification_record:verification_starts->IN_PROGRESS': ['verification.perform'],
254
+ 'verification_record:tests_completed->ASSESSED': ['verification.perform'],
255
+ 'verification_record:conformity_passed->PASSED': ['verification.perform'],
256
+ 'verification_record:conformity_failed->FAILED': ['verification.perform'],
257
+ 'verification_record:marks_applied->MARKED': ['verification.perform'],
258
+ 'sample_verification:initial_verification_passed->VERIFIED': ['verification.perform'],
259
+ 'sample_verification:validity_elapsed->REVERIFICATION_DUE': ['verification.perform'],
260
+ 'sample_verification:invalidating_signal_received->INVALIDATED': ['verification.perform'],
261
+ 'sample_verification:subsequent_verification_passed->VERIFIED': ['verification.perform'],
262
+
263
+ // engagement (TODO.federation/02's funnel, generalized by
264
+ // TODO.adoption/07 into the negotiation primitive: the provider-side
265
+ // edges are driven by the IA desk on ia_applicant (engagement.manage)
266
+ // and by the laboratory on ia_tl / tl_applicant (negotiation.quote);
267
+ // the requester-side edges by the applicant (engagement.respond) and
268
+ // by the Issuing Authority on ia_tl (negotiation.accept). The
269
+ // pending-external legs may be recorded from EITHER party's desk.
270
+ 'engagement:provider_issues_quotation->QUOTED': ['engagement.manage', 'negotiation.quote'],
271
+ 'engagement:agreement_signed->AGREED': ['engagement.manage', 'engagement.respond', 'negotiation.quote', 'negotiation.accept'],
272
+ 'engagement:convert_to_application->CONVERTED': ['engagement.manage'],
273
+ 'engagement:provider_declines->DECLINED': ['engagement.manage', 'negotiation.quote'],
274
+ 'engagement:requester_declines->DECLINED': ['engagement.respond', 'negotiation.accept'],
275
+ 'engagement:requester_withdraws->WITHDRAWN': ['engagement.respond', 'negotiation.accept'],
276
+ 'engagement:park_external->PENDING_EXTERNAL': ['engagement.manage', 'engagement.respond', 'negotiation.quote', 'negotiation.accept'],
277
+ 'engagement:external_agreement_recorded->AGREED': ['engagement.manage', 'engagement.respond', 'negotiation.quote', 'negotiation.accept'],
278
+ 'engagement:external_decline_recorded->DECLINED': ['engagement.manage', 'engagement.respond', 'negotiation.quote', 'negotiation.accept'],
279
+
280
+ // participant_application + participant_declaration (the CS organs)
281
+ 'participant_application:competence_evidence_recorded->ASSESSMENT': ['participants.review'],
282
+ 'participant_application:referred_to_rc->RC_REVIEW': ['participants.review'],
283
+ 'participant_application:rc_recommendation_recorded->MC_VOTE': ['participants.review'],
284
+ 'participant_application:mc_approves->DECLARATION_PENDING': ['participants.decide'],
285
+ 'participant_application:mc_rejects->REJECTED': ['participants.decide'],
286
+ 'participant_application:declaration_signed->ACTIVE': ['participants.manage'],
287
+ 'participant_application:mc_suspends->SUSPENDED': ['participants.decide'],
288
+ 'participant_application:mc_reinstates->ACTIVE': ['participants.decide'],
289
+ 'participant_application:participant_withdraws->WITHDRAWN': ['participants.manage'],
290
+ 'participant_declaration:sign->signed': ['participants.manage'],
291
+ 'participant_declaration:amend->draft': ['participants.manage'],
292
+ 'participant_declaration:suspend->suspended': ['participants.manage'],
293
+ 'participant_declaration:reinstate->signed': ['participants.manage'],
294
+ 'participant_declaration:withdraw->withdrawn': ['participants.manage'],
295
+
296
+ // anr_declaration (TODO.adoption/11 — the Utilizer/Associate declares,
297
+ // the CS registry moderates; amendment/promotion re-enters review)
298
+ 'anr_declaration:submit_for_review->SUBMITTED': ['anr.declare'],
299
+ 'anr_declaration:cs_approves->APPROVED': ['anr.review'],
300
+ 'anr_declaration:cs_rejects->REJECTED': ['anr.review'],
301
+ 'anr_declaration:revise->DRAFT': ['anr.declare'],
302
+ 'anr_declaration:amend->DRAFT': ['anr.declare'],
303
+ 'anr_declaration:withdraw->WITHDRAWN': ['anr.declare'],
304
+
305
+ // payment_record (TODO.adoption/09 — the payer records the settlement;
306
+ // the hub's collection desk holds it too, B 18:2018 §9 b))
307
+ 'payment_record:mark_paid->MARKED_PAID': ['payment.mark_paid'],
308
+
309
+ // appeals / complaints / misuse (the scheme-operations console)
310
+ 'appeal_case:information_circulated->CIRCULATED': ['operations.manage'],
311
+ 'appeal_case:boa_considers->CONSIDERED': ['operations.manage'],
312
+ 'appeal_case:boa_decides->DECIDED': ['operations.manage'],
313
+ 'appeal_case:rule_inadmissible->INADMISSIBLE': ['operations.manage'],
314
+ 'complaint_case:investigation_opened->INVESTIGATION': ['operations.manage'],
315
+ 'complaint_case:complaint_upheld->UPHELD': ['operations.manage'],
316
+ 'complaint_case:complaint_dismissed->DISMISSED': ['operations.manage'],
317
+ 'misuse_case:owner_warned->WARNED': ['operations.manage'],
318
+ 'misuse_case:case_closed->CLOSED': ['operations.manage'],
319
+ 'misuse_case:biml_deregisters->DEREGISTERED': ['operations.manage'],
320
+
321
+ // markings / sealings / calibration records (module B)
322
+ 'marking:marking_affixed->AFFIXED': ['markings.manage'],
323
+ 'marking:marking_inspected->INSPECTED': ['markings.manage'],
324
+ 'marking:marking_superseded->SUPERSEDED': ['markings.manage'],
325
+ 'sealing:sealing_affixed->AFFIXED': ['markings.manage'],
326
+ 'sealing:sealing_inspected->INSPECTED': ['markings.manage'],
327
+ 'sealing:sealing_superseded->SUPERSEDED': ['markings.manage'],
328
+ 'calibration_record:calibration_record_affixed->AFFIXED': ['markings.manage'],
329
+ 'calibration_record:calibration_record_inspected->INSPECTED': ['markings.manage'],
330
+ 'calibration_record:calibration_record_superseded->SUPERSEDED': ['markings.manage'],
331
+ }
332
+
333
+ /** The permissions an edge requires (undefined when the edge is unmapped
334
+ * — the integrity test makes that impossible; the server gate treats it
335
+ * as a map bug and refuses loudly, never silently allows). */
336
+ export function permissionsForTransition(
337
+ machineEntity: string,
338
+ action: string,
339
+ to: string,
340
+ ): readonly ActionPermission[] | undefined {
341
+ return TRANSITION_PERMISSIONS[transitionPermissionKey(machineEntity, action, to)]
342
+ }
343
+
344
+ // ── The store-side views of the map ──────────────────────────────────
345
+
346
+ /** entity store name → machine entity (the snake_case machine map key).
347
+ * Stores absent here have no lifecycle machine — writes to them stay
348
+ * org-gated only (audit events, notifications, reference data). */
349
+ export const STORE_MACHINES: Record<string, string> = {
350
+ applications: 'application',
351
+ engagements: 'engagement',
352
+ testRequests: 'test_request',
353
+ testReports: 'test_report',
354
+ formInstances: 'form_instance',
355
+ evaluationReports: 'evaluation_report',
356
+ certificates: 'certificate',
357
+ testAssignments: 'test_assignment',
358
+ testRuns: 'test_run',
359
+ measuringInstrumentSamples: 'measuring_instrument_sample',
360
+ verificationRecords: 'verification_record',
361
+ participantApplications: 'participant_application',
362
+ participantDeclarations: 'participant_declaration',
363
+ anrDeclarations: 'anr_declaration',
364
+ paymentRecords: 'payment_record',
365
+ appeals: 'appeal_case',
366
+ complaints: 'complaint_case',
367
+ misuseCases: 'misuse_case',
368
+ markings: 'marking',
369
+ sealings: 'sealing',
370
+ calibrationRecords: 'calibration_record',
371
+ }
372
+
373
+ /** CREATION gates: the stores where even creating a row is a named
374
+ * action (a certificate draft IS the issuance act — PD-08). Everywhere
375
+ * else creation stays org-gated data entry (a DRAFT application, a
376
+ * draft dispatch). */
377
+ export const CREATION_PERMISSIONS: Record<string, readonly ActionPermission[]> = {
378
+ certificates: ['certificate.issue'],
379
+ // Declaring an ANR is a named act (TODO.adoption/11): even the DRAFT
380
+ // record carries the declarer's name and country.
381
+ anrDeclarations: ['anr.declare'],
382
+ // Raising a payment record is the invoicing act (TODO.adoption/09); the
383
+ // certificate-fee record rides the issuance act that raises it, so the
384
+ // issuing officer's certificate.issue passes the gate as well.
385
+ paymentRecords: ['payment.invoice', 'certificate.issue'],
386
+ // TODO.adoption/05: defining a scheme is an instance-settings act (the
387
+ // registry rides the deployment's own control surface); a certificate's
388
+ // document list is maintained by the certificate's issuers.
389
+ operatedSchemes: ['instance.settings'],
390
+ certificateDocumentLists: ['certificate.issue'],
391
+ }
392
+
393
+ /** FIELD guards: a field going unset → set is the act (the test
394
+ * report's pinned signature acknowledgment is the SIGNING act — it
395
+ * changes no status, so the transition map never sees it). */
396
+ export const FIELD_GUARD_PERMISSIONS: Record<string, Record<string, readonly ActionPermission[]>> = {
397
+ testReports: { signature_acknowledgment: ['tr.sign'] },
398
+ }