@vantageos/cloud-identity 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +254 -1
  3. package/dist/identity-refusal.d.ts +32 -0
  4. package/dist/identity-refusal.d.ts.map +1 -0
  5. package/dist/identity-refusal.js +28 -0
  6. package/dist/identity-refusal.js.map +1 -0
  7. package/dist/index.d.ts +11 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +10 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/namespace-write.d.ts +20 -0
  12. package/dist/namespace-write.d.ts.map +1 -0
  13. package/dist/namespace-write.js +40 -0
  14. package/dist/namespace-write.js.map +1 -0
  15. package/dist/person-principal.d.ts +7 -0
  16. package/dist/person-principal.d.ts.map +1 -1
  17. package/dist/person-principal.js +7 -0
  18. package/dist/person-principal.js.map +1 -1
  19. package/dist/principal-by-id.d.ts +206 -0
  20. package/dist/principal-by-id.d.ts.map +1 -0
  21. package/dist/principal-by-id.js +334 -0
  22. package/dist/principal-by-id.js.map +1 -0
  23. package/dist/role-policy.d.ts +72 -0
  24. package/dist/role-policy.d.ts.map +1 -0
  25. package/dist/role-policy.js +101 -0
  26. package/dist/role-policy.js.map +1 -0
  27. package/dist/scope-filter.d.ts +26 -0
  28. package/dist/scope-filter.d.ts.map +1 -1
  29. package/dist/scope-filter.js +29 -3
  30. package/dist/scope-filter.js.map +1 -1
  31. package/dist/secret-compare-sync.d.ts +29 -0
  32. package/dist/secret-compare-sync.d.ts.map +1 -0
  33. package/dist/secret-compare-sync.js +64 -0
  34. package/dist/secret-compare-sync.js.map +1 -0
  35. package/dist/tenant-membership.d.ts +85 -0
  36. package/dist/tenant-membership.d.ts.map +1 -0
  37. package/dist/tenant-membership.js +113 -0
  38. package/dist/tenant-membership.js.map +1 -0
  39. package/package.json +25 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,116 @@
3
3
  All notable changes to `@vantageos/cloud-identity` are documented here.
