@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
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Role policy as DATA: a minimum-role check over a caller-supplied order, and a
3
+ * role-claim mapping with no package-chosen default.
4
+ *
5
+ * The package never knows what roles exist. "admin > editor > viewer" is one
6
+ * consumer's vocabulary, passed in at the call site; another consumer passes
7
+ * another. Every absence (no role, unknown role, empty order, no mapping, no
8
+ * fallback) is a refusal, never a default grant.
9
+ *
10
+ * Pure functions: no framework SDK, no I/O.
11
+ */
12
+ import { type IdentityRefusal } from "./identity-refusal.js";
13
+ export type MinRoleInput = {
14
+ /** The verified role of the caller. null/undefined/"" is "no role". */
15
+ role: string | null | undefined;
16
+ /** The least-privileged role that is admitted. Must appear in `order`. */
17
+ minimum: string;
18
+ /**
19
+ * The caller's ordered role list, MOST privileged FIRST
20
+ * (e.g. `["admin", "editor", "viewer"]`). No empty list, no duplicates.
21
+ */
22
+ order: readonly string[];
23
+ /** The consumer's own name for the entry point. Defaults to the function name. */
24
+ door?: string;
25
+ };
26
+ export type MinRoleResult = {
27
+ ok: true;
28
+ role: string;
29
+ } | {
30
+ ok: false;
31
+ refusal: IdentityRefusal;
32
+ };
33
+ /**
34
+ * Admit `role` when it is at or above `minimum` in `order`.
35
+ *
36
+ * Refuses: an invalid order (empty, duplicated, non-string entries), a
37
+ * `minimum` outside the order, a missing or unknown role, a role below the
38
+ * minimum. Matching is exact (no case-folding), and a role is only ever found
39
+ * by array position, so `"constructor"` or `"__proto__"` are unknown roles.
40
+ */
41
+ export declare function resolveMinRole(input: MinRoleInput): MinRoleResult;
42
+ /** Throwing form of {@link resolveMinRole}; returns the role, throws `IdentityRefusalError`. */
43
+ export declare function assertMinRole(input: MinRoleInput): string;
44
+ export type MapRoleClaimInput = {
45
+ /** The raw claim from the verified credential. Only a non-empty string counts. */
46
+ claim: unknown;
47
+ /** Claim value -> consumer role. Data supplied by the caller. */
48
+ mapping: Readonly<Record<string, string>>;
49
+ /**
50
+ * The role to use when the claim is absent or not in `mapping`. The package
51
+ * never picks one: omit it and an absent or unmapped claim is REFUSED.
52
+ * Supply the least-privileged role you have.
53
+ */
54
+ fallback?: string;
55
+ door?: string;
56
+ };
57
+ export type MapRoleClaimResult = {
58
+ ok: true;
59
+ role: string;
60
+ source: "mapped" | "fallback";
61
+ } | {
62
+ ok: false;
63
+ refusal: IdentityRefusal;
64
+ };
65
+ /**
66
+ * Map a verified role claim to the consumer's role, with a least-privilege
67
+ * fallback that the CALLER must name. A claim that is not a string (array,
68
+ * object, number) is absent, never coerced. Lookup uses own keys only, so a
69
+ * claim of `"constructor"` is unmapped.
70
+ */
71
+ export declare function mapRoleClaim(input: MapRoleClaimInput): MapRoleClaimResult;
72
+ //# sourceMappingURL=role-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-policy.d.ts","sourceRoot":"","sources":["../src/role-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAS/B,MAAM,MAAM,YAAY,GAAG;IACzB,uEAAuE;IACvE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,CA2DjE;AAED,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAIzD;AAMD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,kFAAkF;IAClF,KAAK,EAAE,OAAO,CAAC;IACf,iEAAiE;IACjE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAA;CAAE,GACzD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,kBAAkB,CAgDzE"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Role policy as DATA: a minimum-role check over a caller-supplied order, and a
3
+ * role-claim mapping with no package-chosen default.
4
+ *
5
+ * The package never knows what roles exist. "admin > editor > viewer" is one
6
+ * consumer's vocabulary, passed in at the call site; another consumer passes
7
+ * another. Every absence (no role, unknown role, empty order, no mapping, no
8
+ * fallback) is a refusal, never a default grant.
9
+ *
10
+ * Pure functions: no framework SDK, no I/O.
11
+ */
12
+ import { IdentityRefusalError, refusal, } from "./identity-refusal.js";
13
+ const DOOR_MIN = "resolveMinRole";
14
+ const DOOR_MAP = "mapRoleClaim";
15
+ /**
16
+ * Admit `role` when it is at or above `minimum` in `order`.
17
+ *
18
+ * Refuses: an invalid order (empty, duplicated, non-string entries), a
19
+ * `minimum` outside the order, a missing or unknown role, a role below the
20
+ * minimum. Matching is exact (no case-folding), and a role is only ever found
21
+ * by array position, so `"constructor"` or `"__proto__"` are unknown roles.
22
+ */
23
+ export function resolveMinRole(input) {
24
+ const door = input.door ?? DOOR_MIN;
25
+ const { order, role, minimum } = input;
26
+ const orderValid = Array.isArray(order) &&
27
+ order.length > 0 &&
28
+ order.every((r) => typeof r === "string" && r.length > 0) &&
29
+ new Set(order).size === order.length;
30
+ if (!orderValid) {
31
+ return {
32
+ ok: false,
33
+ refusal: refusal("RBAC_DENIED", "invalid-role-order", door, "The role order must be a non-empty list of distinct role names."),
34
+ };
35
+ }
36
+ const minIndex = order.indexOf(minimum);
37
+ if (minIndex === -1) {
38
+ return {
39
+ ok: false,
40
+ refusal: refusal("RBAC_DENIED", "unknown-minimum-role", door, "The required minimum role is not part of the role order."),
41
+ };
42
+ }
43
+ const roleIndex = typeof role === "string" && role.length > 0 ? order.indexOf(role) : -1;
44
+ if (roleIndex === -1) {
45
+ return {
46
+ ok: false,
47
+ refusal: refusal("RBAC_DENIED", "unknown-role", door, "The caller's role is absent or not part of the role order."),
48
+ };
49
+ }
50
+ if (roleIndex > minIndex) {
51
+ return {
52
+ ok: false,
53
+ refusal: refusal("RBAC_DENIED", "role-below-minimum", door, "The caller's role is below the required minimum."),
54
+ };
55
+ }
56
+ return { ok: true, role: order[roleIndex] };
57
+ }
58
+ /** Throwing form of {@link resolveMinRole}; returns the role, throws `IdentityRefusalError`. */
59
+ export function assertMinRole(input) {
60
+ const result = resolveMinRole(input);
61
+ if (!result.ok)
62
+ throw new IdentityRefusalError(result.refusal);
63
+ return result.role;
64
+ }
65
+ /**
66
+ * Map a verified role claim to the consumer's role, with a least-privilege
67
+ * fallback that the CALLER must name. A claim that is not a string (array,
68
+ * object, number) is absent, never coerced. Lookup uses own keys only, so a
69
+ * claim of `"constructor"` is unmapped.
70
+ */
71
+ export function mapRoleClaim(input) {
72
+ const door = input.door ?? DOOR_MAP;
73
+ const { claim, mapping, fallback } = input;
74
+ if (mapping === null || typeof mapping !== "object" || Array.isArray(mapping)) {
75
+ return {
76
+ ok: false,
77
+ refusal: refusal("RBAC_DENIED", "invalid-role-mapping", door, "The role mapping must be a plain object of claim to role."),
78
+ };
79
+ }
80
+ const present = typeof claim === "string" && claim.length > 0;
81
+ if (present && Object.hasOwn(mapping, claim)) {
82
+ const mapped = mapping[claim];
83
+ if (typeof mapped === "string" && mapped.length > 0) {
84
+ return { ok: true, role: mapped, source: "mapped" };
85
+ }
86
+ return {
87
+ ok: false,
88
+ refusal: refusal("RBAC_DENIED", "invalid-role-mapping", door, "The role mapping holds a non-string or empty role."),
89
+ };
90
+ }
91
+ if (typeof fallback === "string" && fallback.length > 0) {
92
+ return { ok: true, role: fallback, source: "fallback" };
93
+ }
94
+ return {
95
+ ok: false,
96
+ refusal: refusal("RBAC_DENIED", present ? "role-claim-unmapped" : "role-claim-absent", door, present
97
+ ? "The role claim is not in the mapping and no fallback was supplied."
98
+ : "No role claim was presented and no fallback was supplied."),
99
+ };
100
+ }
101
+ //# sourceMappingURL=role-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-policy.js","sourceRoot":"","sources":["../src/role-policy.ts"],"names":[],"mappings":"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"}
@@ -61,6 +61,13 @@ import type { OAuthCtx } from "./types.js";
61
61
  * and pass it BY NAME. It is never inferred from an omitted argument.
