@vantageos/cloud-identity 0.13.0 → 0.16.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.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @vantageos/cloud-identity — an organisation is its permanent ID.
3
+ *
4
+ * Everything here decides "which organisation" from the permanent org ID a
5
+ * verified credential or a stored row carries. A label (the slug an org can be
6
+ * renamed to) is display text: it never selects an org and never proves two
7
+ * references are one org.
8
+ *
9
+ * Refusal, always. There is no option that compares or resolves by label: a
10
+ * reference or credential without an org ID names no organisation, and a
11
+ * mapping is reached only by an ID.
12
+ *
13
+ * The store is the consumer's: every read is an adapter it supplies, called
14
+ * with the key as presented, byte for byte. A missing, throwing or malformed
15
+ * adapter refuses. An adapter's error text is never surfaced.
16
+ */
17
+ import { z } from "zod";
18
+ import { refusal } from "./identity-refusal.js";
19
+ import { resolveTenantIdOrAbsent } from "./org-guard.js";
20
+ function nonEmpty(value) {
21
+ return typeof value === "string" && value.length > 0;
22
+ }
23
+ function isUnstamped(ref) {
24
+ return (ref === null ||
25
+ ref === undefined ||
26
+ ((ref.id === null || ref.id === undefined) &&
27
+ (ref.label === null || ref.label === undefined)));
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // sameOrg / isFleetStamp / sameTenantStamp
31
+ // ---------------------------------------------------------------------------
32
+ /**
33
+ * Do two references name the same organisation? Only by ID, when both carry
34
+ * one. A label never matches, and absence is not a match: two unstamped
35
+ * references are NOT the same organisation here.
36
+ */
37
+ export function sameOrg(a, b) {
38
+ if (!a || !b)
39
+ return false;
40
+ return nonEmpty(a.id) && nonEmpty(b.id) && a.id === b.id;
41
+ }
42
+ /**
43
+ * Is a stamp the fleet's: unstamped, or the operator organisation's? The
44
+ * operator is read from stored data by the caller (see `findOperatorOrg`);
45
+ * pass `undefined` when there is not exactly one, and only an unstamped stamp
46
+ * is the fleet's. An empty-string stamp is neither unstamped nor the operator.
47
+ */
48
+ export function isFleetStamp(stamp, operator) {
49
+ if (isUnstamped(stamp))
50
+ return true;
51
+ return operator !== null && operator !== undefined && sameOrg(stamp, operator);
52
+ }
53
+ /**
54
+ * Do two stamps name the same tenant: the same organisation, or both the
55
+ * fleet's? Two IDs that differ are two tenants even under one label.
56
+ */
57
+ export function sameTenantStamp(a, b, operator) {
58
+ if (isFleetStamp(a, operator) && isFleetStamp(b, operator))
59
+ return true;
60
+ return sameOrg(a, b);
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // The stored mapping row
64
+ // ---------------------------------------------------------------------------
65
+ /**
66
+ * The consumer's organisation mapping row, as an adapter returns it. `id` is
67
+ * the permanent org ID and may be absent while the store is being filled;
68
+ * `label` is the slug. Extra fields are allowed and ignored.
69
+ */
70
+ export const orgMappingRowSchema = z.object({
71
+ id: z.string().min(1).nullish(),
72
+ label: z.string().min(1),
73
+ active: z.boolean(),
74
+ allowedOrchestrators: z.array(z.string()),
75
+ scopes: z.array(z.string()),
76
+ orgKind: z.string().nullish(),
77
+ });
78
+ function kindOf(raw) {
79
+ return raw === "operator" || raw === "client" ? raw : null;
80
+ }
81
+ /** Calls an adapter; a missing or throwing one is `called: false`. */
82
+ async function call(lookup, ...args) {
83
+ if (typeof lookup !== "function")
84
+ return { called: false };
85
+ try {
86
+ const raw = await lookup(...args);
87
+ return { called: true, row: raw ?? null };
88
+ }
89
+ catch {
90
+ return { called: false };
91
+ }
92
+ }
93
+ const CLERK_ORG_ID_SHAPE = /^org_[A-Za-z0-9]+$/;
94
+ const ID_CLAIMS = ["org_id", "organizationId", "orgId"];
95
+ /** The first claim that is shaped like a Clerk org ID and resolves as a tenant. */
96
+ function claimedOrgId(claims) {
97
+ for (const name of ID_CLAIMS) {
98
+ const value = claims[name];
99
+ if (typeof value !== "string" || !CLERK_ORG_ID_SHAPE.test(value))
100
+ continue;
101
+ const resolved = resolveTenantIdOrAbsent({ kind: "session", identity: { orgId: value } });
102
+ if (resolved.present)
103
+ return resolved.tenantId;
104
+ }
105
+ return undefined;
106
+ }
107
+ /**
108
+ * Resolves a VERIFIED credential's claims to the stored mapping row of its
109
+ * organisation, or refuses. The claim's org ID selects the row; nothing the
110
+ * request supplies does.
111
+ *
112
+ * A credential carrying no usable org ID is refused
113
+ * (`no-verified-organisation`) and an ID no mapping holds is refused
114
+ * (`org-mapping-not-found`). A label claim (a slug) is never read: it neither
115
+ * selects a mapping nor rescues a missing ID.
116
+ *
117
+ * `claims` is the verified identity's own claim record, never a request field.
118
+ */
119
+ export async function resolveOrgFromClaim(claims, lookups, options) {
120
+ const door = options?.door ?? "resolveOrgFromClaim";
121
+ const deny = (reason, detail) => ({
122
+ ok: false,
123
+ refusal: refusal("RBAC_DENIED", reason, door, detail),
124
+ });
125
+ const record = claims && typeof claims === "object" ? claims : {};
126
+ const orgId = claimedOrgId(record);
127
+ if (orgId === undefined) {
128
+ return deny("no-verified-organisation", "The credential carries no verified organisation.");
129
+ }
130
+ const served = (row, id) => {
131
+ if (row.active !== true) {
132
+ return deny("organisation-not-active", "The organisation is not mapped or not active.");
133
+ }
134
+ return {
135
+ ok: true,
136
+ org: {
137
+ id,
138
+ label: row.label,
139
+ allowedOrchestrators: row.allowedOrchestrators,
140
+ scopes: row.scopes,
141
+ orgKind: kindOf(row.orgKind),
142
+ },
143
+ };
144
+ };
145
+ const invalid = deny("org-mapping-record-invalid", "The organisation mapping record is not well formed.");
146
+ const unreadable = deny("org-mapping-lookup-failed", "The organisation mapping could not be read, so none is resolved.");
147
+ const found = await call(lookups?.orgById, orgId);
148
+ if (!found.called)
149
+ return unreadable;
150
+ if (found.row === null) {
151
+ return deny("org-mapping-not-found", "No organisation mapping holds the credential's organisation ID.");
152
+ }
153
+ const parsed = orgMappingRowSchema.safeParse(found.row);
154
+ if (!parsed.success || parsed.data.id !== orgId)
155
+ return invalid;
156
+ return served(parsed.data, orgId);
157
+ }
158
+ /**
159
+ * BACKFILL ONLY. Derives the permanent org ID of the organisation a label
160
+ * names, from the consumer's stored mapping, so a one-off migration can stamp
161
+ * rows written before IDs existed.
162
+ *
163
+ * MUST NEVER be called on a request path. A request decides its organisation
164
+ * from the verified credential's `org_id` claim (`resolveOrgFromClaim`); a miss
165
+ * there is a refusal, never a retry by label. A label is renamable display
166
+ * text: resolving an org from it on a request is the name-to-ID defect this
167
+ * module exists to close, and the export name carries that restriction so a
168
+ * reviewer sees it at every call site.
169
+ *
170
+ * A derivation and not an authorization: it answers for an inactive mapping
171
+ * too. It never invents an ID: a mapping with none filled is `id-not-filled`.
172
+ */
173
+ export async function resolveOrgIdForLabelBackfillOnly(label, lookups) {
174
+ const absent = (reason) => ({
175
+ present: false,
176
+ absence: { code: "ORG_ID_ABSENT", reason },
177
+ });
178
+ if (!nonEmpty(label))
179
+ return absent("no-label");
180
+ const found = await call(lookups?.orgByLabel, label);
181
+ if (!found.called)
182
+ return absent("lookup-failed");
183
+ if (found.row === null)
184
+ return absent("not-mapped");
185
+ const parsed = orgMappingRowSchema.safeParse(found.row);
186
+ if (!parsed.success || parsed.data.label !== label)
187
+ return absent("record-invalid");
188
+ if (parsed.data.id === undefined || parsed.data.id === null)
189
+ return absent("id-not-filled");
190
+ return { present: true, orgId: parsed.data.id };
191
+ }
192
+ const DEFAULT_OPERATOR_READ_CAP = 1000;
193
+ /**
194
+ * Finds the operator organisation (the fleet) from stored mapping rows: the
195
+ * single ACTIVE row whose kind is exactly `"operator"`. None, several, an
196
+ * over-cap read, an unreadable store and a malformed row are each a typed
197
+ * non-answer, so the caller can only treat the fleet as "no single operator"
198
+ * (fail closed) and never widen from a guess.
199
+ */
200
+ export async function findOperatorOrg(lookups, options) {
201
+ const cap = options?.cap ?? DEFAULT_OPERATOR_READ_CAP;
202
+ const read = lookups?.activeOrganisations;
203
+ if (typeof read !== "function")
204
+ return { kind: "unreadable" };
205
+ let raw;
206
+ try {
207
+ raw = await read(cap + 1);
208
+ }
209
+ catch {
210
+ return { kind: "unreadable" };
211
+ }
212
+ if (!Array.isArray(raw))
213
+ return { kind: "unreadable" };
214
+ if (raw.length > cap)
215
+ return { kind: "overCap" };
216
+ const operators = [];
217
+ for (const item of raw) {
218
+ const parsed = orgMappingRowSchema.safeParse(item);
219
+ if (!parsed.success)
220
+ return { kind: "unreadable" };
221
+ if (parsed.data.active === true && parsed.data.orgKind === "operator") {
222
+ operators.push(parsed.data);
223
+ }
224
+ }
225
+ if (operators.length === 0)
226
+ return { kind: "none" };
227
+ if (operators.length > 1)
228
+ return { kind: "many", count: operators.length };
229
+ const [only] = operators;
230
+ return { kind: "one", org: { id: only.id ?? undefined, label: only.label } };
231
+ }
232
+ //# sourceMappingURL=org-by-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-by-id.js","sourceRoot":"","sources":["../src/org-by-id.ts"],"names":[],"mappings":"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"}
@@ -211,6 +211,40 @@ export declare function resolveActingPrincipal(credential: ActingCredential | nu
211
211
  * absent owner is refused.
212
212
  */
213
213
  export declare function assertTargetBelongsTo(principal: ActingPrincipal | null | undefined, target: TargetIds | null | undefined, lookups: OrgKindLookups, opts?: AssertTargetOptions): Promise<AssertTargetResult>;
214
+ export type FilterTargetsResult<T> = {
215
+ ok: true;
216
+ rows: T[];
217
+ } | {
218
+ ok: false;
219
+ refusal: IdentityRefusal;
220
+ };
221
+ /**
222
+ * Keeps the rows that belong to the principal, deciding by STORED ID only.
223
+ * The ID-keyed replacement for the name-keyed `scopeFilterList`.
224
+ *
225
+ * Each row is judged by `assertTargetBelongsTo` (never re-derived here), so
226
+ * the same organisation, operator-org, unstamped, `ownerOnly` and
227
+ * `fleetCrossOrg` rules apply.
228
+ *
229
+ * - The principal is proved FIRST, before any row is read. An absent or
230
+ * unresolved principal, or a failed organisation-kind lookup for it,
231
+ * returns `{ ok: false, refusal }` naming the door, even for an empty
232
+ * list: a refusal is never an `ok: true` with no rows.
233
+ * - `fleetCrossOrg: true` from a principal that is not `kind: "fleet"` is a
234
+ * misconfigured door and is refused `reserved-fleet-scope` up front, for
235
+ * any list. Per row, `reserved-fleet-scope` therefore only means "this row
236
+ * is in the operator scope" and the row is dropped.
237
+ * - A resolved principal whose rows all belong elsewhere gets
238
+ * `{ ok: true, rows: [] }`: an absence, distinguishable from a refusal.
239
+ * - A row that does not belong (other organisation, operator-org row,
240
+ * unstamped, owner mismatch) is dropped. A same-NAME row of another
241
+ * organisation is dropped: no name field is read.
242
+ * - A lookup failure on any row refuses the whole call. It is never a
243
+ * silent drop and never a pass; any refusal reason not known to mean
244
+ * "this row is not yours" refuses too.
245
+ * - The order of the kept rows is the order of the input.
246
+ */
247
+ export declare function filterTargetsBelongingTo<T extends TargetIds>(principal: ActingPrincipal | null | undefined, rows: readonly T[], lookups: OrgKindLookups, opts?: AssertTargetOptions): Promise<FilterTargetsResult<T>>;
214
248
  export type AssertOrgAdminOptions = {
215
249
  /**
216
250
  * The role values that count as organisation admin, exactly as the verified
@@ -290,5 +324,109 @@ export type AssertPrincipalListedResult = {
290
324
  * The fleet sentinel is out of scope here.
291
325
  */
292
326
  export declare function assertPrincipalListed(principal: ActingPrincipal | null | undefined, list: PrincipalIdList | null | undefined, opts?: AssertPrincipalListedOptions): AssertPrincipalListedResult;
327
+ /** The recipient of a message, named by its stored agent ID and organisation ID. */
328
+ export type RecipientIds = {
329
+ /** The recipient agent's stored ID. */
330
+ agentId: string;
331
+ /** The recipient agent's stored organisation ID. */
332
+ orgId: string;
333
+ };
334
+ /**
335
+ * The lookups `assertRecipientAddressable` reads: the organisation kind, and
336
+ * the stored roster of agent IDs of one organisation (a client's list of the
337
+ * fleet agents it may reach). `null`, a miss or a throw is an unresolvable
338
+ * roster and refuses.
339
+ */
340
+ export type RecipientLookups = OrgKindLookups & {
341
+ rosterOf?: Lookup<[orgId: string], PrincipalIdList>;
342
+ };
343
+ export type AssertRecipientAddressableOptions = {
344
+ /** The consumer's name for the door; carried on every refusal. */
345
+ door?: string;
346
+ };
347
+ export type AssertRecipientAddressableResult = {
348
+ ok: true;
349
+ } | {
350
+ ok: false;
351
+ refusal: IdentityRefusal;
352
+ };
353
+ /**
354
+ * Decides whether a sender may address a recipient, across organisations
355
+ * included, by AGENT ID only. The sender must come from
356
+ * `resolveActingPrincipal`. No name is read anywhere; a name never matches.
357
+ *
358
+ * Admitted iff one of these holds:
359
+ * (a) same organisation: the sender's `orgId` equals the recipient's;
360
+ * (b) client to operator: the sender is in a client organisation, the
361
+ * recipient is in the operator organisation, and the RECIPIENT's ID is
362
+ * on the SENDER organisation's roster;
363
+ * (c) operator to client: the sender is in the operator organisation, the
364
+ * recipient is in a client organisation, and the SENDER's ID is on the
365
+ * RECIPIENT organisation's roster.
366
+ *
367
+ * Everything else is refused with a typed `RBAC_DENIED` carrying the door
368
+ * (default `"assertRecipientAddressable"`): an absent sender or one without an
369
+ * ID (`credential-invalid`), a sender that is not `kind: "agent"`
370
+ * (`principal-not-an-agent`), an absent recipient or one without an agent ID
371
+ * or organisation (`target-unstamped`), client to client across organisations
372
+ * and an organisation kind that is neither operator nor client
373
+ * (`target-other-organisation`), a missing or throwing `orgKindOf` /
374
+ * `rosterOf` (`principal-lookup-failed`), an absent roster (`list-absent`), and
375
+ * a roster that is empty, stored under the wrong organisation or does not list
376
+ * the ID (the refusals of {@link assertPrincipalListed}, which is reused for
377
+ * that check).
378
+ */
379
+ export declare function assertRecipientAddressable(sender: ActingPrincipal | null | undefined, recipient: RecipientIds | null | undefined, lookups: RecipientLookups, opts?: AssertRecipientAddressableOptions): Promise<AssertRecipientAddressableResult>;
380
+ /**
381
+ * A verified session that carries a person but NO organisation (a signed-in
382
+ * user who has not joined one). `resolveActingPrincipal` cannot express this:
383
+ * a person credential requires `verifiedOrgId`, so a no-organisation session
384
+ * is refused there exactly as a garbled credential is. This kind exists so
385
+ * the consumer can say "verified, and no organisation" on purpose; an absent
386
+ * `verifiedOrgId` on a `person` credential stays a garbled credential.
387
+ */
388
+ declare const preOrgCredentialSchema: z.ZodObject<{
389
+ kind: z.ZodLiteral<"person-no-org">;
390
+ personId: z.ZodString;
391
+ }, z.core.$strict>;
392
+ export type CallerCredential = ActingCredential | z.input<typeof preOrgCredentialSchema>;
393
+ export type AssertCallerStandingOptions = {
394
+ /** Role values that make a person an organisation admin; see `assertOrgAdmin`. */
395
+ adminRoles: readonly string[];
396
+ /** The consumer's name for the door; carried on every refusal. */
397
+ door?: string;
398
+ };
399
+ /**
400
+ * The four callers, said apart. `anonymous` means no caller could be
401
+ * resolved (no credential, a garbled one, a miss, an inactive row) and carries
402
+ * the typed refusal; it is never what a signed-in session without an
403
+ * organisation becomes.
404
+ */
405
+ export type CallerStanding = {
406
+ standing: "anonymous";
407
+ refusal: IdentityRefusal;
408
+ } | {
409
+ standing: "pre-org";
410
+ personId: string;
411
+ } | {
412
+ standing: "member";
413
+ principal: ActingPrincipal;
414
+ } | {
415
+ standing: "admin";
416
+ principal: ActingPrincipal;
417
+ } | {
418
+ standing: "fleet";
419
+ principal: ActingPrincipal;
420
+ };
421
+ /**
422
+ * Derives a caller's STANDING from a verified credential. It adds one case to
423
+ * the existing primitives, the signed-in session with no organisation, and
424
+ * reuses them for everything else: `resolveActingPrincipal` decides who the
425
+ * caller is (and `fleet`), `assertOrgAdmin` decides `admin` against the
426
+ * principal's own organisation. Any other resolved principal (a person
427
+ * without the admin role, an agent, a non-operator service account) is a
428
+ * `member`; read `principal.kind` to tell them apart.
429
+ */
430
+ export declare function resolveCallerStanding(credential: CallerCredential | null | undefined, lookups: PrincipalLookups, opts: AssertCallerStandingOptions): Promise<CallerStanding>;
293
431
  export {};
294
432
  //# sourceMappingURL=principal-by-id.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"principal-by-id.d.ts","sourceRoot":"","sources":["../src/principal-by-id.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"principal-by-id.d.ts","sourceRoot":"","sources":["../src/principal-by-id.ts"],"names":[],"mappings":"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"}
@@ -343,6 +343,64 @@ export async function assertTargetBelongsTo(principal, target, lookups, opts = {
343
343
  }
344
344
  return { ok: true };
345
345
  }
346
+ /** Refusal reasons that say only "this ROW is not the principal's". */
347
+ const ROW_NOT_OURS = new Set([
348
+ "target-unstamped",
349
+ "target-other-organisation",
350
+ "target-owner-mismatch",
351
+ "reserved-fleet-scope",
352
+ ]);
353
+ /**
354
+ * Keeps the rows that belong to the principal, deciding by STORED ID only.
355
+ * The ID-keyed replacement for the name-keyed `scopeFilterList`.
356
+ *
357
+ * Each row is judged by `assertTargetBelongsTo` (never re-derived here), so
358
+ * the same organisation, operator-org, unstamped, `ownerOnly` and
359
+ * `fleetCrossOrg` rules apply.
360
+ *
361
+ * - The principal is proved FIRST, before any row is read. An absent or
362
+ * unresolved principal, or a failed organisation-kind lookup for it,
363
+ * returns `{ ok: false, refusal }` naming the door, even for an empty
364
+ * list: a refusal is never an `ok: true` with no rows.
365
+ * - `fleetCrossOrg: true` from a principal that is not `kind: "fleet"` is a
366
+ * misconfigured door and is refused `reserved-fleet-scope` up front, for
367
+ * any list. Per row, `reserved-fleet-scope` therefore only means "this row
368
+ * is in the operator scope" and the row is dropped.
369
+ * - A resolved principal whose rows all belong elsewhere gets
370
+ * `{ ok: true, rows: [] }`: an absence, distinguishable from a refusal.
371
+ * - A row that does not belong (other organisation, operator-org row,
372
+ * unstamped, owner mismatch) is dropped. A same-NAME row of another
373
+ * organisation is dropped: no name field is read.
374
+ * - A lookup failure on any row refuses the whole call. It is never a
375
+ * silent drop and never a pass; any refusal reason not known to mean
376
+ * "this row is not yours" refuses too.
377
+ * - The order of the kept rows is the order of the input.
378
+ */
379
+ export async function filterTargetsBelongingTo(principal, rows, lookups, opts = {}) {
380
+ const door = opts.door ?? "filterTargetsBelongingTo";
381
+ const doorOpts = { ...opts, door };
382
+ // A door asking for cross-organisation reach that the principal does not
383
+ // hold is a misconfigured door: refused, never an absence of rows.
384
+ if (opts.fleetCrossOrg === true && principal != null && principal.kind !== "fleet") {
385
+ return denied("reserved-fleet-scope", door, "Cross-organisation reach is held only by the operator organisation's service account.");
386
+ }
387
+ // Prove the principal on a target that is its own organisation's, owner
388
+ // check off: only a principal-level fault can refuse this.
389
+ const proof = await assertTargetBelongsTo(principal, { orgId: principal?.orgId }, lookups, { ...doorOpts, ownerOnly: false, fleetCrossOrg: false });
390
+ if (!proof.ok)
391
+ return proof;
392
+ const kept = [];
393
+ for (const row of rows) {
394
+ const verdict = await assertTargetBelongsTo(principal, row, lookups, doorOpts);
395
+ if (verdict.ok) {
396
+ kept.push(row);
397
+ }
398
+ else if (!ROW_NOT_OURS.has(verdict.refusal.reason)) {
399
+ return verdict;
400
+ }
401
+ }
402
+ return { ok: true, rows: kept };
403
+ }
346
404
  /**
347
405
  * Admits a principal as ADMIN of the target organisation, deciding by ID only.
348
406
  * The principal must come from `resolveActingPrincipal`; the role is the
@@ -438,4 +496,119 @@ export function assertPrincipalListed(principal, list, opts) {
438
496
  }
439
497
  return { ok: true };
440
498
  }
499
+ /**
500
+ * Decides whether a sender may address a recipient, across organisations
501
+ * included, by AGENT ID only. The sender must come from
502
+ * `resolveActingPrincipal`. No name is read anywhere; a name never matches.
503
+ *
504
+ * Admitted iff one of these holds:
505
+ * (a) same organisation: the sender's `orgId` equals the recipient's;
506
+ * (b) client to operator: the sender is in a client organisation, the
507
+ * recipient is in the operator organisation, and the RECIPIENT's ID is
508
+ * on the SENDER organisation's roster;
509
+ * (c) operator to client: the sender is in the operator organisation, the
510
+ * recipient is in a client organisation, and the SENDER's ID is on the
511
+ * RECIPIENT organisation's roster.
512
+ *
513
+ * Everything else is refused with a typed `RBAC_DENIED` carrying the door
514
+ * (default `"assertRecipientAddressable"`): an absent sender or one without an
515
+ * ID (`credential-invalid`), a sender that is not `kind: "agent"`
516
+ * (`principal-not-an-agent`), an absent recipient or one without an agent ID
517
+ * or organisation (`target-unstamped`), client to client across organisations
518
+ * and an organisation kind that is neither operator nor client
519
+ * (`target-other-organisation`), a missing or throwing `orgKindOf` /
520
+ * `rosterOf` (`principal-lookup-failed`), an absent roster (`list-absent`), and
521
+ * a roster that is empty, stored under the wrong organisation or does not list
522
+ * the ID (the refusals of {@link assertPrincipalListed}, which is reused for
523
+ * that check).
524
+ */
525
+ export async function assertRecipientAddressable(sender, recipient, lookups, opts) {
526
+ const door = opts?.door ?? "assertRecipientAddressable";
527
+ if (sender === null ||
528
+ typeof sender !== "object" ||
529
+ !nonEmpty(sender.principalId) ||
530
+ !nonEmpty(sender.orgId)) {
531
+ return denied("credential-invalid", door, "No resolved sender stands behind this call.");
532
+ }
533
+ if (sender.kind !== "agent") {
534
+ return denied("principal-not-an-agent", door, "Only an agent addresses a recipient; the sender is not an agent.");
535
+ }
536
+ if (recipient === null ||
537
+ typeof recipient !== "object" ||
538
+ !nonEmpty(recipient.agentId) ||
539
+ !nonEmpty(recipient.orgId)) {
540
+ return denied("target-unstamped", door, "The recipient carries no agent ID and organisation.");
541
+ }
542
+ if (recipient.orgId === sender.orgId)
543
+ return { ok: true };
544
+ const senderKind = await loadOrgKind(lookups, sender.orgId, door);
545
+ if (!senderKind.ok)
546
+ return senderKind;
547
+ const recipientKind = await loadOrgKind(lookups, recipient.orgId, door);
548
+ if (!recipientKind.ok)
549
+ return recipientKind;
550
+ let clientOrgId;
551
+ let listed;
552
+ if (senderKind.kind === "client" && recipientKind.kind === "operator") {
553
+ clientOrgId = sender.orgId;
554
+ listed = { principalId: recipient.agentId };
555
+ }
556
+ else if (senderKind.kind === "operator" && recipientKind.kind === "client") {
557
+ clientOrgId = recipient.orgId;
558
+ listed = { principalId: sender.principalId };
559
+ }
560
+ else {
561
+ return denied("target-other-organisation", door, "The recipient belongs to another organisation that this sender may not reach.");
562
+ }
563
+ const rosterOf = lookups?.rosterOf;
564
+ if (typeof rosterOf !== "function") {
565
+ return denied("principal-lookup-failed", door, "No lookup was supplied for the roster.");
566
+ }
567
+ let roster;
568
+ try {
569
+ roster = await rosterOf(clientOrgId);
570
+ }
571
+ catch {
572
+ return denied("principal-lookup-failed", door, "The roster could not be read, so none is resolved.");
573
+ }
574
+ return assertPrincipalListed({ ...listed, orgId: clientOrgId, kind: "agent" }, roster, { door });
575
+ }
576
+ // ---------------------------------------------------------------------------
577
+ // resolveCallerStanding (0.14.0)
578
+ // ---------------------------------------------------------------------------
579
+ /**
580
+ * A verified session that carries a person but NO organisation (a signed-in
581
+ * user who has not joined one). `resolveActingPrincipal` cannot express this:
582
+ * a person credential requires `verifiedOrgId`, so a no-organisation session
583
+ * is refused there exactly as a garbled credential is. This kind exists so
584
+ * the consumer can say "verified, and no organisation" on purpose; an absent
585
+ * `verifiedOrgId` on a `person` credential stays a garbled credential.
586
+ */
587
+ const preOrgCredentialSchema = z.strictObject({
588
+ kind: z.literal("person-no-org"),
589
+ personId: z.string().min(1),
590
+ });
591
+ /**
592
+ * Derives a caller's STANDING from a verified credential. It adds one case to
593
+ * the existing primitives, the signed-in session with no organisation, and
594
+ * reuses them for everything else: `resolveActingPrincipal` decides who the
595
+ * caller is (and `fleet`), `assertOrgAdmin` decides `admin` against the
596
+ * principal's own organisation. Any other resolved principal (a person
597
+ * without the admin role, an agent, a non-operator service account) is a
598
+ * `member`; read `principal.kind` to tell them apart.
599
+ */
600
+ export async function resolveCallerStanding(credential, lookups, opts) {
601
+ const door = opts?.door ?? "resolveCallerStanding";
602
+ const preOrg = preOrgCredentialSchema.safeParse(credential);
603
+ if (preOrg.success)
604
+ return { standing: "pre-org", personId: preOrg.data.personId };
605
+ const resolved = await resolveActingPrincipal(credential, lookups, door);
606
+ if (!resolved.ok)
607
+ return { standing: "anonymous", refusal: resolved.refusal };
608
+ const { principal } = resolved;
609
+ if (principal.kind === "fleet")
610
+ return { standing: "fleet", principal };
611
+ const admin = assertOrgAdmin(principal, principal.orgId, { adminRoles: opts?.adminRoles, door });
612
+ return { standing: admin.ok ? "admin" : "member", principal };
613
+ }
441
614
  //# sourceMappingURL=principal-by-id.js.map