4
4
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [0.11.0]
7
+
8
+ **Additive — the acting principal by ID, and the target checked by ID
9
+ (`./principal-by-id`).** An actor and a target are identified by their stored
10
+ IDs; a name is a display label and never selects or authorises a row. Motivating
11
+ incident: an agent of one organisation, calling by name through the service
12
+ account, landed on another organisation's same-named agent's task at ten task
13
+ doors, because admission compared names. No existing function changes behaviour.
14
+
15
+ ### Added
16
+
17
+ - `resolveActingPrincipal(credential, lookups, door?)`: a verified credential
18
+ (agent bearer, person token, or the service account acting as itself or for
19
+ an agent named BY ID within its own organisation) to `{ principalId, orgId,
20
+ kind, viaServiceAccountId? }` read from stored rows, or a typed refusal.
21
+ Strict credential schemas: a credential carrying a name field is refused
22
+ whole. Missing or throwing lookups, misses, malformed, inactive, unstamped
23
+ and wrong-organisation rows, and inactive organisations all refuse.
24
+ - `assertTargetBelongsTo(principal, target, lookups, opts?)` (async):
25
+ compares the target's stored `orgId` (and `ownerId` with `ownerOnly`) with
26
+ the resolved IDs. An unstamped target is refused to every principal, the
27
+ fleet principal included: no right is inferred from an absence, and a fleet
28
+ row carries the operator organisation's ID explicitly.
29
+ - The fleet is the OPERATOR ORGANISATION, decided from data by the adapter
30
+ method `orgKindOf(orgId) => "operator" | "client" | null` on
31
+ `PrincipalLookups` (and `OrgKindLookups` for `assertTargetBelongsTo`). `null`,
32
+ a miss or any other answer is not the fleet; a missing or throwing adapter
33
+ refuses. An operator-organisation row is refused to every client; the
34
+ fleet principal reaches a client organisation's row only with
35
+ `fleetCrossOrg: true` and only when the adapter reports it as `"client"`.
36
+ - RULING 5: membership of the operator organisation is ordinary membership.
37
+ Its agents and persons resolve as `kind: "agent"` / `"person"` of that
38
+ organisation and reach its rows only; cross-organisation reach (`kind:
39
+ "fleet"` + `fleetCrossOrg`) belongs to its service account alone, and
40
+ `reserved-fleet-scope` refuses a claim of that scope (a `kind: "fleet"`
41
+ credential, or `fleetCrossOrg` used by any other principal), never plain
42
+ membership.
43
+ The operator organisation is checked by `organisationById` like any other.
44
+ - `FLEET_SCOPE_ORG_ID` (a reserved `"vantageos:fleet"` constant, merged on
45
+ `main` but never published) is REMOVED before release, with no alias (RULING
46
+ 4). A row stamped with that literal is an unknown organisation and is refused
47
+ to every caller.
48
+ - `actingCredentialSchema`, `principalRowSchema`, `organisationRowSchema` and
49
+ the types `ActingCredential`, `PrincipalRow`, `OrganisationRow`,
50
+ `PrincipalLookups`, `OrgKind`, `OrgKindLookups`, `ActingPrincipalKind`,
51
+ `ActingPrincipal`,
52
+ `ResolveActingPrincipalResult`, `TargetIds`, `AssertTargetOptions`,
53
+ `AssertTargetResult`.
54
+ - New `IdentityRefusalReason` values: `credential-invalid`,
55
+ `principal-lookup-failed`, `principal-not-found`, `principal-record-invalid`,
56
+ `principal-inactive`, `organisation-not-active`, `reserved-fleet-scope`,
57
+ `acting-agent-other-organisation`, `target-unstamped`,
58
+ `target-other-organisation`, `target-owner-mismatch`.
59
+ - New subpath export `./principal-by-id`.
60
+ - A test that reads the exported signatures with the TypeScript checker and
61
+ fails if any identity input of this module is spelled like a name.
62
+
63
+ ### Deprecated
64
+
65
+ - `resolvePersonActingName` (0.9.0). It takes names (`claimedName`,
66
+ `agentCredential.agentName`) as identity input; backend standard R-53
67
+ clause 4 makes it non-conforming for any new door. Use
68
+ `resolveActingPrincipal` and `assertTargetBelongsTo`. Behaviour unchanged;
69
+ removal is a major change, made only after consumers migrate off it.
70
+ - `passesScopeFilter`, `scopeFilterGet` and `scopeFilterList` (the last two
71
+ through the first). A `createdBy` name matched against `fromAllowList`
72
+ selects the row; backend standard R-53 clause 1 forbids it. Use
73
+ `resolveActingPrincipal` and `assertTargetBelongsTo`. Behaviour unchanged;
74
+ they stay exported until consumers migrate off them.
75
+ - The name-input test now covers every callable export of the package root: a
76
+ new export taking a name-like identity input fails. The deprecated legacy
77
+ exceptions are exactly `resolvePersonActingName`, `passesScopeFilter`,
78
+ `scopeFilterGet` and `scopeFilterList`, and the test fails if any of them
79
+ loses its `@deprecated Since 0.11.0` tag. The scan reads a generic input
80
+ through its constraint, so a name field behind `T extends ScopeFilterable`
81
+ is seen. Two 0.10.0 exports that carry a name-like field without selecting a
82
+ row by it (`isPersonActorName`: `name`; `checkPersonCallShape`:
83
+ `actingName`, accepted only to be refused) are pinned in a frozen map that
84
+ can only shrink.
85
+
86
+ ### Migration (required consumer step)
87
+
88
+ - Before switching a door to `assertTargetBelongsTo`, stamp every existing row
89
+ that has no `orgId` with its real organisation ID or with the operator
90
+ organisation's ID, and supply `orgKindOf` reading the stored `orgKind`. Unstamped rows are refused to every caller, so a door
91
+ switched before the backfill stops serving them.
92
+
93
+ ## [0.10.0]
94
+
95
+ **Additive — identity gaps a consumer had are now in the package.** Every
96
+ consumer adapts these; none copies them. No existing function changes behaviour.
97
+
98
+ ### Added
99
+
100
+ - `resolveMinRole` / `assertMinRole`: minimum-role check over an order the
101
+ caller supplies as data. Unknown role, unknown minimum, invalid order refuse.
102
+ - `mapRoleClaim`: claim to role through caller data, with a fallback that must
103
+ be explicit. Absent or unmapped claim without a fallback refuses.
104
+ - `resolveMembership` (injected async lookup; error, miss, inactive, malformed
105
+ and other-organisation all refuse), `isRowInTenant` (strict equality,
106
+ unstamped row refused), `membershipRecordSchema`.
107
+ - `requireOrgAdmin`: the proof is bound to the caller's verified organisation.
108
+ - `assertNamespaceWrite` and the shared boundary rule `namespaceMatchesPrefix`
109
+ (now also used by the read filter, same behaviour).
110
+ - `timingSafeEqualSync` / `assertSecretSync`: synchronous constant-time
111
+ comparison; an unset expected secret matches nothing.
112
+ - `IdentityRefusalError` and the `IdentityRefusal` types.
113
+ - New subpath exports `./identity-refusal`, `./role-policy`,
114
+ `./tenant-membership`, `./namespace-write`, `./secret-compare-sync`.
115
+
6
116
  ## [0.9.1]
7
117
 
8
118
  ### Fixed
package/README.md CHANGED
@@ -174,13 +174,24 @@ await resolveTenant({ session: null }); // throws Error("Unauthenticated: no ses
174
174
  **Row filtering**
175
175
 
176
176
  - `passesScopeFilter(ctx, row)` — true when this row is visible to this
177
- caller.
177
+ caller. **Deprecated since 0.11.0** (see below).
178
178
  - `scopeFilterList(ctx, rows)` — the subset of rows the caller may see.
179
+ **Deprecated since 0.11.0** (see below).
179
180
  - `scopeFilterGet(ctx, row)` — the row, or `null` if the caller may not see it.
181
+ **Deprecated since 0.11.0** (see below).
180
182
  - `isMasterScope(ctx)` / `isWildcardScope(ctx)` — true when the caller holds
181
183
  unrestricted access. Useful for skipping filtering you know is pointless;
182
184
  never as a substitute for it.
183
185
 