62
62
  */
63
63
  export declare const LEGACY_WILDCARD_CTX: OAuthCtx;
64
+ /**
65
+ * THE path-boundary rule for namespaces: `namespace` is inside `prefix` when it
66
+ * equals it, or continues it after a '/'. `team/finance-archive` is NOT inside
67
+ * `team/finance`. One implementation, used by the read filter and by
68
+ * `assertNamespaceWrite`, so the two can never drift apart.
69
+ */
70
+ export declare function namespaceMatchesPrefix(namespace: string, prefix: string): boolean;
64
71
  /**
65
72
  * Row shape accepted by the scope filter. All fields optional because real
66
73
  * Convex documents from list_peers / list_messages / etc. don't all carry both.
@@ -112,6 +119,13 @@ export type GrantFieldDeclaration = readonly string[];
112
119
  * Substring matches that don't fall on a '/' boundary are explicitly rejected
113
120
  * (e.g. namespace="orchestrator/alphabet" does NOT match prefix
114
121
  * "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
122
+ *
123
+ * @deprecated Since 0.11.0. A `createdBy` name matched against
124
+ * `fromAllowList` selects the row, which backend standard R-53 clause 1
125
+ * forbids: a name never selects or authorises a row. Use
126
+ * `resolveActingPrincipal` and `assertTargetBelongsTo`, which compare stored
127
+ * IDs. Behaviour unchanged; it stays exported until consumers migrate off it,
128
+ * and is removed in a major release.
115
129
  */
