@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,281 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2
|
+
// The role → permission map (TODO.federation/12): which ROLES hold
|
|
3
|
+
// which ACTION PERMISSIONS (auth/permissions.ts) on this instance.
|
|
4
|
+
//
|
|
5
|
+
// The map is PROFILE DATA. A deployment profile (TODO.federation/01)
|
|
6
|
+
// carries an `rbac:` section — `{ <role>: [permissions] }` — that
|
|
7
|
+
// REPLACES the shipped default wholesale (the profile owns its role
|
|
8
|
+
// model; a per-role merge would invite silent drift). fed-01's profile
|
|
9
|
+
// loader passes the section to resolveRolePermissions(); until that
|
|
10
|
+
// lands, the server reads the section through the INSTANCE_PROFILE /
|
|
11
|
+
// INSTANCE_RBAC_JSON carriers (server/rbac-node.ts). The shipped
|
|
12
|
+
// DEFAULT below reproduces the hub's historical behavior exactly: every
|
|
13
|
+
// role keeps the actions it exercises today, and the hub profile ships
|
|
14
|
+
// this map unchanged (the byte-identical acceptance — the existing
|
|
15
|
+
// suite is the proof).
|
|
16
|
+
//
|
|
17
|
+
// The map also declares the NMI split-role vocabulary (B 18: an NMI's
|
|
18
|
+
// officers hold DISTINCT authorities — item 12's e2e): `case_officer`
|
|
19
|
+
// (review + dispatch, never the decision), `certification_officer`
|
|
20
|
+
// (the decision: finalize + issue), `signatory` (the report signature).
|
|
21
|
+
// The hub ships them UNUSED (no hub account holds them); an NMI profile
|
|
22
|
+
// assigns them to its officers and may strip ia_officer down to a
|
|
23
|
+
// reviewer. Declaring the vocabulary in the default map keeps every
|
|
24
|
+
// profile's assignable-role set available to the admin UI and to the
|
|
25
|
+
// users API's validation.
|
|
26
|
+
//
|
|
27
|
+
// OIDC (TODO.federation/10): the identity layer maps claims to roles
|
|
28
|
+
// through rolesFromClaims() below — the documented seam between the
|
|
29
|
+
// items. Item 10 owns the claim SOURCE (which claim key, per the
|
|
30
|
+
// profile's identity section); this module owns the claim→role
|
|
31
|
+
// semantics (never invent a role the map does not know).
|
|
32
|
+
//
|
|
33
|
+
// PLAIN TypeScript — no Vue, no node built-ins (the browser bundle, the
|
|
34
|
+
// node server and the Worker all import it).
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
ACTION_PERMISSIONS,
|
|
39
|
+
isActionPermission,
|
|
40
|
+
type ActionPermission,
|
|
41
|
+
} from './permissions'
|
|
42
|
+
|
|
43
|
+
/** role id → the permissions it holds. */
|
|
44
|
+
export type RolePermissionMap = Record<string, readonly ActionPermission[]>
|
|
45
|
+
|
|
46
|
+
/** The shipped hub default — today's behavior, enumerated. admin and
|
|
47
|
+
* cs_admin hold the whole catalog (today they write everything the org
|
|
48
|
+
* gate passes); the integrity test pins that. */
|
|
49
|
+
export const DEFAULT_ROLE_PERMISSIONS: RolePermissionMap = {
|
|
50
|
+
// The applicant (manufacturer org): its own application, shipments, and
|
|
51
|
+
// its side of the engagement funnel (sign / decline / withdraw).
|
|
52
|
+
// TODO.register/03 — and the instrument register: the manufacturer
|
|
53
|
+
// registers the serial numbers of the instruments it produces under a
|
|
54
|
+
// certificate its organization holds (the scope check applies — an
|
|
55
|
+
// out-of-scope declaration is refused with the reason), and marks
|
|
56
|
+
// their lifecycle.
|
|
57
|
+
applicant: ['application.submit', 'samples.ship', 'engagement.respond', 'serial.register'],
|
|
58
|
+
// The issuing authority's officer: the whole desk end to end (today's
|
|
59
|
+
// ia_officer — review, samples, dispatch, TR review, the evaluation,
|
|
60
|
+
// issuance, the IA-side lifecycle acts certificate-detail exposes, and
|
|
61
|
+
// the LOCAL registration act: with no separate BIML endpoint the IA's
|
|
62
|
+
// "Register with BIML" walks the certificate PENDING_REGISTRATION →
|
|
63
|
+
// ACTIVE itself (the single-instance posture; biml_registration.service).
|
|
64
|
+
// The engagement funnel (TODO.federation/02) is the IA's pre-application
|
|
65
|
+
// desk.
|
|
66
|
+
ia_officer: [
|
|
67
|
+
'application.review', 'application.accept',
|
|
68
|
+
'samples.request', 'samples.register', 'samples.manage',
|
|
69
|
+
'dispatch.issue', 'tr.review', 'er.review', 'er.finalize',
|
|
70
|
+
'certificate.issue', 'certificate.register', 'certificate.manage',
|
|
71
|
+
'engagement.manage',
|
|
72
|
+
// TODO.adoption/02 — records mode: register the offline-produced
|
|
73
|
+
// evaluation chain (the Excel bridge).
|
|
74
|
+
'records.register',
|
|
75
|
+
// TODO.adoption/07 — the IA is the REQUESTER on the dispatch quote
|
|
76
|
+
// leg and on ia_tl negotiations: it accepts or declines the
|
|
77
|
+
// laboratory's quotation.
|
|
78
|
+
'negotiation.accept',
|
|
79
|
+
// TODO.adoption/09 — the payment records: the IA records an incoming
|
|
80
|
+
// IA↔TL arrangement invoice and marks its own settlements paid (the
|
|
81
|
+
// certificate fee's marked-paid act is the IA's).
|
|
82
|
+
'payment.invoice', 'payment.mark_paid',
|
|
83
|
+
],
|
|
84
|
+
// ── the NMI split (the vocabulary every profile may assign) ──
|
|
85
|
+
// Review + dispatch + the engagement desk, never the decision.
|
|
86
|
+
case_officer: [
|
|
87
|
+
'application.review', 'application.accept',
|
|
88
|
+
'samples.request', 'samples.register', 'samples.manage',
|
|
89
|
+
'dispatch.issue', 'tr.review', 'er.review',
|
|
90
|
+
'engagement.manage',
|
|
91
|
+
// TODO.adoption/07 — the case officer works the dispatch quote leg's
|
|
92
|
+
// commercial decision with the officer.
|
|
93
|
+
'negotiation.accept',
|
|
94
|
+
],
|
|
95
|
+
// The decision: finalize the evaluation, issue and manage the
|
|
96
|
+
// certificate (plus the review legs the decision reads) — and the local
|
|
97
|
+
// registration fallback, mirroring the IA's single-instance act.
|
|
98
|
+
certification_officer: [
|
|
99
|
+
'application.review', 'tr.review', 'er.review', 'er.finalize',
|
|
100
|
+
'certificate.issue', 'certificate.register', 'certificate.manage',
|
|
101
|
+
// TODO.adoption/02 — the records-mode registration carries the
|
|
102
|
+
// offline evaluation's decision: the deciding roles hold it.
|
|
103
|
+
'records.register',
|
|
104
|
+
// TODO.adoption/09 — the deciding roles hold the IA's payment-record
|
|
105
|
+
// acts (the certificate fee's marked-paid record among them).
|
|
106
|
+
'payment.invoice', 'payment.mark_paid',
|
|
107
|
+
],
|
|
108
|
+
// The report-signature authority.
|
|
109
|
+
signatory: ['tr.sign'],
|
|
110
|
+
// The laboratory: answer the dispatch, run the tests, sign and submit
|
|
111
|
+
// the report, hold sample custody, run the verification pathways.
|
|
112
|
+
tl_operator: [
|
|
113
|
+
'dispatch.respond', 'run.perform', 'tr.sign', 'tr.submit',
|
|
114
|
+
'samples.custody', 'verification.perform', 'markings.manage',
|
|
115
|
+
// TODO.adoption/02 — records mode: register the offline-produced
|
|
116
|
+
// test report (the laboratory's own records).
|
|
117
|
+
'records.register',
|
|
118
|
+
// TODO.adoption/07 — the laboratory is the quoting PROVIDER: it
|
|
119
|
+
// answers a dispatched test request with a quotation and drives its
|
|
120
|
+
// side of ia_tl / tl_applicant negotiations.
|
|
121
|
+
'negotiation.quote',
|
|
122
|
+
// TODO.adoption/09 — the laboratory invoices the IA (the IA↔TL
|
|
123
|
+
// arrangement record) and uploads its side of the evidence.
|
|
124
|
+
'payment.invoice',
|
|
125
|
+
],
|
|
126
|
+
// The register operator (BIML): registration + the post-registration
|
|
127
|
+
// lifecycle its review console exposes.
|
|
128
|
+
// TODO.adoption/09 — and the collection desk: the hub's own certificate
|
|
129
|
+
// fee records are marked paid as the settlement arrives (B 18:2018 §9 b)
|
|
130
|
+
// names collecting the registration fees a BIML responsibility).
|
|
131
|
+
biml_officer: ['certificate.register', 'certificate.manage', 'payment.mark_paid'],
|
|
132
|
+
// The CS organs (TODO.roadmap/44): the MC votes, the RC recommends,
|
|
133
|
+
// the Executive Secretary administers the registry and operations —
|
|
134
|
+
// INCLUDING the post-issuance certificate acts the scheme-operations
|
|
135
|
+
// console performs (deregistration withdraws the certificate).
|
|
136
|
+
mc_member: ['participants.decide'],
|
|
137
|
+
rc_member: ['participants.review'],
|
|
138
|
+
executive_secretary: ['participants.review', 'participants.manage', 'operations.manage', 'certificate.manage', 'anr.review'],
|
|
139
|
+
// TODO.adoption/11 — the Utilizer/Associate staffer declares ANRs for
|
|
140
|
+
// the participant it acts for; the review stays with the CS registry.
|
|
141
|
+
scheme_participant: ['anr.declare'],
|
|
142
|
+
// TODO.adoption/05 — the market-surveillance authority account: a
|
|
143
|
+
// READ-ONLY register audience. No action permission (the role never
|
|
144
|
+
// acts); the register's per-scheme access tiers show it the full
|
|
145
|
+
// certificate view the public never gets (services/register-tiers.ts).
|
|
146
|
+
market_surveillance: [],
|
|
147
|
+
// Platform operations: the whole catalog (today's behavior — a
|
|
148
|
+
// non-org-bound cs_admin writes anything; keep it explicit).
|
|
149
|
+
cs_admin: [...ACTION_PERMISSIONS],
|
|
150
|
+
admin: [...ACTION_PERMISSIONS],
|
|
151
|
+
// TODO.identity/10 — delegated organization administration: the
|
|
152
|
+
// organization administrator (ONE per registered participant org,
|
|
153
|
+
// created by BIML after verification) manages its own org's people —
|
|
154
|
+
// invites, kind-bounded role assignments, deactivation — through the
|
|
155
|
+
// ORG-SCOPED permission (the users API scopes every read/write to the
|
|
156
|
+
// account's org binding; the role alone never widens the slice). The
|
|
157
|
+
// role carries no workflow authority.
|
|
158
|
+
org_admin: ['org.users.manage'],
|
|
159
|
+
// The read-only account holds no action permission.
|
|
160
|
+
viewer: [],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The assignable role vocabulary of a map (the users API + the admin
|
|
164
|
+
* UI validate assignments against it). */
|
|
165
|
+
export function mapRoles(map: RolePermissionMap): string[] {
|
|
166
|
+
return Object.keys(map)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate + normalize a profile's `rbac:` section into a map. The
|
|
171
|
+
* section REPLACES the default map when present (pass null/undefined
|
|
172
|
+
* for the default). Honest failures: an unknown permission id is a
|
|
173
|
+
* configuration bug and throws naming it; a role with an empty list is
|
|
174
|
+
* allowed (a deliberate no-authority role, cf. viewer).
|
|
175
|
+
*/
|
|
176
|
+
export function resolveRolePermissions(profileRbac?: unknown): RolePermissionMap {
|
|
177
|
+
if (profileRbac === undefined || profileRbac === null) return DEFAULT_ROLE_PERMISSIONS
|
|
178
|
+
if (typeof profileRbac !== 'object' || Array.isArray(profileRbac)) {
|
|
179
|
+
throw new Error('rbac: the profile’s rbac section must be a map of role → permission list')
|
|
180
|
+
}
|
|
181
|
+
const out: RolePermissionMap = {}
|
|
182
|
+
for (const [role, perms] of Object.entries(profileRbac as Record<string, unknown>)) {
|
|
183
|
+
if (!Array.isArray(perms)) {
|
|
184
|
+
throw new Error(`rbac: role '${role}' must list its permissions (an array)`)
|
|
185
|
+
}
|
|
186
|
+
const valid: ActionPermission[] = []
|
|
187
|
+
for (const p of perms) {
|
|
188
|
+
if (typeof p !== 'string' || !isActionPermission(p)) {
|
|
189
|
+
throw new Error(`rbac: role '${role}' names unknown permission '${String(p)}' — the catalog (auth/permissions.ts) is the closed vocabulary`)
|
|
190
|
+
}
|
|
191
|
+
if (!valid.includes(p)) valid.push(p)
|
|
192
|
+
}
|
|
193
|
+
out[role] = valid
|
|
194
|
+
}
|
|
195
|
+
return out
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The user's effective role set: the primary role first, then any
|
|
199
|
+
* additional assigned roles, deduped (the session payload's `roles`
|
|
200
|
+
* carries the full assigned set; `role` stays the section-gating
|
|
201
|
+
* primary). */
|
|
202
|
+
export function effectiveRolesOf(user: { role?: string | null; roles?: readonly string[] | null }): string[] {
|
|
203
|
+
const out: string[] = []
|
|
204
|
+
if (user.role) out.push(user.role)
|
|
205
|
+
for (const r of user.roles ?? []) {
|
|
206
|
+
if (r && !out.includes(r)) out.push(r)
|
|
207
|
+
}
|
|
208
|
+
return out
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The permission set a role list holds under a map (union; unknown
|
|
212
|
+
* roles contribute nothing — honesty lives at assignment time). */
|
|
213
|
+
export function permissionsForRoles(
|
|
214
|
+
roles: readonly string[],
|
|
215
|
+
map: RolePermissionMap = DEFAULT_ROLE_PERMISSIONS,
|
|
216
|
+
): Set<ActionPermission> {
|
|
217
|
+
const out = new Set<ActionPermission>()
|
|
218
|
+
for (const role of roles) {
|
|
219
|
+
for (const p of map[role] ?? []) out.add(p)
|
|
220
|
+
}
|
|
221
|
+
return out
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Whether the role set holds ANY of the listed permissions (a
|
|
225
|
+
* transition's any-of set) — or the one permission when a single id is
|
|
226
|
+
* passed. */
|
|
227
|
+
export function rolesCan(
|
|
228
|
+
roles: readonly string[],
|
|
229
|
+
required: readonly ActionPermission[],
|
|
230
|
+
map: RolePermissionMap = DEFAULT_ROLE_PERMISSIONS,
|
|
231
|
+
): boolean {
|
|
232
|
+
const held = permissionsForRoles(roles, map)
|
|
233
|
+
return required.some(p => held.has(p))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The roles holding a permission under the map (the denial hint's
|
|
237
|
+
* "held by: …" — a disabled action names the roles that CAN do it). */
|
|
238
|
+
export function roleHolders(
|
|
239
|
+
permission: ActionPermission,
|
|
240
|
+
map: RolePermissionMap = DEFAULT_ROLE_PERMISSIONS,
|
|
241
|
+
): string[] {
|
|
242
|
+
return Object.entries(map)
|
|
243
|
+
.filter(([, perms]) => perms.includes(permission))
|
|
244
|
+
.map(([role]) => role)
|
|
245
|
+
.sort()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── The OIDC seam (TODO.federation/10 calls this) ────────────────────
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Map OIDC claims to this instance's roles. Contract with item 10:
|
|
252
|
+
* item 10 owns the claim SOURCE (which claim carries the roles — a
|
|
253
|
+
* profile's identity section names it, default 'roles' then 'groups');
|
|
254
|
+
* THIS function owns the semantics: read the claim(s), accept an array
|
|
255
|
+
* or a space-delimited string (the two OIDC conventions), and keep only
|
|
256
|
+
* roles the instance's map knows — a claim can never invent a role, and
|
|
257
|
+
* an unknown role is dropped, never erroring the login (the approval
|
|
258
|
+
* queue, item 10, handles first-seen users; roles are only ever
|
|
259
|
+
* PROPOSED by claims and land subject to the instance's assignment
|
|
260
|
+
* rules).
|
|
261
|
+
*/
|
|
262
|
+
export function rolesFromClaims(
|
|
263
|
+
claims: Record<string, unknown>,
|
|
264
|
+
map: RolePermissionMap = DEFAULT_ROLE_PERMISSIONS,
|
|
265
|
+
claimKeys: readonly string[] = ['roles', 'groups'],
|
|
266
|
+
): string[] {
|
|
267
|
+
const known = new Set(mapRoles(map))
|
|
268
|
+
const out: string[] = []
|
|
269
|
+
for (const key of claimKeys) {
|
|
270
|
+
const raw = claims[key]
|
|
271
|
+
const values: string[] = Array.isArray(raw)
|
|
272
|
+
? raw.filter((v): v is string => typeof v === 'string')
|
|
273
|
+
: typeof raw === 'string'
|
|
274
|
+
? raw.split(/\s+/)
|
|
275
|
+
: []
|
|
276
|
+
for (const v of values) {
|
|
277
|
+
if (known.has(v) && !out.includes(v)) out.push(v)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return out
|
|
281
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2
|
+
// Role model (TODO.new-paradigm/01) — shared by router guards, nav
|
|
3
|
+
// filtering and the login redirect. Plain module (no Vue) so it can be
|
|
4
|
+
// imported from anywhere without composable context.
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
export const APP_ROLES = [
|
|
8
|
+
'applicant',
|
|
9
|
+
'ia_officer',
|
|
10
|
+
'tl_operator',
|
|
11
|
+
'biml_officer',
|
|
12
|
+
'cs_admin',
|
|
13
|
+
'mc_member',
|
|
14
|
+
'rc_member',
|
|
15
|
+
'executive_secretary',
|
|
16
|
+
'admin',
|
|
17
|
+
'viewer',
|
|
18
|
+
// TODO.federation/12 — the NMI split-role vocabulary (the RBAC default
|
|
19
|
+
// map, auth/rbac.ts, grants them their permission sets; section gating
|
|
20
|
+
// below seats them at the IA console). No hub account holds them.
|
|
21
|
+
'case_officer',
|
|
22
|
+
'certification_officer',
|
|
23
|
+
'signatory',
|
|
24
|
+
// TODO.identity/10 — delegated organization administration: one org
|
|
25
|
+
// admin per registered participant org (created by BIML), managing its
|
|
26
|
+
// own org's people on the identity service (/op/admin/users).
|
|
27
|
+
'org_admin',
|
|
28
|
+
// TODO.adoption/11 — Utilizer/Associate staff: declare Additional
|
|
29
|
+
// National Requirements for their country on the ANR registry console
|
|
30
|
+
// (the declaration carries the participant it acts for; the CS
|
|
31
|
+
// registry's approval is the moderation gate).
|
|
32
|
+
'scheme_participant',
|
|
33
|
+
// TODO.adoption/05 — the market-surveillance authority account: the
|
|
34
|
+
// register's authority audience for the schemes that reserve their
|
|
35
|
+
// full certificate view to the authorities (brief_public /
|
|
36
|
+
// authority_only access tiers). Read-only by construction.
|
|
37
|
+
'market_surveillance',
|
|
38
|
+
] as const
|
|
39
|
+
export type AppRole = (typeof APP_ROLES)[number]
|
|
40
|
+
|
|
41
|
+
/** The IA-desk role family (TODO.federation/12): the roles seated at the
|
|
42
|
+
* IA console, org-bound to their issuing authority — ia_officer and the
|
|
43
|
+
* NMI split roles. Server-side org/catalog legs test the FAMILY (never
|
|
44
|
+
* the literal 'ia_officer') so the split roles inherit the desk's org
|
|
45
|
+
* posture; the ACTION line between them is the RBAC map's. */
|
|
46
|
+
export const IA_DESK_ROLES: readonly string[] = ['ia_officer', 'case_officer', 'certification_officer', 'signatory']
|
|
47
|
+
|
|
48
|
+
/** Home route per role — mismatch redirects land here. */
|
|
49
|
+
export function roleHome(role: string | null | undefined): string {
|
|
50
|
+
switch (role) {
|
|
51
|
+
case 'applicant': return '/app/portal'
|
|
52
|
+
case 'ia_officer': return '/app/ia'
|
|
53
|
+
case 'tl_operator': return '/app/lab'
|
|
54
|
+
case 'biml_officer': return '/app/biml'
|
|
55
|
+
case 'cs_admin': return '/app/cs'
|
|
56
|
+
case 'mc_member':
|
|
57
|
+
case 'rc_member': return '/app/cs/approvals'
|
|
58
|
+
case 'executive_secretary': return '/app/cs/participants'
|
|
59
|
+
// TODO.adoption/11 — the Utilizer/Associate staffer lands on the ANR
|
|
60
|
+
// registry console (the only /app/cs surface the role may enter).
|
|
61
|
+
case 'scheme_participant': return '/app/cs/anr'
|
|
62
|
+
// TODO.adoption/05 — the market-surveillance authority's surface is
|
|
63
|
+
// the register (its read-only authority view).
|
|
64
|
+
case 'market_surveillance': return '/app/register'
|
|
65
|
+
// The NMI split roles work the IA console (TODO.federation/12).
|
|
66
|
+
case 'case_officer':
|
|
67
|
+
case 'certification_officer':
|
|
68
|
+
case 'signatory': return '/app/ia'
|
|
69
|
+
// TODO.identity/10 — the org admin's console lives on the identity
|
|
70
|
+
// service (the account store); the page itself answers honestly on a
|
|
71
|
+
// profile that does not serve organization administration.
|
|
72
|
+
case 'org_admin': return '/op/admin/users'
|
|
73
|
+
default: return '/app'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface RoleSectionRule {
|
|
78
|
+
/** Path prefix (matched exactly or followed by '/'). */
|
|
79
|
+
prefix: string
|
|
80
|
+
/** Roles allowed in the section; 'admin' is always allowed everywhere. */
|
|
81
|
+
roles: AppRole[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Role-scoped app sections. Order matters — first match wins, so more
|
|
86
|
+
* specific prefixes (/app/cs/lab-inbox, /app/cs/approvals) come before
|
|
87
|
+
* their parent (/app/cs).
|
|
88
|
+
* /app/standards/* (References + workflow lists) and /app/library stay
|
|
89
|
+
* accessible to every authenticated role.
|
|
90
|
+
*
|
|
91
|
+
* TODO.roadmap/44 organ roles: the approval-pipeline board
|
|
92
|
+
* (/app/cs/approvals) is the MC/RC working surface; the participant
|
|
93
|
+
* registry + Declaration editor (/app/cs/participants) is the Executive
|
|
94
|
+
* Secretary's surface. Both stay open to cs_admin (scheme operations).
|
|
95
|
+
* TODO.roadmap/45: the scheme-operations console (/app/cs/operations —
|
|
96
|
+
* appeals, complaints, misuse, deregistration) is the Executive
|
|
97
|
+
* Secretary's post-issuance surface.
|
|
98
|
+
*/
|
|
99
|
+
export const ROLE_SECTION_RULES: RoleSectionRule[] = [
|
|
100
|
+
// TODO.federation/02 — the engagement conversion hands off to the
|
|
101
|
+
// application wizard (?engagement=<id>); in the NMI profile the IA
|
|
102
|
+
// officer drives it for the applicant (the white-gloves seam — fed-03
|
|
103
|
+
// completes the on-behalf-of provenance). More specific than
|
|
104
|
+
// /app/portal, so it must come first (first match wins).
|
|
105
|
+
{ prefix: '/app/portal/applications/new', roles: ['applicant', 'ia_officer'] },
|
|
106
|
+
// TODO.cs-e2e/05a — /app/portal/projects/<id> (the applicant's read-only
|
|
107
|
+
// TEP view) rides the /app/portal prefix: the applicant's own section.
|
|
108
|
+
{ prefix: '/app/portal', roles: ['applicant'] },
|
|
109
|
+
// TODO.cs-e2e/02 — the Type Evaluation Project hub: the IA officer's
|
|
110
|
+
// working page, readable by cs_admin (scheme oversight). More specific
|
|
111
|
+
// than /app/ia, so it must come first (first match wins).
|
|
112
|
+
// TODO.federation/12: the NMI split roles (case/certification officer,
|
|
113
|
+
// signatory) sit at the IA console — RBAC (auth/permissions.ts) draws
|
|
114
|
+
// the finer line BETWEEN them inside these pages.
|
|
115
|
+
{ prefix: '/app/ia/projects', roles: ['ia_officer', 'cs_admin', 'case_officer', 'certification_officer', 'signatory'] },
|
|
116
|
+
{ prefix: '/app/ia', roles: ['ia_officer', 'case_officer', 'certification_officer', 'signatory'] },
|
|
117
|
+
// TODO.cs-e2e/05a — /app/lab/projects/<id> (the laboratory's scoped TEP
|
|
118
|
+
// context view) rides the /app/lab prefix: the laboratory's own section.
|
|
119
|
+
{ prefix: '/app/lab', roles: ['tl_operator'] },
|
|
120
|
+
// TODO.register/02 — the register's owner view: the manufacturer org's
|
|
121
|
+
// own rows, the IA desk's issued cone, the estate's all-seeing cone +
|
|
122
|
+
// the holder-claim queue. The module path rule (auth/modules.ts) rides
|
|
123
|
+
// the register module.
|
|
124
|
+
{ prefix: '/app/my-certificates', roles: ['applicant', 'ia_officer', 'case_officer', 'certification_officer', 'signatory', 'biml_officer', 'cs_admin'] },
|
|
125
|
+
// TODO.deploying/10 — the BIML portal: the registry operator's surface
|
|
126
|
+
// (open to cs_admin for scheme operations).
|
|
127
|
+
{ prefix: '/app/biml', roles: ['biml_officer', 'cs_admin'] },
|
|
128
|
+
{ prefix: '/app/cs/lab-inbox', roles: ['tl_operator'] },
|
|
129
|
+
{ prefix: '/app/cs/approvals', roles: ['mc_member', 'rc_member', 'executive_secretary', 'cs_admin'] },
|
|
130
|
+
{ prefix: '/app/cs/participants', roles: ['executive_secretary', 'cs_admin'] },
|
|
131
|
+
{ prefix: '/app/cs/operations', roles: ['executive_secretary', 'cs_admin'] },
|
|
132
|
+
// TODO.adoption/11 — the ANR registry console: the Utilizer/Associate
|
|
133
|
+
// staffer (scheme_participant) declares here; the CS registry moderates.
|
|
134
|
+
// More specific than /app/cs, so it must come first (first match wins).
|
|
135
|
+
{ prefix: '/app/cs/anr', roles: ['scheme_participant', 'executive_secretary', 'cs_admin'] },
|
|
136
|
+
// TODO.cs-e2e/13.15 — Data management is the workspace's own export/
|
|
137
|
+
// import surface, and the profile indicator's degradation banner names
|
|
138
|
+
// it as the way out for EVERY role; it must stay reachable by every
|
|
139
|
+
// signed-in account or the banner would send users to a refused page.
|
|
140
|
+
{ prefix: '/app/cs/data', roles: [...APP_ROLES] },
|
|
141
|
+
{ prefix: '/app/cs', roles: ['cs_admin'] },
|
|
142
|
+
// TODO.roadmap/29 — the v3 runtime surfaces (coverage pages, the
|
|
143
|
+
// live-twin console) are scheme-operator surfaces.
|
|
144
|
+
{ prefix: '/app/coverage', roles: ['cs_admin'] },
|
|
145
|
+
{ prefix: '/app/twin', roles: ['cs_admin'] },
|
|
146
|
+
// TODO.v3 — the twin lab: the scheme operator AND the test laboratory
|
|
147
|
+
// (the guided run's tester is the TL's operator).
|
|
148
|
+
{ prefix: '/app/twin-lab', roles: ['cs_admin', 'tl_operator'] },
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
/** The section rule covering `path`, if any. */
|
|
152
|
+
export function sectionRuleFor(path: string): RoleSectionRule | undefined {
|
|
153
|
+
return ROLE_SECTION_RULES.find(r => path === r.prefix || path.startsWith(r.prefix + '/'))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Whether `role` may enter `path` per ROLE_SECTION_RULES (admin bypasses). */
|
|
157
|
+
export function canAccessPath(role: string | null | undefined, path: string): boolean {
|
|
158
|
+
const rule = sectionRuleFor(path)
|
|
159
|
+
if (!rule) return true
|
|
160
|
+
if (role === 'admin') return true
|
|
161
|
+
return !!role && (rule.roles as string[]).includes(role)
|
|
162
|
+
}
|