186
+ **Deprecated since 0.11.0: `passesScopeFilter`, `scopeFilterGet` and
187
+ `scopeFilterList`.** All three (the last two through the first) admit a row
188
+ when its `createdBy` NAME appears in the caller's `fromAllowList`, so a name
189
+ selects the row, which backend standard R-53 clause 1 forbids. For a target
190
+ that carries an organisation or owner ID, use `resolveActingPrincipal` and
191
+ `assertTargetBelongsTo`, which compare stored IDs (see "The acting principal
192
+ by ID, and the target checked by ID"). Their behaviour is unchanged; they stay
193
+ exported until consumers migrate off them, and are removed in a major release.
194
+
184
195
  Visibility is granted two ways: the row's `createdBy` appears in the caller's
185
196
  `fromAllowList`, or the row's `namespace` sits under one of the caller's
186
197
  `namespaceReadPrefixes`. Prefixes match on a path boundary, so a caller allowed
@@ -410,6 +421,7 @@ const who = await resolvePersonPrincipal(
410
421
  if (!who.ok) return deny(who.refusal);
411
422
 
412
423
  // 2. Under what name: the token's, never the argument's.
424
+ // Deprecated since 0.11.0: a new door uses resolveActingPrincipal instead.
413
425
  const name = resolvePersonActingName({
414
426
  principal: who.principal, claimedName: args.createdBy,
415
427
  agentCredential: presentedAgent, door: "my-tool",
@@ -427,6 +439,12 @@ const mayWrite = resolveWriterRole({
427
439
  - `resolvePersonPrincipal(token, deps, door)` refuses a token that is expired,
428
440
  revoked, not a person's, bound to no organisation, or bound to an inactive or
429
441
  unmapped one. It looks up only the token's own organisation.
442
+ - **Deprecated since 0.11.0:** `resolvePersonActingName`. It takes names
443
+ (`claimedName`, `agentCredential.agentName`) as identity input, which backend
444
+ standard R-53 clause 4 makes non-conforming for any new door. Use
445
+ `resolveActingPrincipal` and `assertTargetBelongsTo` (see "The acting
446
+ principal by ID, and the target checked by ID"). Its behaviour is unchanged;
447
+ it will be removed in a major release once consumers have migrated off it.
430
448
  - `resolvePersonActingName` returns `{ ok: true, actingAs: "person", actor }`
431
449
  for no name or the person's own (`user:<subject>`), and refuses another
432
450
  user's name (`PERSON_ACTS_AS_ITSELF`) and an agent's name without that
@@ -450,6 +468,241 @@ const mayWrite = resolveWriterRole({
450
468
  The module verifies no credential. The token record must come from your own
451
469
  lookup of the bearer the person presented, never from a request body.
452
470
 
471
+ ## Role policy, membership, namespace writes and secrets (0.10.0)
472
+
473
+ Seven primitives that consumers used to copy. Each is pure, takes its policy as
474
+ DATA from the call site, and refuses on every absence: no role, no claim, no
475
+ list, no prefix, no secret and a failing lookup are all refusals, never a
476
+ default grant. Refusals share one shape, `IdentityRefusal` (`code`, `reason`,
477
+ `door`, `detail`), thrown inside an `IdentityRefusalError` by the asserting
478
+ forms. `IdentityRefusalCode` is `RBAC_DENIED` or `CREDENTIAL_REFUSED`;
479
+ `IdentityRefusalReason` is the closed list of reasons. Every function that
480
+ refuses accepts an optional `door` so the refusal names your own entry point.
481
+
482
+ **Minimum role over an order you supply** (`./role-policy`)
483
+
484
+ - `resolveMinRole({ role, minimum, order, door? })` — `order` is your ordered
485
+ list, MOST privileged first (for example `["admin", "editor", "viewer"]`).
486
+ Returns `{ ok: true, role }` or `{ ok: false, refusal }` (`MinRoleInput`,
487
+ `MinRoleResult`). Refuses an empty or duplicated order, a `minimum` outside
488
+ the order, an absent or unknown role, and a role below the minimum. Matching
489
+ is exact.
490
+ - `assertMinRole(input)` — the throwing form; returns the role or throws
491
+ `IdentityRefusalError`.
492
+
493
+ **Role-claim mapping with an explicit fallback** (`./role-policy`)
494
+
495
+ - `mapRoleClaim({ claim, mapping, fallback?, door? })` — maps a verified claim
496
+ (for example `"org:admin"`) to your role through `mapping`, your data
497
+ (`MapRoleClaimInput`, `MapRoleClaimResult`). The package never picks a
498
+ default: an absent or unmapped claim is served only when YOU pass `fallback`
499
+ (use your least-privileged role), and is refused otherwise. A claim that is
500
+ not a string counts as absent; prototype keys are never mapped. The result
501
+ says whether the role was `mapped` or came from the `fallback`.
502
+
503
+ **Membership and tenant visibility** (`./tenant-membership`)
504
+
505
+ - `resolveMembership({ subject, orgId, lookup, door? })` — async. `lookup` is
506
+ your read of one membership (`MembershipRecord`: `active`, optional `role`,
507
+ optional `orgId`). A throwing lookup, a miss, an inactive row, a malformed
508
+ row and a row naming another organisation are all refusals; the lookup's own
509
+ error text is not surfaced. Served result: `{ ok: true, membership }`
510
+ (`ResolvedMembership`, `ResolveMembershipInput`, `ResolveMembershipResult`).
511
+ `membershipRecordSchema` is the zod schema of the record.
512
+ - `isRowInTenant({ rowOrgId, callerOrgId })` — `true` only on strict equality
513
+ of two non-empty ids. An unstamped row is never visible, not even to a caller
514
+ with no organisation.
515
+ - `requireOrgAdmin({ verifiedOrgId, targetOrgId, role, adminRoles, door? })` —
516
+ the organisation-admin proof, bound to the caller's VERIFIED organisation:
517
+ the target must equal it, and the role must be one of `adminRoles` (your
518
+ data; an empty list admits nobody). Throws `IdentityRefusalError`
519
+ (`RequireOrgAdminInput`).
520
+
521
+ **Namespace writes** (`./namespace-write`, `./scope-filter`)
522
+
523
+ - `assertNamespaceWrite(oauthCtx, namespace, door?)` — throws unless the
524
+ caller's `namespaceWritePrefixes` admit the namespace. Master scope passes;
525
+ a non-master with no write prefix is refused; read prefixes never grant a
526
+ write; an empty-string prefix grants nothing.
527
+ - `namespaceMatchesPrefix(namespace, prefix)` — the one path-boundary rule:
528
+ equality, or the prefix followed by `/`. `team/finance-archive` is not inside
529
+ `team/finance`. The read filter and `assertNamespaceWrite` both use it.
530
+
531
+ **Synchronous constant-time comparison** (`./secret-compare-sync`)
532
+
533
+ - `timingSafeEqualSync(a, b)` — strings (UTF-8) or `Uint8Array`, pure
534
+ JavaScript, no early exit on content. It walks the longer length and folds a
535
+ length difference into the result. Any other input type is `false`. The async
536
+ `timingSafeEqual` is unchanged.
537
+ - `assertSecretSync(presented, expected, door?)` — throws `IdentityRefusalError`
538
+ (`CREDENTIAL_REFUSED`) on mismatch. An unset `expected` (undefined, null or
539
+ empty) matches nothing, so a missing configuration never opens a door. The
540
+ refusal carries neither value.
541
+
542
+ ## The acting principal by ID, and the target checked by ID (0.11.0)
543
+
544
+ **Why this exists.** Two organisations each had an agent displayed as `eta`.
545
+ An organisation A agent, calling through the shared service account and naming
546
+ itself `eta`, completed, updated and deleted organisation B's `eta` task at ten
547
+ different task doors. Every door admitted the call by comparing the typed name
548
+ with a name stored on the task. The names were equal; the two agents were not.
549
+ Nothing compared an ID.
550
+
551
+ The rule this module carries: **an actor and a target are identified by their
552
+ stored IDs. A name is a display label; it never selects a row and never
553
+ authorises one.** Resolving the principal and checking the target both happen
554
+ here, so a product never writes its own identity layer.
555
+
556
+ ```js
557
+ import {
558
+ resolveActingPrincipal,
559
+ assertTargetBelongsTo,
560
+ } from "@vantageos/cloud-identity";
561
+
562
+ // The operator org is DATA: the org whose mapping row says orgKind "operator".
563
+ const orgKindOf = async (id) => (await db.orgs.get(id))?.orgKind ?? null;
564
+
565
+ // 1. Who is acting? IDs from the VERIFIED credential, rows read by ID.
566
+ const who = await resolveActingPrincipal(
567
+ { kind: "service", serviceAccountId: "svc_a", actingForAgentId: "agent_a_eta" },
568
+ {
569
+ agentById: (id) => db.agents.get(id), // -> { id, orgId, active }
570
+ serviceAccountById: (id) => db.services.get(id),
571
+ organisationById: (id) => db.orgs.get(id), // -> { id, active }
572
+ orgKindOf, // -> "operator" | "client" | null
573
+ },
574
+ "tasks:complete",
575
+ );
576
+ if (!who.ok) throw toHttpError(who.refusal); // RBAC_DENIED, typed reason
577
+
578
+ // 2. Does the target belong to them? Stored IDs against resolved IDs.
579
+ const task = await db.tasks.get(taskId);
580
+ const may = await assertTargetBelongsTo(
581
+ who.principal,
582
+ { orgId: task.orgId, ownerId: task.ownerId },
583
+ { orgKindOf },
584
+ { ownerOnly: true, door: "tasks:complete" },
585
+ );
586
+ if (!may.ok) throw toHttpError(may.refusal);
587
+ ```
588
+
589
+ Organisation B's `eta` task carries `orgId: "org_b"`; the principal above
590
+ resolved to `orgId: "org_a"`. The target check refuses it with
591
+ `target-other-organisation`, whatever either agent is called.
592
+
593
+ **`resolveActingPrincipal(credential, lookups, door?)`** (`./principal-by-id`)
594
+
595
+ Async. Returns `{ ok: true, principal }` or `{ ok: false, refusal }`
596
+ (`ResolveActingPrincipalResult`). The `principal` (`ActingPrincipal`) is
597
+ `{ principalId, orgId, kind, viaServiceAccountId? }`, where `kind` is
598
+ `"agent" | "person" | "service" | "fleet"` (`ActingPrincipalKind`).
599
+
600
+ `credential` (`ActingCredential`, validated by `actingCredentialSchema`) is one
601
+ of three shapes, every identity field an ID:
602
+
603
+ - `{ kind: "agent", agentId, verifiedOrgId }` — the machine path: an agent's
604
+ own bearer, already verified by you (for example with
605
+ `validatePresentedBearer`). The agent row's stored `orgId` must equal
606
+ `verifiedOrgId`.
607
+ - `{ kind: "person", personId, verifiedOrgId }` — the human path: a person's
608
+ token, already verified by you. The person's row in that organisation is read
609
+ by `(personId, verifiedOrgId)`.
610
+ - `{ kind: "service", serviceAccountId, actingForAgentId? }` — the service
611
+ account. Alone it acts as itself (`kind: "service"`, or `kind: "fleet"` if
612
+ `orgKindOf` reports its organisation as `"operator"`). With `actingForAgentId` it acts for that
613
+ agent, named BY ID and resolved within the service account's OWN
614
+ organisation; the principal is the agent, and `viaServiceAccountId` records
615
+ the carrier.
616
+
617
+ The schemas are strict: a credential carrying any other key (`agentName`,
618
+ `callerOrchestrator`, `name`, `assignedTo`) is refused whole with
619
+ `credential-invalid`, before any lookup runs.
620
+
621
+ `lookups` (`PrincipalLookups`) are your indexed reads by ID: `agentById(id)`,
622
+ `personById(id, orgId)`, `serviceAccountById(id)` returning a `PrincipalRow`
623
+ (`{ id, orgId?, active }`, schema `principalRowSchema`), and
624
+ `organisationById(id)` returning an `OrganisationRow` (`{ id, active }`, schema
625
+ `organisationRowSchema`), and `orgKindOf(orgId)` returning an `OrgKind`
626
+ (`"operator" | "client"`) or `null`. Only the lookups of the presented path
627
+ are called; `orgKindOf` is called on every path.
628
+ Each row must repeat the ID that was asked for, and `active` is required and
629
+ must be `true`.
630
+
631
+ Refused, each with a typed `IdentityRefusal` reason: a credential that does not
632
+ parse; a missing or throwing lookup (its error text is never surfaced); a miss;
633
+ a malformed, inactive or unstamped row; a row of another organisation than its
634
+ credential; an unmapped or inactive organisation; a service account acting for
635
+ an agent of another organisation. Nothing falls back to a default or master
636
+ principal.
637
+
638
+ **`assertTargetBelongsTo(principal, target, lookups, opts?)`** (`./principal-by-id`)
639
+
640
+ Async. Returns `{ ok: true }` or `{ ok: false, refusal }`
641
+ (`AssertTargetResult`). `target` (`TargetIds`) is `{ orgId?, ownerId? }` read
642
+ from the STORED row; `lookups` (`OrgKindLookups`) is `{ orgKindOf }`, the same
643
+ adapter as above, and is required (a missing or throwing adapter refuses with
644
+ `principal-lookup-failed`); `opts` (`AssertTargetOptions`) is
645
+ `{ door?, ownerOnly?, fleetCrossOrg? }`. IDs are compared byte for byte.
646
+
647
+ - The target's `orgId` must equal the principal's `orgId`
648
+ (`target-other-organisation` otherwise).
649
+ - `ownerOnly: true` also requires `ownerId` to equal `principalId`; a target
650
+ with no owner is refused (`target-owner-mismatch`).
651
+ - A target with no `orgId` is refused (`target-unstamped`) to EVERY
652
+ principal, the fleet principal included, with or without `fleetCrossOrg`.
653
+ No right is inferred from an absence: a row with no organisation is never
654
+ treated as a master row.
655
+ - An absent principal, or one with an empty ID, is refused
656
+ (`credential-invalid`).
657
+
658
+ **The fleet is the operator organisation.** The fleet is not a reserved ID: it
659
+ is the organisation your `orgKindOf` adapter reports as `"operator"` (for
660
+ example the organisation whose mapping row carries `orgKind: "operator"`),
661
+ read from your data at run time. The package spells no organisation ID. An
662
+ adapter answering `null`, a miss, or anything other than exactly
663
+ `"operator"` means "not the fleet"; a missing or throwing adapter refuses.
664
+
665
+ **RULING 5: membership of the operator organisation is ordinary membership.**
666
+ Its agents and persons resolve as `kind: "agent"` / `"person"` of that
667
+ organisation and reach its rows only; cross-organisation reach (`kind: "fleet"`
668
+ with `fleetCrossOrg`) belongs to the operator organisation's service account
669
+ alone, and `reserved-fleet-scope` refuses a CLAIM of that scope, never plain
670
+ membership:
671
+
672
+ - an agent or person credential verified for the operator organisation
673
+ resolves as an ordinary principal of it, with no cross-organisation reach;
674
+ only a service account stamped with it resolves to `kind: "fleet"`;
675
+ - a credential carrying `kind: "fleet"` is refused (`reserved-fleet-scope`),
676
+ whoever presents it;
677
+ - any non-fleet principal reaching another organisation's row under
678
+ `fleetCrossOrg: true` is refused (`reserved-fleet-scope`); without it,
679
+ `target-other-organisation`;
680
+ - an operator-organisation row is refused to every client principal, with or
681
+ without `fleetCrossOrg` (`reserved-fleet-scope`);
682
+ - a `kind: "fleet"` principal whose organisation the adapter does not report
683
+ as `"operator"` is refused, and so is a `kind: "service"` principal in the
684
+ operator organisation (its service account is the fleet);
685
+ - the fleet principal reaches a CLIENT organisation's row only when the door
686
+ passes `fleetCrossOrg: true` (a master export) AND the adapter reports that
687
+ organisation as `"client"`. An unknown organisation is refused;
688
+ - the operator organisation is an ordinary stored organisation:
689
+ `organisationById` must return it mapped and active, like any other;
690
+ - a fleet row is a row stamped with the operator organisation's ID explicitly.
691
+ A row with no `orgId` is not a fleet row and is refused to the fleet
692
+ principal too.
693
+
694
+ **Migration (required consumer step).** Before a product switches any door to
695
+ `assertTargetBelongsTo`, it must stamp every existing row that has no `orgId`:
696
+ with its real organisation ID, or with the operator organisation's ID if it
697
+ belongs to the operator's own scope. An unstamped row is refused to every caller,
698
+ including the fleet principal, so a door switched before the backfill stops
699
+ serving those rows. Run the backfill first, confirm that no unstamped row is
700
+ left in the tables the door reads, then switch the door.
701
+
702
+ `viaServiceAccountId` and `door` are not identity; no function in this module
703
+ takes a name as an identity input, and a test reads the exported signatures to
704
+ keep it that way.
705
+
453
706
  ## What this package does not do
454
707
 
455
708
  Worth reading before you rely on it.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Typed refusal shared by the 0.10.0 primitives (role policy, membership,
3
+ * org-admin proof, namespace write, secret comparison) and the 0.11.0
4
+ * principal-by-id resolver and target check.
5
+ *
6
+ * A refusal is a value carrying its own code, reason and door, so a caller
7
+ * branches on content rather than on the mere presence of an exception, and an
8
+ * absence can never be mistaken for a refusal. It carries no credential.
9
+ *
10
+ * Mirrors the shape of `PersonRefusal` / `PersonRefusalError` on purpose: same
11
+ * `code`/`reason`/`door`/`detail` fields, same `ERROR: detail (reason, door)`
12
+ * message form. It is a separate type only because `PersonRefusalCode` is a
13
+ * closed vocabulary for the person path.
14
+ */
15
+ export type IdentityRefusalCode = "RBAC_DENIED" | "CREDENTIAL_REFUSED";
16
+ export type IdentityRefusalReason = "unknown-role" | "unknown-minimum-role" | "invalid-role-order" | "role-below-minimum" | "role-claim-absent" | "role-claim-unmapped" | "invalid-role-mapping" | "membership-invalid-input" | "membership-lookup-failed" | "membership-not-found" | "membership-record-invalid" | "membership-inactive" | "other-organisation" | "no-verified-organisation" | "role-not-admin" | "namespace-invalid" | "no-write-prefixes" | "namespace-write-denied" | "secret-not-configured" | "secret-mismatch" | "credential-invalid" | "principal-lookup-failed" | "principal-not-found" | "principal-record-invalid" | "principal-inactive" | "organisation-not-active" | "reserved-fleet-scope" | "acting-agent-other-organisation" | "target-unstamped" | "target-other-organisation" | "target-owner-mismatch";
17
+ export type IdentityRefusal = {
18
+ code: IdentityRefusalCode;
19
+ reason: IdentityRefusalReason;
20
+ /** The consumer's own name for the entry point that refused. */
21
+ door: string;
22
+ /** One sentence, safe to show to the caller. Never a credential. */
23
+ detail: string;
24
+ };
25
+ /** Thrown by the asserting forms. Carries the typed refusal untouched. */
26
+ export declare class IdentityRefusalError extends Error {
27
+ readonly refusal: IdentityRefusal;
28
+ constructor(refusal: IdentityRefusal);
29
+ }
30
+ /** @internal Build a refusal. Not exported from the package. */
31
+ export declare function refusal(code: IdentityRefusalCode, reason: IdentityRefusalReason, door: string, detail: string): IdentityRefusal;
32
+ //# sourceMappingURL=identity-refusal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-refusal.d.ts","sourceRoot":"","sources":["../src/identity-refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,oBAAoB,CAAC;AAEvE,MAAM,MAAM,qBAAqB,GAE7B,cAAc,GACd,sBAAsB,GACtB,oBAAoB,GACpB,oBAAoB,GACpB,mBAAmB,GACnB,qBAAqB,GACrB,sBAAsB,GAEtB,0BAA0B,GAC1B,0BAA0B,GAC1B,sBAAsB,GACtB,2BAA2B,GAC3B,qBAAqB,GACrB,oBAAoB,GAEpB,0BAA0B,GAC1B,gBAAgB,GAEhB,mBAAmB,GACnB,mBAAmB,GACnB,wBAAwB,GAExB,uBAAuB,GACvB,iBAAiB,GAEjB,oBAAoB,GACpB,yBAAyB,GACzB,qBAAqB,GACrB,0BAA0B,GAC1B,oBAAoB,GACpB,yBAAyB,GACzB,sBAAsB,GACtB,iCAAiC,GACjC,kBAAkB,GAClB,2BAA2B,GAC3B,uBAAuB,CAAC;AAE5B,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,qBAAqB,CAAC;IAC9B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,0EAA0E;AAC1E,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;gBACtB,OAAO,EAAE,eAAe;CAKrC;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CACrB,IAAI,EAAE,mBAAmB,EACzB,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,eAAe,CAEjB"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Typed refusal shared by the 0.10.0 primitives (role policy, membership,
3
+ * org-admin proof, namespace write, secret comparison) and the 0.11.0
4
+ * principal-by-id resolver and target check.
5
+ *
6
+ * A refusal is a value carrying its own code, reason and door, so a caller
7
+ * branches on content rather than on the mere presence of an exception, and an
8
+ * absence can never be mistaken for a refusal. It carries no credential.
9
+ *
10
+ * Mirrors the shape of `PersonRefusal` / `PersonRefusalError` on purpose: same
11
+ * `code`/`reason`/`door`/`detail` fields, same `ERROR: detail (reason, door)`
12
+ * message form. It is a separate type only because `PersonRefusalCode` is a
13
+ * closed vocabulary for the person path.
14
+ */
15
+ /** Thrown by the asserting forms. Carries the typed refusal untouched. */
16
+ export class IdentityRefusalError extends Error {
17
+ refusal;
18
+ constructor(refusal) {
19
+ super(`${refusal.code}: ${refusal.detail} (${refusal.reason}, ${refusal.door})`);
20
+ this.name = "IdentityRefusalError";
21
+ this.refusal = refusal;
22
+ }
23
+ }
24
+ /** @internal Build a refusal. Not exported from the package. */
25
+ export function refusal(code, reason, door, detail) {
26
+ return { code, reason, door, detail };
27
+ }
28
+ //# sourceMappingURL=identity-refusal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-refusal.js","sourceRoot":"","sources":["../src/identity-refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAoDH,0EAA0E;AAC1E,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IACpC,OAAO,CAAkB;IAClC,YAAY,OAAwB;QAClC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;QACjF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED,gEAAgE;AAChE,MAAM,UAAU,OAAO,CACrB,IAAyB,EACzB,MAA6B,EAC7B,IAAY,EACZ,MAAc;IAEd,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACxC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * validation.
7
7
  */
8
8
  export { timingSafeEqual, sha256Hex } from "./crypto.js";
9
- export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, LEGACY_WILDCARD_CTX, type ScopeFilterable, } from "./scope-filter.js";
9
+ export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, namespaceMatchesPrefix, LEGACY_WILDCARD_CTX, type ScopeFilterable, } from "./scope-filter.js";
10
10
  export { validateMasterBearer } from "./bearer-validation.js";
11
11
  export type { OAuthCtx, ScopeProfile, NamespacePrefix, FromAllowListEntry, ValidateMasterBearerResult, } from "./types.js";
12
12
  export { workspaceSchema, workspaceMemberSchema, workspaceRoleSchema, tenantContextSchema, ScopeViolationError, getEffectiveTenantId, decodeUnverifiedBearer, } from "./tenancy-domain.js";
@@ -29,4 +29,14 @@ export { oauthErrorFor, pkceChallengeFromVerifier } from "./authorize-shared.js"
29
29
  export type { AuthorizeRefusal, AuthorizeRefusalReason, } from "./authorize-shared.js";
30
30
  export { PERSON_ACTOR_PREFIX, PersonRefusalError, personActorName, isPersonActorName, personTokenRecordSchema, resolvePersonPrincipal, resolvePersonActingName, checkPersonCallShape, resolvePersonTenantAccess, resolveWriterRole, requireWriterRole, } from "./person-principal.js";
31
31
  export type { PersonRefusal, PersonRefusalCode, PersonRefusalReason, PersonTokenRecord, PersonPrincipal, OrganisationState, PersonPrincipalDeps, PersonPrincipalResult, PersonActingNameResult, PersonAccessResult, } from "./person-principal.js";
32
+ export { IdentityRefusalError } from "./identity-refusal.js";
33
+ export type { IdentityRefusal, IdentityRefusalCode, IdentityRefusalReason, } from "./identity-refusal.js";
34
+ export { resolveMinRole, assertMinRole, mapRoleClaim } from "./role-policy.js";
35
+ export type { MinRoleInput, MinRoleResult, MapRoleClaimInput, MapRoleClaimResult, } from "./role-policy.js";
36
+ export { isRowInTenant, resolveMembership, requireOrgAdmin, membershipRecordSchema, } from "./tenant-membership.js";
37
+ export type { MembershipRecord, ResolvedMembership, ResolveMembershipInput, ResolveMembershipResult, RequireOrgAdminInput, } from "./tenant-membership.js";
38
+ export { assertNamespaceWrite } from "./namespace-write.js";
39
+ export { timingSafeEqualSync, assertSecretSync } from "./secret-compare-sync.js";
40
+ export { actingCredentialSchema, principalRowSchema, organisationRowSchema, resolveActingPrincipal, assertTargetBelongsTo, } from "./principal-by-id.js";
41
+ export type { ActingCredential, PrincipalRow, OrganisationRow, PrincipalLookups, OrgKind, OrgKindLookups, ActingPrincipalKind, ActingPrincipal, ResolveActingPrincipalResult, TargetIds, AssertTargetOptions, AssertTargetResult, } from "./principal-by-id.js";
32
42
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * validation.
7
7
  */
8
8
  export { timingSafeEqual, sha256Hex } from "./crypto.js";
9
- export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, LEGACY_WILDCARD_CTX, } from "./scope-filter.js";
9
+ export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, namespaceMatchesPrefix, LEGACY_WILDCARD_CTX, } from "./scope-filter.js";
10
10
  export { validateMasterBearer } from "./bearer-validation.js";
11
11
  // 0.2.0 — Domain-tenancy layer (additive)
12
12
  export { workspaceSchema, workspaceMemberSchema, workspaceRoleSchema, tenantContextSchema, ScopeViolationError, getEffectiveTenantId, decodeUnverifiedBearer, } from "./tenancy-domain.js";
@@ -29,4 +29,13 @@ export { oauthErrorFor, pkceChallengeFromVerifier } from "./authorize-shared.js"
29
29
  // pure data, the own-name actor rule, the tenant compare and the writer-role
30
30
  // assertion. Every consumer wires a person the same way.
31
31
  export { PERSON_ACTOR_PREFIX, PersonRefusalError, personActorName, isPersonActorName, personTokenRecordSchema, resolvePersonPrincipal, resolvePersonActingName, checkPersonCallShape, resolvePersonTenantAccess, resolveWriterRole, requireWriterRole, } from "./person-principal.js";
32
+ // 0.10.0 — Identity gaps closed in the package so consumers adapt, never copy.
33
+ export { IdentityRefusalError } from "./identity-refusal.js";
34
+ export { resolveMinRole, assertMinRole, mapRoleClaim } from "./role-policy.js";
35
+ export { isRowInTenant, resolveMembership, requireOrgAdmin, membershipRecordSchema, } from "./tenant-membership.js";
36
+ export { assertNamespaceWrite } from "./namespace-write.js";
37
+ export { timingSafeEqualSync, assertSecretSync } from "./secret-compare-sync.js";
38
+ // 0.11.0 — The acting principal by ID and the target checked by ID. A name is
39
+ // a display label: it never selects a row and never authorises one.
40
+ export { actingCredentialSchema, principalRowSchema, organisationRowSchema, resolveActingPrincipal, assertTargetBelongsTo, } from "./principal-by-id.js";
32
41
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,mBAAmB,GAEpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAS9D,0CAA0C;AAC1C,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAU7B,gFAAgF;AAChF,OAAO,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAU1E,kFAAkF;AAClF,OAAO,EACL,sBAAsB,EACtB,6BAA6B,EAC7B,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAWzB,sEAAsE;AACtE,yEAAyE;AACzE,mEAAmE;AACnE,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAY/B,+EAA+E;AAC/E,mEAAmE;AACnE,OAAO,EACL,cAAc,EACd,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAa7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAOhE,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAQ7D,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AASlE,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAMjF,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,oBAAoB,EACpB,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,sBAAsB,EACtB,mBAAmB,GAEpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAS9D,0CAA0C;AAC1C,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAU7B,gFAAgF;AAChF,OAAO,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAU1E,kFAAkF;AAClF,OAAO,EACL,sBAAsB,EACtB,6BAA6B,EAC7B,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAWzB,sEAAsE;AACtE,yEAAyE;AACzE,mEAAmE;AACnE,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAY/B,+EAA+E;AAC/E,mEAAmE;AACnE,OAAO,EACL,cAAc,EACd,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAa7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAOhE,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAQ7D,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AASlE,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAMjF,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,oBAAoB,EACpB,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAc/B,+EAA+E;AAC/E,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAM7D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAO/E,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAQhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjF,8EAA8E;AAC9E,oEAAoE;AACpE,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Namespace WRITE assertion.
3
+ *
4
+ * `OAuthCtx.namespaceWritePrefixes` has always been on the type with the note
5
+ * "enforced by the caller". This is that enforcement, so no consumer carries
6
+ * its own copy. It uses `namespaceMatchesPrefix` from `./scope-filter`, the
7
+ * same boundary rule as the read filter: prefix equality, or prefix + '/'.
8
+ *
9
+ * A write is never granted by a READ prefix, by an empty prefix list (for a
10
+ * non-master) or by an empty-string prefix.
11
+ */
12
+ import type { OAuthCtx } from "./types.js";
13
+ /**
14
+ * Throws `IdentityRefusalError` unless the caller may write `namespace`.
15
+ * Master scope (`scope === "master"` or `"*"` in `fromAllowList`) passes.
16
+ *
17
+ * @param door The consumer's own name for the entry point, for the refusal.
18
+ */
19
+ export declare function assertNamespaceWrite(oauthCtx: OAuthCtx, namespace: string, door?: string): void;
20
+ //# sourceMappingURL=namespace-write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"namespace-write.d.ts","sourceRoot":"","sources":["../src/namespace-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,SAAyB,GAC5B,IAAI,CA2BN"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Namespace WRITE assertion.
3
+ *
4
+ * `OAuthCtx.namespaceWritePrefixes` has always been on the type with the note
5
+ * "enforced by the caller". This is that enforcement, so no consumer carries
6
+ * its own copy. It uses `namespaceMatchesPrefix` from `./scope-filter`, the
7
+ * same boundary rule as the read filter: prefix equality, or prefix + '/'.
8
+ *
9
+ * A write is never granted by a READ prefix, by an empty prefix list (for a
10
+ * non-master) or by an empty-string prefix.
11
+ */
12
+ import { IdentityRefusalError, refusal } from "./identity-refusal.js";
13
+ import { isMasterScope, namespaceMatchesPrefix } from "./scope-filter.js";
14
+ /**
15
+ * Throws `IdentityRefusalError` unless the caller may write `namespace`.
16
+ * Master scope (`scope === "master"` or `"*"` in `fromAllowList`) passes.
17
+ *
18
+ * @param door The consumer's own name for the entry point, for the refusal.
19
+ */
20
+ export function assertNamespaceWrite(oauthCtx, namespace, door = "assertNamespaceWrite") {
21
+ if (oauthCtx == null) {
22
+ throw new TypeError("assertNamespaceWrite: oauthCtx is required — pass an explicit OAuthCtx.");
23
+ }
24
+ const deny = (reason, detail) => {
25
+ throw new IdentityRefusalError(refusal("RBAC_DENIED", reason, door, detail));
26
+ };
27
+ if (isMasterScope(oauthCtx))
28
+ return;
29
+ if (typeof namespace !== "string" || namespace.length === 0) {
30
+ deny("namespace-invalid", "A non-empty namespace is required.");
31
+ }
32
+ const prefixes = (oauthCtx.namespaceWritePrefixes ?? []).filter((p) => typeof p === "string" && p.length > 0);
33
+ if (prefixes.length === 0) {
34
+ deny("no-write-prefixes", "This caller holds no namespace write prefix.");
35
+ }
36
+ if (!prefixes.some((p) => namespaceMatchesPrefix(namespace, p))) {
37
+ deny("namespace-write-denied", "The namespace is outside this caller's write prefixes.");
38
+ }
39
+ }
40
+ //# sourceMappingURL=namespace-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"namespace-write.js","sourceRoot":"","sources":["../src/namespace-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAAkB,EAClB,SAAiB,EACjB,IAAI,GAAG,sBAAsB;IAE7B,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,IAAI,SAAS,CACjB,yEAAyE,CAC1E,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,CACX,MAA4E,EAC5E,MAAc,EACP,EAAE;QACT,MAAM,IAAI,oBAAoB,CAAC,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/E,CAAC,CAAC;IAEF,IAAI,aAAa,CAAC,QAAQ,CAAC;QAAE,OAAO;IAEpC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5D,IAAI,CAAC,mBAAmB,EAAE,oCAAoC,CAAC,CAAC;IAClE,CAAC;IACD,MAAM,QAAQ,GAAG,CAAC,QAAQ,CAAC,sBAAsB,IAAI,EAAE,CAAC,CAAC,MAAM,CAC7D,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAC7C,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,IAAI,CAAC,mBAAmB,EAAE,8CAA8C,CAAC,CAAC;IAC5E,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,IAAI,CAAC,wBAAwB,EAAE,wDAAwD,CAAC,CAAC;IAC3F,CAAC;AACH,CAAC"}
@@ -136,6 +136,13 @@ export type PersonActingNameResult = {
136
136
  * resolved by the consumer): none ->
137
137
  * AGENT_CREDENTIAL_REQUIRED, a
138
138
  * different agent -> AGENT_IDENTITY_MISMATCH.
139
+ *
140
+ * @deprecated Since 0.11.0. Use `resolveActingPrincipal` (and
141
+ * `assertTargetBelongsTo` for the target), which resolve the actor by stored
142
+ * ID. This function takes names (`claimedName`, `agentCredential.agentName`)
143
+ * as identity input, which backend standard R-53 clause 4 makes
144
+ * non-conforming for any new door. Behaviour is unchanged; it will be removed
145
+ * in a major release once consumers have migrated off it.
139
146
  */
140
147
  export declare function resolvePersonActingName(input: {
141
148
  /** Only the actor name is read: the rule needs no organisation. */