116
130
  export declare function passesScopeFilter<T extends ScopeFilterable>(oauthCtx: OAuthCtx, row: T, grantFields?: GrantFieldDeclaration): boolean;
117
131
  /**
@@ -119,6 +133,12 @@ export declare function passesScopeFilter<T extends ScopeFilterable>(oauthCtx: O
119
133
  *
120
134
  * `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
121
135
  * behaviour).
136
+ *
137
+ * @deprecated Since 0.11.0. It selects rows through `passesScopeFilter`,
138
+ * where a `createdBy` name selects the row, which backend standard R-53
139
+ * clause 1 forbids. Use `resolveActingPrincipal` and `assertTargetBelongsTo`,
140
+ * which compare stored IDs. Behaviour unchanged; it stays exported until
141
+ * consumers migrate off it, and is removed in a major release.
122
142
  */
123
143
  export declare function scopeFilterList<T extends ScopeFilterable>(oauthCtx: OAuthCtx, rows: T[], grantFields?: GrantFieldDeclaration): T[];
124
144
  /**
@@ -128,6 +148,12 @@ export declare function scopeFilterList<T extends ScopeFilterable>(oauthCtx: OAu
128
148
  *
129
149
  * `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
130
150
  * behaviour).
151
+ *
152
+ * @deprecated Since 0.11.0. It admits the row through `passesScopeFilter`,
153
+ * where a `createdBy` name selects the row, which backend standard R-53
154
+ * clause 1 forbids. Use `resolveActingPrincipal` and `assertTargetBelongsTo`,
155
+ * which compare stored IDs. Behaviour unchanged; it stays exported until
156
+ * consumers migrate off it, and is removed in a major release.
131
157
  */
132
158
  export declare function scopeFilterGet<T extends ScopeFilterable>(oauthCtx: OAuthCtx, row: T | null | undefined, grantFields?: GrantFieldDeclaration): T | null;
