@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.
- package/README.md +92 -0
- package/migrations/0001_init.sql +69 -0
- package/migrations/0002_identity.sql +24 -0
- package/migrations/0003_federation_peers.sql +21 -0
- package/migrations/0003_users_rbac.sql +8 -0
- package/migrations/0004_oidc_op.sql +61 -0
- package/migrations/0005_upstream_providers.sql +34 -0
- package/migrations/0006_op_accounts.sql +28 -0
- package/migrations/0007_org_join_requests.sql +26 -0
- package/migrations/0008_op_client_roles.sql +19 -0
- package/migrations/0009_account_console.sql +32 -0
- package/migrations/0009_sso_states.sql +13 -0
- package/migrations/0010_notify_events.sql +23 -0
- package/migrations/0011_op_launch.sql +18 -0
- package/migrations/0011_org_memberships.sql +62 -0
- package/migrations/0012_notify_subscriptions.sql +57 -0
- package/migrations/0012_strong_auth.sql +109 -0
- package/migrations/0013_org_registry.sql +53 -0
- package/migrations/0014_notify_inbox.sql +34 -0
- package/migrations/0015_certificate_holder_attribution.sql +63 -0
- package/migrations/0016_instrument_registrations.sql +78 -0
- package/package.json +52 -0
- package/src/client-info.ts +25 -0
- package/src/context.ts +31 -0
- package/src/github.ts +284 -0
- package/src/mailer.ts +309 -0
- package/src/oidc.ts +369 -0
- package/src/profile/node.ts +83 -0
- package/src/profile.ts +582 -0
- package/src/rbac/node.ts +42 -0
- package/src/rbac.ts +53 -0
- package/src/session.ts +45 -0
- package/src/store/d1.ts +2850 -0
- package/src/store/sqlite/entities.ts +71 -0
- package/src/store/sqlite/events.ts +82 -0
- package/src/store/sqlite/factors-store.ts +348 -0
- package/src/store/sqlite/notify.ts +247 -0
- package/src/store/sqlite/op-accounts-store.ts +470 -0
- package/src/store/sqlite/op-store.ts +280 -0
- package/src/store/sqlite/schema.sql +745 -0
- package/src/store/sqlite/store.ts +1390 -0
- package/src/store/sqlite/upstream-store.ts +148 -0
- package/src/store/sqlite.ts +1027 -0
- package/src/store.ts +1826 -0
- package/src/vocab/index.ts +12 -0
- package/src/vocab/permissions.ts +398 -0
- package/src/vocab/rbac.ts +281 -0
- 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
|
+
}
|