@oimlsmart/platform-server 0.2.7 → 0.2.9
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 +35 -6
- package/src/store/sqlite/entities.ts +35 -7
- package/src/store/sqlite.ts +5 -1
- package/src/store.ts +47 -0
- package/src/vocab/permissions.ts +84 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oimlsmart/platform-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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
|
@@ -32,6 +32,7 @@ import type { D1Database, D1PreparedStatement } from '@cloudflare/workers-types'
|
|
|
32
32
|
import {
|
|
33
33
|
DEMO_PASSWORD,
|
|
34
34
|
EVENTS_BULK_KEY_CHUNK,
|
|
35
|
+
PUT_ENTITIES_CHUNK,
|
|
35
36
|
StoreUnavailable,
|
|
36
37
|
type AccountEmail,
|
|
37
38
|
type AddAccountEmailResult,
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
type EnrollmentToken,
|
|
45
46
|
type EntityChange,
|
|
46
47
|
type EntityRow,
|
|
48
|
+
type EntityWriteInput,
|
|
47
49
|
type EventEntityKey,
|
|
48
50
|
type EventKeyFilter,
|
|
49
51
|
type FederationPeer,
|
|
@@ -413,6 +415,14 @@ function boundedD1Writes(binding: D1Database, budgetMs: number): D1Database {
|
|
|
413
415
|
}
|
|
414
416
|
}
|
|
415
417
|
|
|
418
|
+
/** The entity write's two statements, ONE textual source for putEntity
|
|
419
|
+
* and putEntities alike (the multi-row write must land each row
|
|
420
|
+
* byte-identically to the single-row verb — the upsert, then its
|
|
421
|
+
* journal entry). */
|
|
422
|
+
const ENTITY_UPSERT_SQL = `INSERT INTO entities (store, id, org_id, data, updated_at) VALUES (?, ?, ?, ?, datetime('now'))
|
|
423
|
+
ON CONFLICT (store, id) DO UPDATE SET org_id = excluded.org_id, data = excluded.data, updated_at = datetime('now')`
|
|
424
|
+
const ENTITY_CHANGE_SQL = 'INSERT INTO entity_changes (store, type, id) VALUES (?, ?, ?)'
|
|
425
|
+
|
|
416
426
|
export class D1ServerStore implements ServerStore {
|
|
417
427
|
/** The RAW binding — the ensure memos (and d1StoreFor's map) key on
|
|
418
428
|
* it: the facade below is per-instance and would never hit. */
|
|
@@ -3450,15 +3460,34 @@ export class D1ServerStore implements ServerStore {
|
|
|
3450
3460
|
// all-or-nothing, the same atomicity the SQLite path gets from its
|
|
3451
3461
|
// transaction.
|
|
3452
3462
|
await this.db.batch([
|
|
3453
|
-
this.stmt(
|
|
3454
|
-
|
|
3455
|
-
ON CONFLICT (store, id) DO UPDATE SET org_id = excluded.org_id, data = excluded.data, updated_at = datetime('now')`,
|
|
3456
|
-
store, id, orgId, data,
|
|
3457
|
-
),
|
|
3458
|
-
this.stmt('INSERT INTO entity_changes (store, type, id) VALUES (?, ?, ?)', store, 'persist', id),
|
|
3463
|
+
this.stmt(ENTITY_UPSERT_SQL, store, id, orgId, data),
|
|
3464
|
+
this.stmt(ENTITY_CHANGE_SQL, store, 'persist', id),
|
|
3459
3465
|
])
|
|
3460
3466
|
}
|
|
3461
3467
|
|
|
3468
|
+
async putEntities(store: string, rows: readonly EntityWriteInput[]): Promise<void> {
|
|
3469
|
+
// The seam's contract, mechanically: each row lands exactly as
|
|
3470
|
+
// putEntity would land it (the upsert, then its journal entry), the
|
|
3471
|
+
// statements ride each batch in INPUT order, and the chunks issue
|
|
3472
|
+
// SERIALLY — so the journal's seq order IS the input's row order
|
|
3473
|
+
// (the audit-chain property the CSV commit relies on). The chunk is
|
|
3474
|
+
// the atomic unit: a D1 batch lands all-or-nothing; a failed chunk
|
|
3475
|
+
// throws with its rows unlanded, earlier chunks standing, later
|
|
3476
|
+
// chunks never issued.
|
|
3477
|
+
if (rows.length === 0) return
|
|
3478
|
+
for (let i = 0; i < rows.length; i += PUT_ENTITIES_CHUNK) {
|
|
3479
|
+
const chunk = rows.slice(i, i + PUT_ENTITIES_CHUNK)
|
|
3480
|
+
const statements: D1PreparedStatement[] = []
|
|
3481
|
+
for (const row of chunk) {
|
|
3482
|
+
statements.push(
|
|
3483
|
+
this.stmt(ENTITY_UPSERT_SQL, store, row.id, row.orgId, row.data),
|
|
3484
|
+
this.stmt(ENTITY_CHANGE_SQL, store, 'persist', row.id),
|
|
3485
|
+
)
|
|
3486
|
+
}
|
|
3487
|
+
await this.db.batch(statements)
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3462
3491
|
async deleteEntity(store: string, id: string): Promise<boolean> {
|
|
3463
3492
|
const res = await this.stmt('DELETE FROM entities WHERE store = ? AND id = ?', store, id).run()
|
|
3464
3493
|
const gone = (res.meta.changes ?? 0) > 0
|
|
@@ -13,12 +13,20 @@
|
|
|
13
13
|
// ═══════════════════════════════════════════════════════════════════
|
|
14
14
|
|
|
15
15
|
import { getDb } from './store'
|
|
16
|
-
import { orgIdOf } from '../../store'
|
|
16
|
+
import { PUT_ENTITIES_CHUNK, orgIdOf } from '../../store'
|
|
17
17
|
|
|
18
18
|
export type { EntityRow, EntityChange } from '../../store'
|
|
19
19
|
export { ORG_FIELDS, CATALOG_STORES, orgIdOf } from '../../store'
|
|
20
20
|
|
|
21
|
-
import type { EntityRow, EntityChange } from '../../store'
|
|
21
|
+
import type { EntityRow, EntityChange, EntityWriteInput } from '../../store'
|
|
22
|
+
|
|
23
|
+
/** The entity write's two statements, ONE textual source for putEntity
|
|
24
|
+
* and putEntities alike (the multi-row write must land each row
|
|
25
|
+
* byte-identically to the single-row verb — the upsert, then its
|
|
26
|
+
* journal entry). */
|
|
27
|
+
const ENTITY_UPSERT_SQL = `INSERT INTO entities (store, id, org_id, data, updated_at) VALUES (?, ?, ?, ?, datetime('now'))
|
|
28
|
+
ON CONFLICT (store, id) DO UPDATE SET org_id = excluded.org_id, data = excluded.data, updated_at = datetime('now')`
|
|
29
|
+
const ENTITY_CHANGE_SQL = 'INSERT INTO entity_changes (store, type, id) VALUES (?, ?, ?)'
|
|
22
30
|
|
|
23
31
|
export function listEntities(store: string): EntityRow[] {
|
|
24
32
|
// The ORDER BY is the seam's contract (the 0.2.3 pin, the D1 half's
|
|
@@ -40,15 +48,35 @@ export function getEntity(store: string, id: string): EntityRow | undefined {
|
|
|
40
48
|
export function putEntity(store: string, id: string, orgId: string | null, data: string): void {
|
|
41
49
|
const db = getDb()
|
|
42
50
|
const write = db.transaction(() => {
|
|
43
|
-
db.prepare(
|
|
44
|
-
|
|
45
|
-
ON CONFLICT (store, id) DO UPDATE SET org_id = excluded.org_id, data = excluded.data, updated_at = datetime('now')`,
|
|
46
|
-
).run(store, id, orgId, data)
|
|
47
|
-
db.prepare('INSERT INTO entity_changes (store, type, id) VALUES (?, ?, ?)').run(store, 'persist', id)
|
|
51
|
+
db.prepare(ENTITY_UPSERT_SQL).run(store, id, orgId, data)
|
|
52
|
+
db.prepare(ENTITY_CHANGE_SQL).run(store, 'persist', id)
|
|
48
53
|
})
|
|
49
54
|
write()
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/** The multi-row write (the seam's putEntities, the 2026-09-07 audit's
|
|
58
|
+
* J1): each row lands exactly as putEntity would land it, in INPUT
|
|
59
|
+
* order, one transaction per PUT_ENTITIES_CHUNK rows — the chunk is
|
|
60
|
+
* the atomic unit, matching the D1 batch's all-or-nothing; a failed
|
|
61
|
+
* chunk throws with its rows unlanded, earlier chunks standing, later
|
|
62
|
+
* chunks never issued. The chunks run serially, so the journal's seq
|
|
63
|
+
* order IS the input's row order. */
|
|
64
|
+
export function putEntities(store: string, rows: readonly EntityWriteInput[]): void {
|
|
65
|
+
if (rows.length === 0) return
|
|
66
|
+
const db = getDb()
|
|
67
|
+
const upsert = db.prepare(ENTITY_UPSERT_SQL)
|
|
68
|
+
const journal = db.prepare(ENTITY_CHANGE_SQL)
|
|
69
|
+
for (let i = 0; i < rows.length; i += PUT_ENTITIES_CHUNK) {
|
|
70
|
+
const chunk = rows.slice(i, i + PUT_ENTITIES_CHUNK)
|
|
71
|
+
db.transaction(() => {
|
|
72
|
+
for (const row of chunk) {
|
|
73
|
+
upsert.run(store, row.id, row.orgId, row.data)
|
|
74
|
+
journal.run(store, 'persist', row.id)
|
|
75
|
+
}
|
|
76
|
+
})()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
export function deleteEntity(store: string, id: string): boolean {
|
|
53
81
|
const db = getDb()
|
|
54
82
|
let gone = false
|
package/src/store/sqlite.ts
CHANGED
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
latestChangeSeq,
|
|
87
87
|
latestChangeSeqFor,
|
|
88
88
|
listEntities,
|
|
89
|
+
putEntities,
|
|
89
90
|
putEntity,
|
|
90
91
|
} from './sqlite/entities'
|
|
91
92
|
import {
|
|
@@ -186,7 +187,7 @@ import {
|
|
|
186
187
|
updateOpAccount,
|
|
187
188
|
updateUserName,
|
|
188
189
|
} from './sqlite/op-accounts-store'
|
|
189
|
-
import { installStore, type AccountEmail, type AddAccountEmailResult, type AuthUserPayload, type CertificateHolderClaim, type CertificateHolderOrg, type CompleteEmailChangeResult, type CompleteEnrollmentResult, type ConsumeOidcRefreshTokenResult, type EmailChangeToken, type EnrollmentToken, type EntityChange, type EntityRow, type EventEntityKey, type EventKeyFilter, type FederationPeer, type IdentityApproval, type IdentityLink, type IdentityProvider, type InstrumentRegistration, type InstrumentRegistrationLifecycle, type InstrumentRegistrationScopeStatus, type NotifyDelivery, type NotifyDeliveryStatus, type NotifyEntityMute, type NotifyInboxState, type NotifyPreferences, type NotifyRule, type OAuthInitialAssignment, type OidcAccessToken, type OidcAuthorization, type OidcClient, type OidcClientLaunch, type OidcCode, type OidcConsentGrant, type OidcKeyRow, type OidcRefreshToken, type OpAccountErasure, type OpClientRoleAssignment, type OpLiveSession, type OrgJoinRequest, type OrgMembership, type OrgMembershipState, type OrgRegistryContact, type OrgRegistryOrg, type OrgRegistryState, type PersonalAccessToken, type PlatformEvent, type ServerStore, type SessionView, type SsoSignInState, type UserAdminRow, type AdvanceCounterResult, type MfaPending, type RecoveryCodeState, type TotpSecret, type WebauthnChallenge, type WebauthnCredential } from '../store'
|
|
190
|
+
import { installStore, type AccountEmail, type AddAccountEmailResult, type AuthUserPayload, type CertificateHolderClaim, type CertificateHolderOrg, type CompleteEmailChangeResult, type CompleteEnrollmentResult, type ConsumeOidcRefreshTokenResult, type EmailChangeToken, type EnrollmentToken, type EntityChange, type EntityRow, type EntityWriteInput, type EventEntityKey, type EventKeyFilter, type FederationPeer, type IdentityApproval, type IdentityLink, type IdentityProvider, type InstrumentRegistration, type InstrumentRegistrationLifecycle, type InstrumentRegistrationScopeStatus, type NotifyDelivery, type NotifyDeliveryStatus, type NotifyEntityMute, type NotifyInboxState, type NotifyPreferences, type NotifyRule, type OAuthInitialAssignment, type OidcAccessToken, type OidcAuthorization, type OidcClient, type OidcClientLaunch, type OidcCode, type OidcConsentGrant, type OidcKeyRow, type OidcRefreshToken, type OpAccountErasure, type OpClientRoleAssignment, type OpLiveSession, type OrgJoinRequest, type OrgMembership, type OrgMembershipState, type OrgRegistryContact, type OrgRegistryOrg, type OrgRegistryState, type PersonalAccessToken, type PlatformEvent, type ServerStore, type SessionView, type SsoSignInState, type UserAdminRow, type AdvanceCounterResult, type MfaPending, type RecoveryCodeState, type TotpSecret, type WebauthnChallenge, type WebauthnCredential } from '../store'
|
|
190
191
|
import {
|
|
191
192
|
advanceWebauthnCounter,
|
|
192
193
|
consumeMfaPending,
|
|
@@ -1014,6 +1015,9 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
1014
1015
|
async putEntity(store: string, id: string, orgId: string | null, data: string): Promise<void> {
|
|
1015
1016
|
putEntity(store, id, orgId, data)
|
|
1016
1017
|
},
|
|
1018
|
+
async putEntities(store: string, rows: readonly EntityWriteInput[]): Promise<void> {
|
|
1019
|
+
putEntities(store, rows)
|
|
1020
|
+
},
|
|
1017
1021
|
async deleteEntity(store: string, id: string): Promise<boolean> {
|
|
1018
1022
|
return deleteEntity(store, id)
|
|
1019
1023
|
},
|
package/src/store.ts
CHANGED
|
@@ -175,6 +175,26 @@ export interface EntityChange {
|
|
|
175
175
|
at: string
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/** One row of the multi-row write (putEntities): putEntity's (id,
|
|
179
|
+
* orgId, data) legs — the store is the call's, once for the whole
|
|
180
|
+
* batch. */
|
|
181
|
+
export interface EntityWriteInput {
|
|
182
|
+
id: string
|
|
183
|
+
orgId: string | null
|
|
184
|
+
data: string
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The multi-row write's batch chunk, in ROWS (putEntities): each row
|
|
188
|
+
* contributes two statements (the upsert + its journal entry), so a
|
|
189
|
+
* chunk is one db.batch of 2 × PUT_ENTITIES_CHUNK statements — the
|
|
190
|
+
* same conservative order as EVENTS_BULK_KEY_CHUNK. The chunk bounds
|
|
191
|
+
* the ATOMIC unit (a D1 batch is all-or-nothing; the SQLite half's
|
|
192
|
+
* per-chunk transaction matches it) and keeps one batch's latency
|
|
193
|
+
* well inside the write budget at demo-hub latency. Chunks issue
|
|
194
|
+
* SERIALLY, in input order — the journal's seq order IS the input
|
|
195
|
+
* order, and parallel chunks would forfeit it. */
|
|
196
|
+
export const PUT_ENTITIES_CHUNK = 50
|
|
197
|
+
|
|
178
198
|
// ── the platform event store (TODO.notify/01) ────────────────────────
|
|
179
199
|
|
|
180
200
|
/** A notifiable platform event (TODO.notify/00's event model): one row
|
|
@@ -2349,6 +2369,33 @@ export interface ServerStore {
|
|
|
2349
2369
|
listEntities(store: string): Promise<EntityRow[]>
|
|
2350
2370
|
getEntity(store: string, id: string): Promise<EntityRow | undefined>
|
|
2351
2371
|
putEntity(store: string, id: string, orgId: string | null, data: string): Promise<void>
|
|
2372
|
+
/** The MULTI-ROW write (the 2026-09-07 performance audit's J1 — the
|
|
2373
|
+
* CSV registration commit awaited putEntity per imported row, ~10⁴
|
|
2374
|
+
* rows = minutes of serial D1 writes at demo latency): every row
|
|
2375
|
+
* lands exactly as putEntity would land it (the upsert + its
|
|
2376
|
+
* journal 'persist' entry, one journal row per input row, a
|
|
2377
|
+
* duplicate id re-journals and the last write wins), but the rows
|
|
2378
|
+
* ride ONE db.batch per PUT_ENTITIES_CHUNK rows instead of one
|
|
2379
|
+
* round trip per row.
|
|
2380
|
+
*
|
|
2381
|
+
* The ordering contract — a semantic property of the audit chain,
|
|
2382
|
+
* never an implementation detail: the journal's seq order IS the
|
|
2383
|
+
* input's row order. The statements ride each batch in input order
|
|
2384
|
+
* and the chunks issue SERIALLY (parallel chunks would interleave
|
|
2385
|
+
* the seq assignment); a caller parallelizing the call itself
|
|
2386
|
+
* forfeits the contract.
|
|
2387
|
+
*
|
|
2388
|
+
* The failure contract: the chunk is the ATOMIC unit (a D1 batch is
|
|
2389
|
+
* all-or-nothing; the SQLite half wraps each chunk in a
|
|
2390
|
+
* transaction). A failed chunk lands NOTHING of its rows and the
|
|
2391
|
+
* call throws — but earlier chunks' writes STAND, and later chunks
|
|
2392
|
+
* never issue. A caller needing the whole import all-or-nothing
|
|
2393
|
+
* keeps it under one chunk (or compensates above the seam; the
|
|
2394
|
+
* retry re-journals, the same caveat as any putEntity retry — the
|
|
2395
|
+
* bounded-write facade's StoreUnavailable "may have landed"
|
|
2396
|
+
* posture). An empty rows list resolves without issuing a
|
|
2397
|
+
* statement. */
|
|
2398
|
+
putEntities(store: string, rows: readonly EntityWriteInput[]): Promise<void>
|
|
2352
2399
|
deleteEntity(store: string, id: string): Promise<boolean>
|
|
2353
2400
|
changesAfter(seq: number, limit?: number): Promise<EntityChange[]>
|
|
2354
2401
|
/** The GLOBAL journal high-water: the bootstrap snapshot's ETag leg
|
package/src/vocab/permissions.ts
CHANGED
|
@@ -428,3 +428,87 @@ export const CREATION_PERMISSIONS: Record<string, readonly ActionPermission[]> =
|
|
|
428
428
|
export const FIELD_GUARD_PERMISSIONS: Record<string, Record<string, readonly ActionPermission[]>> = {
|
|
429
429
|
testReports: { signature_acknowledgment: ['tr.sign'] },
|
|
430
430
|
}
|
|
431
|
+
|
|
432
|
+
// ── The store-wide write gate (the 2026-09-07 security cone audit's
|
|
433
|
+
// F2) ────────────────────────────────────────────────────────────
|
|
434
|
+
// ORG_FIELDS (store.ts) cones the org-fielded workflow stores and
|
|
435
|
+
// STORE_MACHINES + CREATION/FIELD_GUARD gate the machinated ones — but
|
|
436
|
+
// both leave holes the audit named: a store with NO org fields and NO
|
|
437
|
+
// machine answered a generic PUT/DELETE from ANY authenticated role
|
|
438
|
+
// (viewer included), and a machinated store gated only its status moves
|
|
439
|
+
// — a field update without a status change, or a creation the CREATION
|
|
440
|
+
// map does not name, computed NO requirement at all. The worst
|
|
441
|
+
// amplification: organizations carries the submission_public_keys the
|
|
442
|
+
// intake's signature check trusts.
|
|
443
|
+
//
|
|
444
|
+
// THIS map is the write side's store-class declaration for the stores
|
|
445
|
+
// the org cone cannot carry: EVERY generic write to a listed store
|
|
446
|
+
// (create, in-place update, delete — status change or not) requires the
|
|
447
|
+
// actor to hold ANY of the named permissions, on top of every other
|
|
448
|
+
// gate that fires (the machine's transitions, the creation acts, the
|
|
449
|
+
// field guards). The consumer's entities router composes the classes:
|
|
450
|
+
// org-fielded → the org cone; catalog → the catalog write gate;
|
|
451
|
+
// machinated → the machine's edges; listed here → this gate; NONE of
|
|
452
|
+
// those → the generic write refuses outright (deny-by-default). The
|
|
453
|
+
// gate is uniform over roles: the platform roles pass because their map
|
|
454
|
+
// grants the permission, never because the gate looks away.
|
|
455
|
+
|
|
456
|
+
/** The store-wide write requirements (store → any-of permission set).
|
|
457
|
+
* The assignments follow the app's actual write callers, audited
|
|
458
|
+
* 2026-09-07:
|
|
459
|
+
*
|
|
460
|
+
* - the participants register (PD-03/PD-04/PD-08/PD-09): the Executive
|
|
461
|
+
* Secretary administers (participants.manage), the RC assesses
|
|
462
|
+
* (participants.review), the MC votes (participants.decide);
|
|
463
|
+
* - the scheme-operations records (B 18 §7 — appeals, complaints,
|
|
464
|
+
* misuse, deregistrations) are the operations console's;
|
|
465
|
+
* - the §15.8 registered copies record at the registration act (the
|
|
466
|
+
* IA's local register-with-BIML fallback holds certificate.register)
|
|
467
|
+
* and at the operations console's corrections;
|
|
468
|
+
* - the ANR validation letters record at issuance (the issuing
|
|
469
|
+
* officer's certificate.issue) and at the operations console;
|
|
470
|
+
* - the ANR declarations: the Utilizer/Associate staffer declares
|
|
471
|
+
* (anr.declare), the registry moderates (anr.review) — the DRAFT's
|
|
472
|
+
* in-place edits ride the declarer's permission;
|
|
473
|
+
* - the instrument register's parties (organizations, manufacturers)
|
|
474
|
+
* are instance-settings/registry records — organizations in
|
|
475
|
+
* particular carries submission_public_keys, the intake signature
|
|
476
|
+
* trust root, so its writes are the instance's own administration;
|
|
477
|
+
* the manufacturer org rows are created at application intake (the
|
|
478
|
+
* wizard's quick-create, the IA desk's white-gloves entry);
|
|
479
|
+
* - the per-model determination records are the evaluation desk's
|
|
480
|
+
* (er.review / the TR review that stamps them);
|
|
481
|
+
* - the lab's work records (form instances, verification records, the
|
|
482
|
+
* module-B markings/sealings/calibration records) ride their work
|
|
483
|
+
* permissions; the IA's review desks write form instances on the
|
|
484
|
+
* review/reopen legs;
|
|
485
|
+
* - the certificate's document lists and annexes are the issuers'. */
|
|
486
|
+
export const STORE_WRITE_PERMISSIONS: Record<string, readonly ActionPermission[]> = {
|
|
487
|
+
organizations: ['instance.settings'],
|
|
488
|
+
manufacturers: ['application.submit', 'application.review', 'instance.settings'],
|
|
489
|
+
experts: ['participants.manage'],
|
|
490
|
+
utilizers: ['participants.manage'],
|
|
491
|
+
associates: ['participants.manage'],
|
|
492
|
+
categorySchemes: ['participants.manage'],
|
|
493
|
+
approvalVotes: ['participants.decide'],
|
|
494
|
+
competenceEvidence: ['participants.review', 'participants.manage'],
|
|
495
|
+
participantApplications: ['participants.review', 'participants.manage'],
|
|
496
|
+
participantDeclarations: ['participants.manage'],
|
|
497
|
+
operatedSchemes: ['instance.settings'],
|
|
498
|
+
certificateDocumentLists: ['certificate.issue'],
|
|
499
|
+
certificateAnnexes: ['certificate.issue', 'certificate.manage'],
|
|
500
|
+
registeredCopies: ['certificate.register', 'operations.manage'],
|
|
501
|
+
validationLetters: ['certificate.issue', 'operations.manage'],
|
|
502
|
+
deregistrations: ['operations.manage'],
|
|
503
|
+
appeals: ['operations.manage'],
|
|
504
|
+
complaints: ['operations.manage'],
|
|
505
|
+
misuseCases: ['operations.manage'],
|
|
506
|
+
anrDeclarations: ['anr.declare', 'anr.review'],
|
|
507
|
+
testReportDeterminations: ['tr.review', 'er.review'],
|
|
508
|
+
formInstances: ['run.perform', 'tr.review', 'er.review'],
|
|
509
|
+
verificationRecords: ['verification.perform'],
|
|
510
|
+
certificateRegistrations: ['certificate.register'],
|
|
511
|
+
markings: ['markings.manage'],
|
|
512
|
+
sealings: ['markings.manage'],
|
|
513
|
+
calibrationRecords: ['markings.manage'],
|
|
514
|
+
}
|