133
159
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"scope-filter.d.ts","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAKjC,CAAC;AAWF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAIpD;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAAG,SAAS,MAAM,EAAE,CAAC;AActD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,eAAe,EACzD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,EACN,WAAW,GAAE,qBAAuC,GACnD,OAAO,CAgBT;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,CAAC,EAAE,EACT,WAAW,GAAE,qBAAuC,GACnD,CAAC,EAAE,CAGL;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EACzB,WAAW,GAAE,qBAAuC,GACnD,CAAC,GAAG,IAAI,CAIV;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAGtD"}
1
+ {"version":3,"file":"scope-filter.d.ts","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAKjC,CAAC;AAWF;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEjF;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAIpD;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAAG,SAAS,MAAM,EAAE,CAAC;AActD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,eAAe,EACzD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,EACN,WAAW,GAAE,qBAAuC,GACnD,OAAO,CAeT;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,CAAC,EAAE,EACT,WAAW,GAAE,qBAAuC,GACnD,CAAC,EAAE,CAGL;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EACzB,WAAW,GAAE,qBAAuC,GACnD,CAAC,GAAG,IAAI,CAIV;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAGtD"}
@@ -71,6 +71,15 @@ function assertOauthCtx(oauthCtx) {
71
71
  "if you deliberately want the pre-0.3.0 legacy-bearer wildcard behaviour.");
72
72
  }
73
73
  }
74
+ /**
75
+ * THE path-boundary rule for namespaces: `namespace` is inside `prefix` when it
76
+ * equals it, or continues it after a '/'. `team/finance-archive` is NOT inside
77
+ * `team/finance`. One implementation, used by the read filter and by
78
+ * `assertNamespaceWrite`, so the two can never drift apart.
79
+ */
80
+ export function namespaceMatchesPrefix(namespace, prefix) {
81
+ return namespace === prefix || namespace.startsWith(`${prefix}/`);
82
+ }
74
83
  /**
75
84
  * Returns true when the scope profile grants full, wildcard access.
76
85
  * Mirrors `isMasterScope` from the source module: `scope === "master"` OR
@@ -116,6 +125,13 @@ function grantFieldMatches(value, fromAllowList) {
116
125
  * Substring matches that don't fall on a '/' boundary are explicitly rejected
117
126
  * (e.g. namespace="orchestrator/alphabet" does NOT match prefix
118
127
  * "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
128
+ *
129
+ * @deprecated Since 0.11.0. A `createdBy` name matched against
130
+ * `fromAllowList` selects the row, which backend standard R-53 clause 1
131
+ * forbids: a name never selects or authorises a row. Use
132
+ * `resolveActingPrincipal` and `assertTargetBelongsTo`, which compare stored
133
+ * IDs. Behaviour unchanged; it stays exported until consumers migrate off it,
134
+ * and is removed in a major release.
119
135
  */
120
136
  export function passesScopeFilter(oauthCtx, row, grantFields = NO_GRANT_FIELDS) {
121
137
  assertOauthCtx(oauthCtx);
@@ -126,9 +142,7 @@ export function passesScopeFilter(oauthCtx, row, grantFields = NO_GRANT_FIELDS)
126
142
  return true;
127
143
  if (namespace) {
128
144
  for (const p of oauthCtx.namespaceReadPrefixes) {
129
- if (namespace === p)
130
- return true;
131
- if (namespace.startsWith(`${p}/`))
145
+ if (namespaceMatchesPrefix(namespace, p))
132
146
  return true;
133
147
  }
134
148
  }
@@ -144,6 +158,12 @@ export function passesScopeFilter(oauthCtx, row, grantFields = NO_GRANT_FIELDS)
144
158
  *
145
159
  * `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
146
160
  * behaviour).
161
+ *
162
+ * @deprecated Since 0.11.0. It selects rows through `passesScopeFilter`,
163
+ * where a `createdBy` name selects the row, which backend standard R-53
164
+ * clause 1 forbids. Use `resolveActingPrincipal` and `assertTargetBelongsTo`,
165
+ * which compare stored IDs. Behaviour unchanged; it stays exported until
166
+ * consumers migrate off it, and is removed in a major release.
147
167
  */
148
168
  export function scopeFilterList(oauthCtx, rows, grantFields = NO_GRANT_FIELDS) {
149
169
  assertOauthCtx(oauthCtx);
@@ -156,6 +176,12 @@ export function scopeFilterList(oauthCtx, rows, grantFields = NO_GRANT_FIELDS) {
156
176
  *
157
177
  * `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
158
178
  * behaviour).
179
+ *
180
+ * @deprecated Since 0.11.0. It admits the row through `passesScopeFilter`,
181
+ * where a `createdBy` name selects the row, which backend standard R-53
182
+ * clause 1 forbids. Use `resolveActingPrincipal` and `assertTargetBelongsTo`,
183
+ * which compare stored IDs. Behaviour unchanged; it stays exported until
184
+ * consumers migrate off it, and is removed in a major release.
159
185
  */
