@oimlsmart/platform-server 0.2.14 → 0.2.16
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/package.json +1 -1
- package/src/store/d1.ts +54 -1
- package/src/store/sqlite/op-accounts-store.ts +32 -0
- package/src/store/sqlite/upstream-store.ts +18 -0
- package/src/store/sqlite.ts +8 -0
- package/src/store.ts +24 -10
- package/src/vocab/permissions.ts +6 -0
- package/src/vocab/roles.ts +34 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oimlsmart/platform-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
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
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// key on the RAW binding (a rejected chain still evicts itself).
|
|
29
29
|
// ═══════════════════════════════════════════════════════════════════
|
|
30
30
|
|
|
31
|
-
import type { D1Database, D1PreparedStatement } from '@cloudflare/workers-types'
|
|
31
|
+
import type { D1Database, D1PreparedStatement, D1Result } from '@cloudflare/workers-types'
|
|
32
32
|
import {
|
|
33
33
|
APPEND_EVENTS_CHUNK,
|
|
34
34
|
DEMO_PASSWORD,
|
|
@@ -1831,6 +1831,24 @@ export class D1ServerStore implements ServerStore {
|
|
|
1831
1831
|
return res.results.map(D1ServerStore.toIdentityLink)
|
|
1832
1832
|
}
|
|
1833
1833
|
|
|
1834
|
+
/** The bulk list-endpoint variant (identity's TODO.restructure/06):
|
|
1835
|
+
* ONE read for the whole set (the per-row loop's O(rows) D1 round
|
|
1836
|
+
* trips collapsed); every requested id answers, an unknown id as the
|
|
1837
|
+
* empty array; each account's links keep the per-id read's own
|
|
1838
|
+
* (linked_at, provider) order. */
|
|
1839
|
+
async listIdentityLinksBulk(userIds: string[]): Promise<Map<string, IdentityLink[]>> {
|
|
1840
|
+
const answer = new Map<string, IdentityLink[]>(userIds.map(id => [id, []]))
|
|
1841
|
+
if (userIds.length === 0) return answer
|
|
1842
|
+
const placeholders = userIds.map(() => '?').join(',')
|
|
1843
|
+
const res = await this.stmt(`SELECT * FROM identity_links WHERE user_id IN (${placeholders}) ORDER BY linked_at, provider`, ...userIds)
|
|
1844
|
+
.all<Record<string, unknown>>()
|
|
1845
|
+
for (const row of res.results) {
|
|
1846
|
+
const link = D1ServerStore.toIdentityLink(row)
|
|
1847
|
+
answer.get(link.userId)?.push(link)
|
|
1848
|
+
}
|
|
1849
|
+
return answer
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1834
1852
|
async findIdentityLink(provider: string, providerAccountId: string): Promise<IdentityLink | null> {
|
|
1835
1853
|
const row = await this.stmt('SELECT * FROM identity_links WHERE provider = ? AND provider_account_id = ?', provider, providerAccountId)
|
|
1836
1854
|
.first<Record<string, unknown>>()
|
|
@@ -1941,6 +1959,41 @@ export class D1ServerStore implements ServerStore {
|
|
|
1941
1959
|
return { password: (pw?.n ?? 0) > 0, links: links?.n ?? 0, passkeys: passkeys?.n ?? 0 }
|
|
1942
1960
|
}
|
|
1943
1961
|
|
|
1962
|
+
/** The bulk list-endpoint variant (identity's TODO.restructure/06): the
|
|
1963
|
+
* three grouped counts ride ONE batch — a single D1 round trip for
|
|
1964
|
+
* the whole set, never three per account. Every requested id answers;
|
|
1965
|
+
* an absent row reads as zero (the per-id read's posture). */
|
|
1966
|
+
async countSignInMethodsBulk(userIds: string[]): Promise<Map<string, { password: boolean; links: number; passkeys: number }>> {
|
|
1967
|
+
const answer = new Map<string, { password: boolean; links: number; passkeys: number }>(
|
|
1968
|
+
userIds.map(id => [id, { password: false, links: 0, passkeys: 0 }]),
|
|
1969
|
+
)
|
|
1970
|
+
if (userIds.length === 0) return answer
|
|
1971
|
+
const placeholders = userIds.map(() => '?').join(',')
|
|
1972
|
+
const grouped = (table: string) =>
|
|
1973
|
+
`SELECT user_id, COUNT(*) AS n FROM ${table} WHERE user_id IN (${placeholders}) GROUP BY user_id`
|
|
1974
|
+
const [pw, links, passkeys] = await this.db.batch<Record<string, unknown>>([
|
|
1975
|
+
this.stmt(grouped('passwords'), ...userIds),
|
|
1976
|
+
this.stmt(grouped('identity_links'), ...userIds),
|
|
1977
|
+
this.stmt(grouped('webauthn_credentials'), ...userIds),
|
|
1978
|
+
])
|
|
1979
|
+
const tally = (res: D1Result<Record<string, unknown>>): Map<string, number> => {
|
|
1980
|
+
const m = new Map<string, number>()
|
|
1981
|
+
for (const row of res.results) m.set(String(row.user_id), Number(row.n))
|
|
1982
|
+
return m
|
|
1983
|
+
}
|
|
1984
|
+
const pwBy = tally(pw)
|
|
1985
|
+
const linksBy = tally(links)
|
|
1986
|
+
const passkeysBy = tally(passkeys)
|
|
1987
|
+
for (const id of userIds) {
|
|
1988
|
+
answer.set(id, {
|
|
1989
|
+
password: (pwBy.get(id) ?? 0) > 0,
|
|
1990
|
+
links: linksBy.get(id) ?? 0,
|
|
1991
|
+
passkeys: passkeysBy.get(id) ?? 0,
|
|
1992
|
+
})
|
|
1993
|
+
}
|
|
1994
|
+
return answer
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1944
1997
|
async createEnrollmentToken(input: {
|
|
1945
1998
|
token: string
|
|
1946
1999
|
userId: string
|
|
@@ -139,6 +139,38 @@ export function countSignInMethods(userId: string): { password: boolean; links:
|
|
|
139
139
|
return { password: pw.n > 0, links: links.n, passkeys: passkeys.n }
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** The bulk list-endpoint variant (identity's TODO.restructure/06): one
|
|
143
|
+
* grouped read per underlying table — three statements for the whole
|
|
144
|
+
* set, never three per account. Every requested id answers; an absent
|
|
145
|
+
* row reads as zero (the per-id read's posture). */
|
|
146
|
+
export function countSignInMethodsBulk(userIds: string[]): Map<string, { password: boolean; links: number; passkeys: number }> {
|
|
147
|
+
const answer = new Map<string, { password: boolean; links: number; passkeys: number }>(
|
|
148
|
+
userIds.map(id => [id, { password: false, links: 0, passkeys: 0 }]),
|
|
149
|
+
)
|
|
150
|
+
if (userIds.length === 0) return answer
|
|
151
|
+
const db = getDb()
|
|
152
|
+
const placeholders = userIds.map(() => '?').join(',')
|
|
153
|
+
const counts = (table: string): Map<string, number> => {
|
|
154
|
+
const m = new Map<string, number>()
|
|
155
|
+
const rows = db
|
|
156
|
+
.prepare(`SELECT user_id, COUNT(*) AS n FROM ${table} WHERE user_id IN (${placeholders}) GROUP BY user_id`)
|
|
157
|
+
.all(...userIds) as Array<{ user_id: string; n: number }>
|
|
158
|
+
for (const row of rows) m.set(row.user_id, row.n)
|
|
159
|
+
return m
|
|
160
|
+
}
|
|
161
|
+
const pw = counts('passwords')
|
|
162
|
+
const links = counts('identity_links')
|
|
163
|
+
const passkeys = counts('webauthn_credentials')
|
|
164
|
+
for (const id of userIds) {
|
|
165
|
+
answer.set(id, {
|
|
166
|
+
password: (pw.get(id) ?? 0) > 0,
|
|
167
|
+
links: links.get(id) ?? 0,
|
|
168
|
+
passkeys: passkeys.get(id) ?? 0,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
return answer
|
|
172
|
+
}
|
|
173
|
+
|
|
142
174
|
export function createEnrollmentToken(input: {
|
|
143
175
|
token: string
|
|
144
176
|
userId: string
|
|
@@ -120,6 +120,24 @@ export function listIdentityLinks(userId: string): IdentityLink[] {
|
|
|
120
120
|
return rows.map(toIdentityLink)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** The bulk list-endpoint variant (identity's TODO.restructure/06): the
|
|
124
|
+
* same rows for every id, ONE read for the whole set, grouped in
|
|
125
|
+
* memory. The single ORDER BY keeps each account's links in the
|
|
126
|
+
* per-id read's own (linked_at, provider) order. */
|
|
127
|
+
export function listIdentityLinksBulk(userIds: string[]): Map<string, IdentityLink[]> {
|
|
128
|
+
const answer = new Map<string, IdentityLink[]>(userIds.map(id => [id, []]))
|
|
129
|
+
if (userIds.length === 0) return answer
|
|
130
|
+
const placeholders = userIds.map(() => '?').join(',')
|
|
131
|
+
const rows = getDb()
|
|
132
|
+
.prepare(`SELECT * FROM identity_links WHERE user_id IN (${placeholders}) ORDER BY linked_at, provider`)
|
|
133
|
+
.all(...userIds) as Array<Record<string, unknown>>
|
|
134
|
+
for (const row of rows) {
|
|
135
|
+
const link = toIdentityLink(row)
|
|
136
|
+
answer.get(link.userId)?.push(link)
|
|
137
|
+
}
|
|
138
|
+
return answer
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
export function findIdentityLink(provider: string, providerAccountId: string): IdentityLink | null {
|
|
124
142
|
const row = getDb().prepare('SELECT * FROM identity_links WHERE provider = ? AND provider_account_id = ?')
|
|
125
143
|
.get(provider, providerAccountId) as Record<string, unknown> | undefined
|
package/src/store/sqlite.ts
CHANGED
|
@@ -154,6 +154,7 @@ import {
|
|
|
154
154
|
findIdentityLink,
|
|
155
155
|
getIdentityProvider,
|
|
156
156
|
listIdentityLinks,
|
|
157
|
+
listIdentityLinksBulk,
|
|
157
158
|
listIdentityProviders,
|
|
158
159
|
setIdentityProviderEnabled,
|
|
159
160
|
upsertIdentityProvider,
|
|
@@ -163,6 +164,7 @@ import {
|
|
|
163
164
|
completeEmailChange,
|
|
164
165
|
completeEnrollment,
|
|
165
166
|
countSignInMethods,
|
|
167
|
+
countSignInMethodsBulk,
|
|
166
168
|
createEmailChangeToken,
|
|
167
169
|
createEnrollmentToken,
|
|
168
170
|
createOpAccount,
|
|
@@ -759,6 +761,9 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
759
761
|
async listIdentityLinks(userId: string): Promise<IdentityLink[]> {
|
|
760
762
|
return listIdentityLinks(userId)
|
|
761
763
|
},
|
|
764
|
+
async listIdentityLinksBulk(userIds: string[]): Promise<Map<string, IdentityLink[]>> {
|
|
765
|
+
return listIdentityLinksBulk(userIds)
|
|
766
|
+
},
|
|
762
767
|
async findIdentityLink(provider: string, providerAccountId: string): Promise<IdentityLink | null> {
|
|
763
768
|
return findIdentityLink(provider, providerAccountId)
|
|
764
769
|
},
|
|
@@ -792,6 +797,9 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
792
797
|
async countSignInMethods(userId: string): Promise<{ password: boolean; links: number; passkeys: number }> {
|
|
793
798
|
return countSignInMethods(userId)
|
|
794
799
|
},
|
|
800
|
+
async countSignInMethodsBulk(userIds: string[]): Promise<Map<string, { password: boolean; links: number; passkeys: number }>> {
|
|
801
|
+
return countSignInMethodsBulk(userIds)
|
|
802
|
+
},
|
|
795
803
|
async createEnrollmentToken(input: {
|
|
796
804
|
token: string
|
|
797
805
|
userId: string
|
package/src/store.ts
CHANGED
|
@@ -843,7 +843,7 @@ export type AddAccountEmailResult =
|
|
|
843
843
|
| 'present'
|
|
844
844
|
/** Another account holds the address (as its primary or an
|
|
845
845
|
* additional), or it IS this account's primary — an address names at
|
|
846
|
-
* most one account across the
|
|
846
|
+
* most one account across the platform. */
|
|
847
847
|
| 'conflict'
|
|
848
848
|
|
|
849
849
|
// ── strong authentication: the factor registry (TODO.identity-sso/02 + /03)
|
|
@@ -933,7 +933,7 @@ export interface MfaPending {
|
|
|
933
933
|
// ── the personal access tokens (TODO.identity-features/08) ───────────
|
|
934
934
|
// The developer surface: an ACCOUNT-minted credential for programmatic
|
|
935
935
|
// access (the lab CLI, scripts, the agent pipelines). The GitHub
|
|
936
|
-
// fine-grained pattern mapped to the
|
|
936
|
+
// fine-grained pattern mapped to the platform:
|
|
937
937
|
//
|
|
938
938
|
// - the PAT NEVER rides a request directly — it exchanges at the OP's
|
|
939
939
|
// token endpoint (the RFC 8693 grant, subject_token_type
|
|
@@ -961,7 +961,7 @@ export const PAT_ACTION_CLASSES = ['read', 'write', 'admin'] as const
|
|
|
961
961
|
export type PatActionClass = (typeof PAT_ACTION_CLASSES)[number]
|
|
962
962
|
|
|
963
963
|
/** One parsed scope: the service (a registered application-class OIDC
|
|
964
|
-
* client id — the
|
|
964
|
+
* client id — the platform's service registry IS the OP's client
|
|
965
965
|
* registry) × the action class. */
|
|
966
966
|
export interface PatScope {
|
|
967
967
|
service: string
|
|
@@ -1263,7 +1263,7 @@ export interface OrgRegistryContact {
|
|
|
1263
1263
|
* the org claim's value against its own participant registry directly:
|
|
1264
1264
|
* the same string on both sides, so the mapping is identity, never a
|
|
1265
1265
|
* lookup table). `kind` names the participant kind for a participant
|
|
1266
|
-
* org (NULL = a non-participant org — the
|
|
1266
|
+
* org (NULL = a non-participant org — the platform operator's own org, a
|
|
1267
1267
|
* scheme consumer); the program side bounds the assignable per-org
|
|
1268
1268
|
* roles by it. `participantRef` is the OPTIONAL annotation documenting
|
|
1269
1269
|
* which participant record the org mirrors (the link's documentation,
|
|
@@ -1316,7 +1316,7 @@ export interface OrgRegistryOrg {
|
|
|
1316
1316
|
* organization — the OP-minted org id, never an instance-minted one.
|
|
1317
1317
|
* The hub's OWN row (the certificate_holder_orgs table): the registrar's
|
|
1318
1318
|
* act extracts the descriptor the federation registration package carried
|
|
1319
|
-
* (source 'registration'), or the
|
|
1319
|
+
* (source 'registration'), or the register operator's claim confirmation writes
|
|
1320
1320
|
* it for a legacy row (source 'claim'). ONE row per certificate — the
|
|
1321
1321
|
* first attribution wins, a later writer never overwrites silently.
|
|
1322
1322
|
*
|
|
@@ -1330,7 +1330,7 @@ export interface CertificateHolderOrg {
|
|
|
1330
1330
|
orgName: string
|
|
1331
1331
|
source: 'registration' | 'claim'
|
|
1332
1332
|
attributedAt: string
|
|
1333
|
-
/** The registrar / the confirming
|
|
1333
|
+
/** The registrar / the confirming register operator (the actor's name). */
|
|
1334
1334
|
attributedBy: string | null
|
|
1335
1335
|
/** The confirming claim (source 'claim' only). */
|
|
1336
1336
|
claimId: string | null
|
|
@@ -1338,7 +1338,7 @@ export interface CertificateHolderOrg {
|
|
|
1338
1338
|
|
|
1339
1339
|
/** The legacy-row claim act's lifecycle: PENDING (the manufacturer org's
|
|
1340
1340
|
* administrator claimed the row by holder-name match — a claim is a
|
|
1341
|
-
* claim until confirmed) → CONFIRMED (the
|
|
1341
|
+
* claim until confirmed) → CONFIRMED (the register operator's act; the
|
|
1342
1342
|
* attribution row lands) / REFUSED (terminal for THAT claim, with the
|
|
1343
1343
|
* written reason; a fresh claim may follow). */
|
|
1344
1344
|
export type CertificateHolderClaimState = 'pending' | 'confirmed' | 'refused'
|
|
@@ -1893,6 +1893,13 @@ export interface ServerStore {
|
|
|
1893
1893
|
// ── the linked identities (TODO.identity/02's shape, 08's flows) ──
|
|
1894
1894
|
/** The account's linked upstream identities (the account surface). */
|
|
1895
1895
|
listIdentityLinks(userId: string): Promise<IdentityLink[]>
|
|
1896
|
+
/** The bulk list-endpoint variant (identity's TODO.restructure/06):
|
|
1897
|
+
* the same rows as listIdentityLinks for every id, ONE read for the
|
|
1898
|
+
* whole set — the per-row loop's O(rows) store-call disease's
|
|
1899
|
+
* kernel-side answer. Every requested id answers (an unknown id
|
|
1900
|
+
* honestly as the empty array, the per-id read's own posture); an
|
|
1901
|
+
* empty array answers an empty map. */
|
|
1902
|
+
listIdentityLinksBulk(userIds: string[]): Promise<Map<string, IdentityLink[]>>
|
|
1896
1903
|
/** THE match rule's read: resolve (provider, providerAccountId) → the
|
|
1897
1904
|
* link (and thereby the account). NEVER match by email alone. */
|
|
1898
1905
|
findIdentityLink(provider: string, providerAccountId: string): Promise<IdentityLink | null>
|
|
@@ -1938,6 +1945,13 @@ export interface ServerStore {
|
|
|
1938
1945
|
* sign-in method (passwordless), so the at-least-one-way-in guard
|
|
1939
1946
|
* reads it alongside the password and the links. */
|
|
1940
1947
|
countSignInMethods(userId: string): Promise<{ password: boolean; links: number; passkeys: number }>
|
|
1948
|
+
/** The bulk list-endpoint variant (identity's TODO.restructure/06):
|
|
1949
|
+
* the same counts for every id, one grouped read per underlying
|
|
1950
|
+
* table (the D1 leg rides them in ONE batch) instead of three reads
|
|
1951
|
+
* per account. Every requested id answers; an absent row reads as
|
|
1952
|
+
* zero (the per-id read's posture); an empty array answers an empty
|
|
1953
|
+
* map. */
|
|
1954
|
+
countSignInMethodsBulk(userIds: string[]): Promise<Map<string, { password: boolean; links: number; passkeys: number }>>
|
|
1941
1955
|
/** The enrollment links (invite-only). The token arrives from the
|
|
1942
1956
|
* caller (auth/op/accounts.ts's mint); expires_at = now + ttlMs. */
|
|
1943
1957
|
createEnrollmentToken(input: {
|
|
@@ -2411,7 +2425,7 @@ export interface ServerStore {
|
|
|
2411
2425
|
}): Promise<CertificateHolderOrg | null>
|
|
2412
2426
|
getCertificateHolderOrg(certificateId: string): Promise<CertificateHolderOrg | null>
|
|
2413
2427
|
/** The surface's reads: one org's attributions (the manufacturer cone),
|
|
2414
|
-
* or every row (the
|
|
2428
|
+
* or every row (the register-operator cone — no filter). */
|
|
2415
2429
|
listCertificateHolderOrgs(filter?: { orgId?: string }): Promise<CertificateHolderOrg[]>
|
|
2416
2430
|
/** File the legacy-row claim (the manufacturer org admin's act). */
|
|
2417
2431
|
createCertificateHolderClaim(input: {
|
|
@@ -2422,7 +2436,7 @@ export interface ServerStore {
|
|
|
2422
2436
|
claimedBy: string
|
|
2423
2437
|
}): Promise<CertificateHolderClaim>
|
|
2424
2438
|
getCertificateHolderClaim(id: string): Promise<CertificateHolderClaim | null>
|
|
2425
|
-
/** The queues: the
|
|
2439
|
+
/** The queues: the register operator's pending list (state filter), the
|
|
2426
2440
|
* claiming org's own claims (claimantOrgId filter), one certificate's
|
|
2427
2441
|
* claim history (certificateId filter). */
|
|
2428
2442
|
listCertificateHolderClaims(filter?: {
|
|
@@ -2430,7 +2444,7 @@ export interface ServerStore {
|
|
|
2430
2444
|
claimantOrgId?: string
|
|
2431
2445
|
certificateId?: string
|
|
2432
2446
|
}): Promise<CertificateHolderClaim[]>
|
|
2433
|
-
/** The
|
|
2447
|
+
/** The register operator's decision — ATOMIC on 'pending': an
|
|
2434
2448
|
* already-decided claim answers null (a double confirm/refuse loses
|
|
2435
2449
|
* the race honestly). */
|
|
2436
2450
|
decideCertificateHolderClaim(
|
package/src/vocab/permissions.ts
CHANGED
|
@@ -420,6 +420,12 @@ export const CREATION_PERMISSIONS: Record<string, readonly ActionPermission[]> =
|
|
|
420
420
|
// document list is maintained by the certificate's issuers.
|
|
421
421
|
operatedSchemes: ['instance.settings'],
|
|
422
422
|
certificateDocumentLists: ['certificate.issue'],
|
|
423
|
+
// Registering a participant is the scheme operator's named act (the
|
|
424
|
+
// 2026-09-15 direct-registration posture: participation cases are
|
|
425
|
+
// CREATED active by the Secretariat — the B 18:2025 §11 RC/MC ballot
|
|
426
|
+
// pipeline is modelled, not operated). The organ split holds: the MC
|
|
427
|
+
// decides (suspension/reinstatement), the Secretariat administers.
|
|
428
|
+
participantApplications: ['participants.manage'],
|
|
423
429
|
}
|
|
424
430
|
|
|
425
431
|
/** FIELD guards: a field going unset → set is the act (the test
|
package/src/vocab/roles.ts
CHANGED
|
@@ -26,9 +26,9 @@ export const APP_ROLES = [
|
|
|
26
26
|
// own org's people on the identity service (/op/admin/users).
|
|
27
27
|
'org_admin',
|
|
28
28
|
// TODO.adoption/11 — Utilizer/Associate staff: declare Additional
|
|
29
|
-
// National Requirements for their country on the
|
|
30
|
-
//
|
|
31
|
-
//
|
|
29
|
+
// National Requirements for their country on the Member portal (the
|
|
30
|
+
// declaration carries the participant it acts for; the CS registry's
|
|
31
|
+
// approval is the moderation gate).
|
|
32
32
|
'scheme_participant',
|
|
33
33
|
// TODO.adoption/05 — the market-surveillance authority account: the
|
|
34
34
|
// register's authority audience for the schemes that reserve their
|
|
@@ -51,14 +51,20 @@ export function roleHome(role: string | null | undefined): string {
|
|
|
51
51
|
case 'applicant': return '/app/portal'
|
|
52
52
|
case 'ia_officer': return '/app/ia'
|
|
53
53
|
case 'tl_operator': return '/app/lab'
|
|
54
|
-
|
|
54
|
+
// The registration officer's desk: the former /app/biml portal, now a
|
|
55
|
+
// carve-out of the CS console (the six-portal taxonomy).
|
|
56
|
+
case 'biml_officer': return '/app/cs/registration'
|
|
55
57
|
case 'cs_admin': return '/app/cs'
|
|
58
|
+
// The scheme-desk roles land on the /app/cs hub; their working desks
|
|
59
|
+
// (the approval pipeline, the participant registry, the ANR review
|
|
60
|
+
// queue) sit one prefix below it.
|
|
56
61
|
case 'mc_member':
|
|
57
|
-
case 'rc_member': return '/app/cs
|
|
58
|
-
case 'executive_secretary': return '/app/cs
|
|
59
|
-
// TODO.adoption/11 — the Utilizer/Associate staffer lands on the
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
+
case 'rc_member': return '/app/cs'
|
|
63
|
+
case 'executive_secretary': return '/app/cs'
|
|
64
|
+
// TODO.adoption/11 — the Utilizer/Associate staffer lands on the
|
|
65
|
+
// Member portal (the ANR declaration desk moved there from the scheme
|
|
66
|
+
// console).
|
|
67
|
+
case 'scheme_participant': return '/app/member'
|
|
62
68
|
// TODO.adoption/05 — the market-surveillance authority's surface is
|
|
63
69
|
// the register (its read-only authority view).
|
|
64
70
|
case 'market_surveillance': return '/app/register'
|
|
@@ -94,7 +100,11 @@ export interface RoleSectionRule {
|
|
|
94
100
|
* Secretary's surface. Both stay open to cs_admin (scheme operations).
|
|
95
101
|
* TODO.roadmap/45: the scheme-operations console (/app/cs/operations —
|
|
96
102
|
* appeals, complaints, misuse, deregistration) is the Executive
|
|
97
|
-
* Secretary's post-issuance surface.
|
|
103
|
+
* Secretary's post-issuance surface. The registration desk
|
|
104
|
+
* (/app/cs/registration — the former /app/biml portal) is the
|
|
105
|
+
* registration officer's surface. The Member portal (/app/member) is the
|
|
106
|
+
* Utilizer/Associate staffer's surface; the ANR declaration desk moved
|
|
107
|
+
* there from /app/cs/anr (the review queue stays scheme-side).
|
|
98
108
|
*/
|
|
99
109
|
export const ROLE_SECTION_RULES: RoleSectionRule[] = [
|
|
100
110
|
// TODO.federation/02 — the engagement conversion hands off to the
|
|
@@ -117,8 +127,13 @@ export const ROLE_SECTION_RULES: RoleSectionRule[] = [
|
|
|
117
127
|
// TODO.cs-e2e/05a — /app/lab/projects/<id> (the laboratory's scoped TEP
|
|
118
128
|
// context view) rides the /app/lab prefix: the laboratory's own section.
|
|
119
129
|
{ prefix: '/app/lab', roles: ['tl_operator'] },
|
|
130
|
+
// The Member portal: the Utilizer/Associate staffer's surface (the ANR
|
|
131
|
+
// declaration desk moved here from the CS console — the six-portal
|
|
132
|
+
// taxonomy).
|
|
133
|
+
{ prefix: '/app/member', roles: ['scheme_participant'] },
|
|
120
134
|
// TODO.register/02 — the register's owner view: the manufacturer org's
|
|
121
|
-
// own rows, the IA desk's issued cone, the
|
|
135
|
+
// own rows, the IA desk's issued cone, the register operator's all-seeing
|
|
136
|
+
// cone +
|
|
122
137
|
// the holder-claim queue. The module path rule (auth/modules.ts) rides
|
|
123
138
|
// the register module.
|
|
124
139
|
{ prefix: '/app/my-certificates', roles: ['applicant', 'ia_officer', 'case_officer', 'certification_officer', 'signatory', 'biml_officer', 'cs_admin'] },
|
|
@@ -129,10 +144,15 @@ export const ROLE_SECTION_RULES: RoleSectionRule[] = [
|
|
|
129
144
|
{ prefix: '/app/cs/approvals', roles: ['mc_member', 'rc_member', 'executive_secretary', 'cs_admin'] },
|
|
130
145
|
{ prefix: '/app/cs/participants', roles: ['executive_secretary', 'cs_admin'] },
|
|
131
146
|
{ prefix: '/app/cs/operations', roles: ['executive_secretary', 'cs_admin'] },
|
|
132
|
-
// TODO.adoption/11 — the ANR
|
|
133
|
-
//
|
|
147
|
+
// TODO.adoption/11 — the ANR review queue: the scheme side (the
|
|
148
|
+
// Executive Secretary, open to cs_admin) moderates the member-declared
|
|
149
|
+
// declarations; the declaration desk itself moved to the Member portal.
|
|
134
150
|
// More specific than /app/cs, so it must come first (first match wins).
|
|
135
|
-
{ prefix: '/app/cs/anr', roles: ['
|
|
151
|
+
{ prefix: '/app/cs/anr', roles: ['executive_secretary', 'cs_admin'] },
|
|
152
|
+
// The registration desk: the registration officer's surface (the
|
|
153
|
+
// former /app/biml portal), open to cs_admin for scheme operations.
|
|
154
|
+
// More specific than /app/cs, so it must come first (first match wins).
|
|
155
|
+
{ prefix: '/app/cs/registration', roles: ['biml_officer', 'cs_admin'] },
|
|
136
156
|
// TODO.cs-e2e/13.15 — Data management is the workspace's own export/
|
|
137
157
|
// import surface, and the profile indicator's degradation banner names
|
|
138
158
|
// it as the way out for EVERY role; it must stay reachable by every
|