@vantageos/cloud-identity 0.9.1 → 0.10.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.
- package/CHANGELOG.md +23 -0
- package/README.md +71 -0
- package/dist/identity-refusal.d.ts +31 -0
- package/dist/identity-refusal.d.ts.map +1 -0
- package/dist/identity-refusal.js +27 -0
- package/dist/identity-refusal.js.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/namespace-write.d.ts +20 -0
- package/dist/namespace-write.d.ts.map +1 -0
- package/dist/namespace-write.js +40 -0
- package/dist/namespace-write.js.map +1 -0
- package/dist/role-policy.d.ts +72 -0
- package/dist/role-policy.d.ts.map +1 -0
- package/dist/role-policy.js +101 -0
- package/dist/role-policy.js.map +1 -0
- package/dist/scope-filter.d.ts +7 -0
- package/dist/scope-filter.d.ts.map +1 -1
- package/dist/scope-filter.js +10 -3
- package/dist/scope-filter.js.map +1 -1
- package/dist/secret-compare-sync.d.ts +29 -0
- package/dist/secret-compare-sync.d.ts.map +1 -0
- package/dist/secret-compare-sync.js +64 -0
- package/dist/secret-compare-sync.js.map +1 -0
- package/dist/tenant-membership.d.ts +85 -0
- package/dist/tenant-membership.d.ts.map +1 -0
- package/dist/tenant-membership.js +113 -0
- package/dist/tenant-membership.js.map +1 -0
- package/package.json +21 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
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.10.0]
|
|
7
|
+
|
|
8
|
+
**Additive — identity gaps a consumer had are now in the package.** Every
|
|
9
|
+
consumer adapts these; none copies them. No existing function changes behaviour.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `resolveMinRole` / `assertMinRole`: minimum-role check over an order the
|
|
14
|
+
caller supplies as data. Unknown role, unknown minimum, invalid order refuse.
|
|
15
|
+
- `mapRoleClaim`: claim to role through caller data, with a fallback that must
|
|
16
|
+
be explicit. Absent or unmapped claim without a fallback refuses.
|
|
17
|
+
- `resolveMembership` (injected async lookup; error, miss, inactive, malformed
|
|
18
|
+
and other-organisation all refuse), `isRowInTenant` (strict equality,
|
|
19
|
+
unstamped row refused), `membershipRecordSchema`.
|
|
20
|
+
- `requireOrgAdmin`: the proof is bound to the caller's verified organisation.
|
|
21
|
+
- `assertNamespaceWrite` and the shared boundary rule `namespaceMatchesPrefix`
|
|
22
|
+
(now also used by the read filter, same behaviour).
|
|
23
|
+
- `timingSafeEqualSync` / `assertSecretSync`: synchronous constant-time
|
|
24
|
+
comparison; an unset expected secret matches nothing.
|
|
25
|
+
- `IdentityRefusalError` and the `IdentityRefusal` types.
|
|
26
|
+
- New subpath exports `./identity-refusal`, `./role-policy`,
|
|
27
|
+
`./tenant-membership`, `./namespace-write`, `./secret-compare-sync`.
|
|
28
|
+
|
|
6
29
|
## [0.9.1]
|
|
7
30
|
|
|
8
31
|
### Fixed
|
package/README.md
CHANGED
|
@@ -450,6 +450,77 @@ const mayWrite = resolveWriterRole({
|
|
|
450
450
|
The module verifies no credential. The token record must come from your own
|
|
451
451
|
lookup of the bearer the person presented, never from a request body.
|
|
452
452
|
|
|
453
|
+
## Role policy, membership, namespace writes and secrets (0.10.0)
|
|
454
|
+
|
|
455
|
+
Seven primitives that consumers used to copy. Each is pure, takes its policy as
|
|
456
|
+
DATA from the call site, and refuses on every absence: no role, no claim, no
|
|
457
|
+
list, no prefix, no secret and a failing lookup are all refusals, never a
|
|
458
|
+
default grant. Refusals share one shape, `IdentityRefusal` (`code`, `reason`,
|
|
459
|
+
`door`, `detail`), thrown inside an `IdentityRefusalError` by the asserting
|
|
460
|
+
forms. `IdentityRefusalCode` is `RBAC_DENIED` or `CREDENTIAL_REFUSED`;
|
|
461
|
+
`IdentityRefusalReason` is the closed list of reasons. Every function that
|
|
462
|
+
refuses accepts an optional `door` so the refusal names your own entry point.
|
|
463
|
+
|
|
464
|
+
**Minimum role over an order you supply** (`./role-policy`)
|
|
465
|
+
|
|
466
|
+
- `resolveMinRole({ role, minimum, order, door? })` — `order` is your ordered
|
|
467
|
+
list, MOST privileged first (for example `["admin", "editor", "viewer"]`).
|
|
468
|
+
Returns `{ ok: true, role }` or `{ ok: false, refusal }` (`MinRoleInput`,
|
|
469
|
+
`MinRoleResult`). Refuses an empty or duplicated order, a `minimum` outside
|
|
470
|
+
the order, an absent or unknown role, and a role below the minimum. Matching
|
|
471
|
+
is exact.
|
|
472
|
+
- `assertMinRole(input)` — the throwing form; returns the role or throws
|
|
473
|
+
`IdentityRefusalError`.
|
|
474
|
+
|
|
475
|
+
**Role-claim mapping with an explicit fallback** (`./role-policy`)
|
|
476
|
+
|
|
477
|
+
- `mapRoleClaim({ claim, mapping, fallback?, door? })` — maps a verified claim
|
|
478
|
+
(for example `"org:admin"`) to your role through `mapping`, your data
|
|
479
|
+
(`MapRoleClaimInput`, `MapRoleClaimResult`). The package never picks a
|
|
480
|
+
default: an absent or unmapped claim is served only when YOU pass `fallback`
|
|
481
|
+
(use your least-privileged role), and is refused otherwise. A claim that is
|
|
482
|
+
not a string counts as absent; prototype keys are never mapped. The result
|
|
483
|
+
says whether the role was `mapped` or came from the `fallback`.
|
|
484
|
+
|
|
485
|
+
**Membership and tenant visibility** (`./tenant-membership`)
|
|
486
|
+
|
|
487
|
+
- `resolveMembership({ subject, orgId, lookup, door? })` — async. `lookup` is
|
|
488
|
+
your read of one membership (`MembershipRecord`: `active`, optional `role`,
|
|
489
|
+
optional `orgId`). A throwing lookup, a miss, an inactive row, a malformed
|
|
490
|
+
row and a row naming another organisation are all refusals; the lookup's own
|
|
491
|
+
error text is not surfaced. Served result: `{ ok: true, membership }`
|
|
492
|
+
(`ResolvedMembership`, `ResolveMembershipInput`, `ResolveMembershipResult`).
|
|
493
|
+
`membershipRecordSchema` is the zod schema of the record.
|
|
494
|
+
- `isRowInTenant({ rowOrgId, callerOrgId })` — `true` only on strict equality
|
|
495
|
+
of two non-empty ids. An unstamped row is never visible, not even to a caller
|
|
496
|
+
with no organisation.
|
|
497
|
+
- `requireOrgAdmin({ verifiedOrgId, targetOrgId, role, adminRoles, door? })` —
|
|
498
|
+
the organisation-admin proof, bound to the caller's VERIFIED organisation:
|
|
499
|
+
the target must equal it, and the role must be one of `adminRoles` (your
|
|
500
|
+
data; an empty list admits nobody). Throws `IdentityRefusalError`
|
|
501
|
+
(`RequireOrgAdminInput`).
|
|
502
|
+
|
|
503
|
+
**Namespace writes** (`./namespace-write`, `./scope-filter`)
|
|
504
|
+
|
|
505
|
+
- `assertNamespaceWrite(oauthCtx, namespace, door?)` — throws unless the
|
|
506
|
+
caller's `namespaceWritePrefixes` admit the namespace. Master scope passes;
|
|
507
|
+
a non-master with no write prefix is refused; read prefixes never grant a
|
|
508
|
+
write; an empty-string prefix grants nothing.
|
|
509
|
+
- `namespaceMatchesPrefix(namespace, prefix)` — the one path-boundary rule:
|
|
510
|
+
equality, or the prefix followed by `/`. `team/finance-archive` is not inside
|
|
511
|
+
`team/finance`. The read filter and `assertNamespaceWrite` both use it.
|
|
512
|
+
|
|
513
|
+
**Synchronous constant-time comparison** (`./secret-compare-sync`)
|
|
514
|
+
|
|
515
|
+
- `timingSafeEqualSync(a, b)` — strings (UTF-8) or `Uint8Array`, pure
|
|
516
|
+
JavaScript, no early exit on content. It walks the longer length and folds a
|
|
517
|
+
length difference into the result. Any other input type is `false`. The async
|
|
518
|
+
`timingSafeEqual` is unchanged.
|
|
519
|
+
- `assertSecretSync(presented, expected, door?)` — throws `IdentityRefusalError`
|
|
520
|
+
(`CREDENTIAL_REFUSED`) on mismatch. An unset `expected` (undefined, null or
|
|
521
|
+
empty) matches nothing, so a missing configuration never opens a door. The
|
|
522
|
+
refusal carries neither value.
|
|
523
|
+
|
|
453
524
|
## What this package does not do
|
|
454
525
|
|
|
455
526
|
Worth reading before you rely on it.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed refusal shared by the 0.10.0 primitives (role policy, membership,
|
|
3
|
+
* org-admin proof, namespace write, secret comparison).
|
|
4
|
+
*
|
|
5
|
+
* A refusal is a value carrying its own code, reason and door, so a caller
|
|
6
|
+
* branches on content rather than on the mere presence of an exception, and an
|
|
7
|
+
* absence can never be mistaken for a refusal. It carries no credential.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the shape of `PersonRefusal` / `PersonRefusalError` on purpose: same
|
|
10
|
+
* `code`/`reason`/`door`/`detail` fields, same `ERROR: detail (reason, door)`
|
|
11
|
+
* message form. It is a separate type only because `PersonRefusalCode` is a
|
|
12
|
+
* closed vocabulary for the person path.
|
|
13
|
+
*/
|
|
14
|
+
export type IdentityRefusalCode = "RBAC_DENIED" | "CREDENTIAL_REFUSED";
|
|
15
|
+
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";
|
|
16
|
+
export type IdentityRefusal = {
|
|
17
|
+
code: IdentityRefusalCode;
|
|
18
|
+
reason: IdentityRefusalReason;
|
|
19
|
+
/** The consumer's own name for the entry point that refused. */
|
|
20
|
+
door: string;
|
|
21
|
+
/** One sentence, safe to show to the caller. Never a credential. */
|
|
22
|
+
detail: string;
|
|
23
|
+
};
|
|
24
|
+
/** Thrown by the asserting forms. Carries the typed refusal untouched. */
|
|
25
|
+
export declare class IdentityRefusalError extends Error {
|
|
26
|
+
readonly refusal: IdentityRefusal;
|
|
27
|
+
constructor(refusal: IdentityRefusal);
|
|
28
|
+
}
|
|
29
|
+
/** @internal Build a refusal. Not exported from the package. */
|
|
30
|
+
export declare function refusal(code: IdentityRefusalCode, reason: IdentityRefusalReason, door: string, detail: string): IdentityRefusal;
|
|
31
|
+
//# 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;;;;;;;;;;;;GAYG;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,CAAC;AAEtB,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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed refusal shared by the 0.10.0 primitives (role policy, membership,
|
|
3
|
+
* org-admin proof, namespace write, secret comparison).
|
|
4
|
+
*
|
|
5
|
+
* A refusal is a value carrying its own code, reason and door, so a caller
|
|
6
|
+
* branches on content rather than on the mere presence of an exception, and an
|
|
7
|
+
* absence can never be mistaken for a refusal. It carries no credential.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the shape of `PersonRefusal` / `PersonRefusalError` on purpose: same
|
|
10
|
+
* `code`/`reason`/`door`/`detail` fields, same `ERROR: detail (reason, door)`
|
|
11
|
+
* message form. It is a separate type only because `PersonRefusalCode` is a
|
|
12
|
+
* closed vocabulary for the person path.
|
|
13
|
+
*/
|
|
14
|
+
/** Thrown by the asserting forms. Carries the typed refusal untouched. */
|
|
15
|
+
export class IdentityRefusalError extends Error {
|
|
16
|
+
refusal;
|
|
17
|
+
constructor(refusal) {
|
|
18
|
+
super(`${refusal.code}: ${refusal.detail} (${refusal.reason}, ${refusal.door})`);
|
|
19
|
+
this.name = "IdentityRefusalError";
|
|
20
|
+
this.refusal = refusal;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** @internal Build a refusal. Not exported from the package. */
|
|
24
|
+
export function refusal(code, reason, door, detail) {
|
|
25
|
+
return { code, reason, door, detail };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=identity-refusal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-refusal.js","sourceRoot":"","sources":["../src/identity-refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAwCH,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,12 @@ 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";
|
|
32
40
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,10 @@ 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";
|
|
32
38
|
//# 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"}
|
|
@@ -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"}
|
|
@@ -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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"}
|
package/dist/scope-filter.d.ts
CHANGED
|
@@ -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.
|
|
@@ -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,
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,eAAe,EACzD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,EACN,WAAW,GAAE,qBAAuC,GACnD,OAAO,CAeT;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"}
|
package/dist/scope-filter.js
CHANGED
|
@@ -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
|
|
@@ -126,9 +135,7 @@ export function passesScopeFilter(oauthCtx, row, grantFields = NO_GRANT_FIELDS)
|
|
|
126
135
|
return true;
|
|
127
136
|
if (namespace) {
|
|
128
137
|
for (const p of oauthCtx.namespaceReadPrefixes) {
|
|
129
|
-
if (namespace
|
|
130
|
-
return true;
|
|
131
|
-
if (namespace.startsWith(`${p}/`))
|
|
138
|
+
if (namespaceMatchesPrefix(namespace, p))
|
|
132
139
|
return true;
|
|
133
140
|
}
|
|
134
141
|
}
|
package/dist/scope-filter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAIH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAa;IAC3C,KAAK,EAAE,QAAQ;IACf,aAAa,EAAE,CAAC,GAAG,CAAC;IACpB,qBAAqB,EAAE,EAAE;IACzB,sBAAsB,EAAE,EAAE;CAC3B,CAAC;AAEF,SAAS,cAAc,CAAC,QAAkB;IACxC,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,IAAI,SAAS,CACjB,2EAA2E;YACzE,0EAA0E,CAC7E,CAAC;IACJ,CAAC;AACH,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,GAAa;IACzC,cAAc,CAAC,GAAG,CAAC,CAAC;IACpB,IAAI,GAAG,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,GAAG,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC;AAeD,MAAM,eAAe,GAA0B,EAAE,CAAC;AAElD,SAAS,iBAAiB,CAAC,KAAc,EAAE,aAAuB;IAChE,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,IAAI,CACf,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CACtE,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAkB,EAClB,GAAM,EACN,cAAqC,eAAe;IAEpD,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzB,IAAI,aAAa,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC;IACrC,IAAI,SAAS,IAAI,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IACzE,IAAI,SAAS,EAAE,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,qBAAqB,EAAE,CAAC;YAC/C,IAAI,
|
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantageos/cloud-identity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Framework-agnostic identity + scope-filter primitives for VantagePeers Cloud (multi-tenant MCP).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -57,6 +57,26 @@
|
|
|
57
57
|
"./person-principal": {
|
|
58
58
|
"types": "./dist/person-principal.d.ts",
|
|
59
59
|
"import": "./dist/person-principal.js"
|
|
60
|
+
},
|
|
61
|
+
"./identity-refusal": {
|
|
62
|
+
"types": "./dist/identity-refusal.d.ts",
|
|
63
|
+
"import": "./dist/identity-refusal.js"
|
|
64
|
+
},
|
|
65
|
+
"./role-policy": {
|
|
66
|
+
"types": "./dist/role-policy.d.ts",
|
|
67
|
+
"import": "./dist/role-policy.js"
|
|
68
|
+
},
|
|
69
|
+
"./tenant-membership": {
|
|
70
|
+
"types": "./dist/tenant-membership.d.ts",
|
|
71
|
+
"import": "./dist/tenant-membership.js"
|
|
72
|
+
},
|
|
73
|
+
"./namespace-write": {
|
|
74
|
+
"types": "./dist/namespace-write.d.ts",
|
|
75
|
+
"import": "./dist/namespace-write.js"
|
|
76
|
+
},
|
|
77
|
+
"./secret-compare-sync": {
|
|
78
|
+
"types": "./dist/secret-compare-sync.d.ts",
|
|
79
|
+
"import": "./dist/secret-compare-sync.js"
|
|
60
80
|
}
|
|
61
81
|
},
|
|
62
82
|
"files": [
|