160
186
  export function scopeFilterGet(oauthCtx, row, grantFields = NO_GRANT_FIELDS) {
161
187
  assertOauthCtx(oauthCtx);
@@ -1 +1 @@
1
- {"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Synchronous constant-time secret comparison.
3
+ *
4
+ * `timingSafeEqual` in `./crypto` is async (it uses Web Crypto for its dummy
5
+ * work) and takes bytes. A synchronous call site — a plain admin-secret gate,
6
+ * a framework hook that cannot await — needs a synchronous equivalent. This is
7
+ * pure JavaScript with no runtime dependency, so it also runs on any edge
8
+ * runtime. The async comparator is unchanged.
9
+ *
10
+ * Constant time here means: the number of loop iterations depends only on the
11
+ * LONGER input's length, never on where the first difference is. JavaScript
12
+ * gives no hard timing guarantee; this removes the content-dependent early
13
+ * exit, which is the leak a naive `===` has.
14
+ */
15
+ /**
16
+ * Compare two strings (UTF-8) or byte arrays in constant time with respect to
17
+ * their content. Always walks the longer length; a length difference is folded
18
+ * into the result rather than returned early. Any other input type is `false`.
19
+ */
20
+ export declare function timingSafeEqualSync(a: string | Uint8Array, b: string | Uint8Array): boolean;
21
+ /**
22
+ * Assert a presented secret equals the configured one. Throws
23
+ * `IdentityRefusalError` (`CREDENTIAL_REFUSED`) on mismatch. An unset expected
24
+ * secret (undefined, null, empty) matches NOTHING, not even an empty
25
+ * presentation: a missing configuration must never open a door. The refusal
26
+ * carries neither value.
27
+ */
28
+ export declare function assertSecretSync(presented: string | Uint8Array | null | undefined, expected: string | Uint8Array | null | undefined, door?: string): void;
29
+ //# sourceMappingURL=secret-compare-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-compare-sync.d.ts","sourceRoot":"","sources":["../src/secret-compare-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,MAAM,GAAG,UAAU,EACtB,CAAC,EAAE,MAAM,GAAG,UAAU,GACrB,OAAO,CAcT;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,EACjD,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,EAChD,IAAI,SAAqB,GACxB,IAAI,CAyBN"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Synchronous constant-time secret comparison.
3
+ *
4
+ * `timingSafeEqual` in `./crypto` is async (it uses Web Crypto for its dummy
5
+ * work) and takes bytes. A synchronous call site — a plain admin-secret gate,
6
+ * a framework hook that cannot await — needs a synchronous equivalent. This is
7
+ * pure JavaScript with no runtime dependency, so it also runs on any edge
8
+ * runtime. The async comparator is unchanged.
9
+ *
10
+ * Constant time here means: the number of loop iterations depends only on the
11
+ * LONGER input's length, never on where the first difference is. JavaScript
12
+ * gives no hard timing guarantee; this removes the content-dependent early
13
+ * exit, which is the leak a naive `===` has.
14
+ */
15
+ import { IdentityRefusalError, refusal } from "./identity-refusal.js";
16
+ const encoder = new TextEncoder();
17
+ function toBytes(value) {
18
+ if (typeof value === "string")
19
+ return encoder.encode(value);
20
+ if (value instanceof Uint8Array)
21
+ return value;
22
+ return null;
23
+ }
24
+ /**
25
+ * Compare two strings (UTF-8) or byte arrays in constant time with respect to
26
+ * their content. Always walks the longer length; a length difference is folded
27
+ * into the result rather than returned early. Any other input type is `false`.
28
+ */
29
+ export function timingSafeEqualSync(a, b) {
30
+ const x = toBytes(a);
31
+ const y = toBytes(b);
32
+ if (x === null || y === null)
33
+ return false;
34
+ const xl = x.length;
35
+ const yl = y.length;
36
+ const max = xl > yl ? xl : yl;
37
+ let diff = xl ^ yl;
38
+ for (let i = 0; i < max; i++) {
39
+ const xi = i < xl ? x[i] : 0;
40
+ const yi = i < yl ? y[i] : 0;
41
+ diff |= xi ^ yi;
42
+ }
43
+ return diff === 0;
44
+ }
45
+ /**
46
+ * Assert a presented secret equals the configured one. Throws
47
+ * `IdentityRefusalError` (`CREDENTIAL_REFUSED`) on mismatch. An unset expected
48
+ * secret (undefined, null, empty) matches NOTHING, not even an empty
49
+ * presentation: a missing configuration must never open a door. The refusal
50
+ * carries neither value.
51
+ */
52
+ export function assertSecretSync(presented, expected, door = "assertSecretSync") {
53
+ const expectedBytes = toBytes(expected);
54
+ if (expectedBytes === null || expectedBytes.length === 0) {
55
+ throw new IdentityRefusalError(refusal("CREDENTIAL_REFUSED", "secret-not-configured", door, "No secret is configured for this entry point, so nothing can match."));
56
+ }
57
+ const presentedBytes = toBytes(presented) ?? new Uint8Array(0);
58
+ // Compare even when presented is empty so the work does not depend on it.
59
+ const equal = timingSafeEqualSync(presentedBytes, expectedBytes);
60
+ if (!equal || presentedBytes.length === 0) {
61
+ throw new IdentityRefusalError(refusal("CREDENTIAL_REFUSED", "secret-mismatch", door, "The presented secret does not match."));
62
+ }
63
+ }
64
+ //# sourceMappingURL=secret-compare-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-compare-sync.js","sourceRoot":"","sources":["../src/secret-compare-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAEtE,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC,SAAS,OAAO,CAAC,KAAc;IAC7B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5D,IAAI,KAAK,YAAY,UAAU;QAAE,OAAO,KAAK,CAAC;IAC9C,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,CAAsB,EACtB,CAAsB;IAEtB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACrB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACrB,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC;IACpB,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC;IACpB,MAAM,GAAG,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,IAAI,IAAI,GAAG,EAAE,GAAG,EAAE,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QACzC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,IAAI,EAAE,GAAG,EAAE,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,SAAiD,EACjD,QAAgD,EAChD,IAAI,GAAG,kBAAkB;IAEzB,MAAM,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,IAAI,aAAa,KAAK,IAAI,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,oBAAoB,CAC5B,OAAO,CACL,oBAAoB,EACpB,uBAAuB,EACvB,IAAI,EACJ,qEAAqE,CACtE,CACF,CAAC;IACJ,CAAC;IACD,MAAM,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IAC/D,0EAA0E;IAC1E,MAAM,KAAK,GAAG,mBAAmB,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;IACjE,IAAI,CAAC,KAAK,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,oBAAoB,CAC5B,OAAO,CACL,oBAAoB,EACpB,iBAAiB,EACjB,IAAI,EACJ,sCAAsC,CACvC,CACF,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Membership resolution, row-in-tenant visibility and the org-admin proof.
3
+ *
4
+ * The package cannot query a datastore, so the membership read is an INJECTED
5
+ * async lookup. Everything that can be got wrong stays here: a throwing lookup,
6
+ * a miss, an inactive row, a row of the wrong shape and a row that names a
7
+ * different organisation are all refusals. Nothing in this module returns a
8
+ * grant on an error path.
9
+ *
10
+ * Identifiers are OPAQUE and compared byte-for-byte: no trimming, no case
11
+ * folding, and never a slug compared against an id (supply the same kind of
12
+ * identifier on both sides).
13
+ */
14
+ import { z } from "zod";
15
+ import { type IdentityRefusal } from "./identity-refusal.js";
16
+ /**
17
+ * True only when the row is stamped with an organisation, the caller has one,
18
+ * and the two are strictly equal. An unstamped row is NEVER visible, including
19
+ * to a caller that also has no organisation (absence equals absence is not a
20
+ * match).
21
+ */
22
+ export declare function isRowInTenant(input: {
23
+ rowOrgId: string | null | undefined;
24
+ callerOrgId: string | null | undefined;
25
+ }): boolean;
26
+ /**
27
+ * What the consumer's lookup returns for (subject, organisation).
28
+ * `orgId`, when the row carries it, is re-checked here so a loose lookup (a
29
+ * prefix match, a wrong index) cannot become a membership in another org.
30
+ */
31
+ export declare const membershipRecordSchema: z.ZodObject<{
32
+ active: z.ZodBoolean;
33
+ role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
34
+ orgId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
35
+ }, z.core.$strip>;
36
+ export type MembershipRecord = z.infer<typeof membershipRecordSchema>;
37
+ export type ResolvedMembership = {
38
+ subject: string;
39
+ orgId: string;
40
+ /** The role the record carries, or null when it carries none. */
41
+ role: string | null;
42
+ };
43
+ export type ResolveMembershipInput = {
44
+ /** The verified subject. Never a client-supplied argument. */
45
+ subject: string;
46
+ /** The verified organisation id. */
47
+ orgId: string;
48
+ /** Indexed read of one membership. Return null/undefined on a miss. */
49
+ lookup: (subject: string, orgId: string) => Promise<MembershipRecord | null | undefined> | MembershipRecord | null | undefined;
50
+ door?: string;
51
+ };
52
+ export type ResolveMembershipResult = {
53
+ ok: true;
54
+ membership: ResolvedMembership;
55
+ } | {
56
+ ok: false;
57
+ refusal: IdentityRefusal;
58
+ };
59
+ /**
60
+ * Resolve the membership of a verified subject in a verified organisation via
61
+ * the injected `lookup`. Only `active === true` on a well-shaped record that
62
+ * names no other organisation is served. The lookup's own error text is never
63
+ * surfaced.
64
+ */
65
+ export declare function resolveMembership(input: ResolveMembershipInput): Promise<ResolveMembershipResult>;
66
+ export type RequireOrgAdminInput = {
67
+ /** The organisation the credential was VERIFIED for (a verified claim). */
68
+ verifiedOrgId: string | null | undefined;
69
+ /** The organisation the action is aimed at. Must equal `verifiedOrgId`. */
70
+ targetOrgId: string | null | undefined;
71
+ /** The caller's verified role. */
72
+ role: string | null | undefined;
73
+ /** Roles that count as organisation admin: data, supplied by the consumer. */
74
+ adminRoles: readonly string[];
75
+ door?: string;
76
+ };
77
+ /**
78
+ * An org-admin proof is bound to the caller's own verified organisation: the
79
+ * target must equal it, and the role must be one of `adminRoles`. Being an
80
+ * admin somewhere is not being an admin of the target. Throws
81
+ * `IdentityRefusalError`; returns nothing when served. An empty `adminRoles`
82
+ * admits nobody.
83
+ */
84
+ export declare function requireOrgAdmin(input: RequireOrgAdminInput): void;
85
+ //# sourceMappingURL=tenant-membership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-membership.d.ts","sourceRoot":"","sources":["../src/tenant-membership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAU/B;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACxC,GAAG,OAAO,CAGV;AAMD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;;;;iBAIjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,KACV,OAAO,CAAC,gBAAgB,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,gBAAgB,GAAG,IAAI,GAAG,SAAS,CAAC;IACxF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,kBAAkB,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAU5C;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,uBAAuB,CAAC,CA4ClC;AAMD,MAAM,MAAM,oBAAoB,GAAG;IACjC,2EAA2E;IAC3E,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACvC,kCAAkC;IAClC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,8EAA8E;IAC9E,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAmBjE"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Membership resolution, row-in-tenant visibility and the org-admin proof.
3
+ *
4
+ * The package cannot query a datastore, so the membership read is an INJECTED
5
+ * async lookup. Everything that can be got wrong stays here: a throwing lookup,
6
+ * a miss, an inactive row, a row of the wrong shape and a row that names a
7
+ * different organisation are all refusals. Nothing in this module returns a
8
+ * grant on an error path.
9
+ *
10
+ * Identifiers are OPAQUE and compared byte-for-byte: no trimming, no case
11
+ * folding, and never a slug compared against an id (supply the same kind of
12
+ * identifier on both sides).
13
+ */
14
+ import { z } from "zod";
15
+ import { IdentityRefusalError, refusal, } from "./identity-refusal.js";
16
+ function nonEmpty(value) {
17
+ return typeof value === "string" && value.length > 0;
18
+ }
19
+ // ---------------------------------------------------------------------------
20
+ // Row in tenant
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * True only when the row is stamped with an organisation, the caller has one,
24
+ * and the two are strictly equal. An unstamped row is NEVER visible, including
25
+ * to a caller that also has no organisation (absence equals absence is not a
26
+ * match).
27
+ */
28
+ export function isRowInTenant(input) {
29
+ const { rowOrgId, callerOrgId } = input;
30
+ return nonEmpty(rowOrgId) && nonEmpty(callerOrgId) && rowOrgId === callerOrgId;
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Membership
34
+ // ---------------------------------------------------------------------------
35
+ /**
36
+ * What the consumer's lookup returns for (subject, organisation).
37
+ * `orgId`, when the row carries it, is re-checked here so a loose lookup (a
38
+ * prefix match, a wrong index) cannot become a membership in another org.
39
+ */
40
+ export const membershipRecordSchema = z.object({
41
+ active: z.boolean(),
42
+ role: z.string().nullish(),
43
+ orgId: z.string().nullish(),
44
+ });
45
+ function deny(reason, door, detail) {
46
+ return { ok: false, refusal: refusal("RBAC_DENIED", reason, door, detail) };
47
+ }
48
+ /**
49
+ * Resolve the membership of a verified subject in a verified organisation via
50
+ * the injected `lookup`. Only `active === true` on a well-shaped record that
51
+ * names no other organisation is served. The lookup's own error text is never
52
+ * surfaced.
53
+ */
54
+ export async function resolveMembership(input) {
55
+ const door = input.door ?? "resolveMembership";
56
+ if (typeof input?.lookup !== "function") {
57
+ // A programming error in the consumer, not a claim about any caller.
58
+ throw new TypeError("resolveMembership: lookup must be a function.");
59
+ }
60
+ const { subject, orgId, lookup } = input;
61
+ if (!nonEmpty(subject) || !nonEmpty(orgId)) {
62
+ return deny("membership-invalid-input", door, "A verified subject and a verified organisation are both required.");
63
+ }
64
+ let record;
65
+ try {
66
+ record = await lookup(subject, orgId);
67
+ }
68
+ catch {
69
+ return deny("membership-lookup-failed", door, "The membership could not be read, so none is granted.");
70
+ }
71
+ if (record === null || record === undefined) {
72
+ return deny("membership-not-found", door, "No membership for this subject in this organisation.");
73
+ }
74
+ const parsed = membershipRecordSchema.safeParse(record);
75
+ if (!parsed.success) {
76
+ return deny("membership-record-invalid", door, "The membership record is not well formed.");
77
+ }
78
+ const row = parsed.data;
79
+ if (nonEmpty(row.orgId) && row.orgId !== orgId) {
80
+ return deny("other-organisation", door, "The membership belongs to another organisation.");
81
+ }
82
+ if (row.active !== true) {
83
+ return deny("membership-inactive", door, "The membership is not active.");
84
+ }
85
+ return {
86
+ ok: true,
87
+ membership: { subject, orgId, role: nonEmpty(row.role) ? row.role : null },
88
+ };
89
+ }
90
+ /**
91
+ * An org-admin proof is bound to the caller's own verified organisation: the
92
+ * target must equal it, and the role must be one of `adminRoles`. Being an
93
+ * admin somewhere is not being an admin of the target. Throws
94
+ * `IdentityRefusalError`; returns nothing when served. An empty `adminRoles`
95
+ * admits nobody.
96
+ */
97
+ export function requireOrgAdmin(input) {
98
+ const door = input.door ?? "requireOrgAdmin";
99
+ const { verifiedOrgId, targetOrgId, role, adminRoles } = input;
100
+ const fail = (reason, detail) => {
101
+ throw new IdentityRefusalError(refusal("RBAC_DENIED", reason, door, detail));
102
+ };
103
+ if (!nonEmpty(verifiedOrgId)) {
104
+ fail("no-verified-organisation", "The caller has no verified organisation.");
105
+ }
106
+ if (!nonEmpty(targetOrgId) || targetOrgId !== verifiedOrgId) {
107
+ fail("other-organisation", "The target organisation is not the caller's verified organisation.");
108
+ }
109
+ if (!nonEmpty(role) || !Array.isArray(adminRoles) || !adminRoles.includes(role)) {
110
+ fail("role-not-admin", "The caller's role is not an organisation-admin role.");
111
+ }
112
+ }
113
+ //# sourceMappingURL=tenant-membership.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-membership.js","sourceRoot":"","sources":["../src/tenant-membership.ts"],"names":[],"mappings":"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"}