@vantageos/cloud-identity 0.2.0 → 0.4.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 +77 -1
- package/LICENSE +105 -16
- package/README.md +279 -155
- package/dist/bearer-validation.d.ts +2 -3
- package/dist/bearer-validation.d.ts.map +1 -1
- package/dist/bearer-validation.js +2 -3
- package/dist/bearer-validation.js.map +1 -1
- package/dist/crypto.d.ts +3 -5
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +3 -5
- package/dist/crypto.js.map +1 -1
- package/dist/human-path.d.ts +95 -0
- package/dist/human-path.d.ts.map +1 -0
- package/dist/human-path.js +117 -0
- package/dist/human-path.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/org-guard.d.ts +95 -0
- package/dist/org-guard.d.ts.map +1 -0
- package/dist/org-guard.js +77 -0
- package/dist/org-guard.js.map +1 -0
- package/dist/scope-filter.d.ts +33 -18
- package/dist/scope-filter.d.ts.map +1 -1
- package/dist/scope-filter.js +44 -19
- package/dist/scope-filter.js.map +1 -1
- package/dist/tenancy-domain.d.ts +4 -8
- package/dist/tenancy-domain.d.ts.map +1 -1
- package/dist/tenancy-domain.js +4 -8
- package/dist/tenancy-domain.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"human-path.d.ts","sourceRoot":"","sources":["../src/human-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,sBAAsB;;;;;EAKjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAMtE;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAMF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,gBAAgB,CAAC;CACxB,CAAC;AAoBF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,GAC3C,qBAAqB,CAiCvB"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vantageos/cloud-identity 0.4.0 — Human-path resolver (Clerk → { tenant,
|
|
3
|
+
* subject, role }).
|
|
4
|
+
*
|
|
5
|
+
* Additive-only: does not import any Clerk SDK, and does not modify any
|
|
6
|
+
* existing export. The caller resolves a Clerk session (or any session
|
|
7
|
+
* provider with an equivalent shape) *outside* this package and passes in a
|
|
8
|
+
* plain, already-resolved object; this module only normalizes it into the
|
|
9
|
+
* package's contract.
|
|
10
|
+
*
|
|
11
|
+
* `role` is a NEW literal union (`owner | admin | member | client`),
|
|
12
|
+
* distinct from `workspaceRoleSchema` (`Admin | Editor | Viewer`) in
|
|
13
|
+
* `./tenancy-domain.ts` — the two are unrelated vocabularies (workspace
|
|
14
|
+
* membership role vs. organization-account role) and are never conflated.
|
|
15
|
+
*
|
|
16
|
+
* Consistent with the package's "a right is presented, never inferred from
|
|
17
|
+
* an absence" contract: a session with no organization refuses (throws),
|
|
18
|
+
* and an unrecognized/absent Clerk org role refuses (throws) rather than
|
|
19
|
+
* silently defaulting to a role.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// HumanAccountRole
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export const humanAccountRoleSchema = z.enum([
|
|
26
|
+
"owner",
|
|
27
|
+
"admin",
|
|
28
|
+
"member",
|
|
29
|
+
"client",
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Maps Clerk's own org-role strings (`org:admin`, `org:member`, etc. — and
|
|
33
|
+
* their bare forms `admin`, `member`) onto the package's `HumanAccountRole`
|
|
34
|
+
* union. Deliberately explicit and exhaustive-by-throw: any string not
|
|
35
|
+
* listed here is refused rather than silently coerced to a default, per
|
|
36
|
+
* "presented, never inferred from an absence."
|
|
37
|
+
*/
|
|
38
|
+
const CLERK_ORG_ROLE_MAP = {
|
|
39
|
+
"org:owner": "owner",
|
|
40
|
+
owner: "owner",
|
|
41
|
+
"org:admin": "admin",
|
|
42
|
+
admin: "admin",
|
|
43
|
+
"org:member": "member",
|
|
44
|
+
member: "member",
|
|
45
|
+
"org:client": "client",
|
|
46
|
+
client: "client",
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* @security ⚠️ DECODE/NORMALIZE-ONLY. NOT AUTHENTICATION. NOT VERIFICATION.
|
|
50
|
+
*
|
|
51
|
+
* This function does NOT authenticate a request and does NOT verify
|
|
52
|
+
* anything. It NORMALIZES a session that the caller has ALREADY verified
|
|
53
|
+
* upstream — e.g. via Clerk's own server-side `auth()` in a Next.js/Convex
|
|
54
|
+
* server context — into this package's `{ tenant, subject, role }` contract.
|
|
55
|
+
*
|
|
56
|
+
* Passing an unverified, client-supplied object here (for example
|
|
57
|
+
* `req.body`, a query-string payload, or anything else an attacker
|
|
58
|
+
* controls) makes THAT object the trusted source of `tenant`, `subject`,
|
|
59
|
+
* AND `role` — a privilege-escalation bug. The shape checks below (missing
|
|
60
|
+
* orgId/userId/orgRole) only guard against malformed input; they perform
|
|
61
|
+
* NO signature check, NO issuer check, NO session-validity check.
|
|
62
|
+
*
|
|
63
|
+
* Callers MUST verify the session upstream (Clerk `auth()` server-side, or
|
|
64
|
+
* an equivalent signed-session verifier) BEFORE calling this function. Only
|
|
65
|
+
* pass in the object Clerk itself already verified — never pass through an
|
|
66
|
+
* unverified request body.
|
|
67
|
+
*
|
|
68
|
+
* Resolves an already-verified, framework-agnostic Clerk session object into
|
|
69
|
+
* the package's `{ tenant, subject, role }` contract.
|
|
70
|
+
*
|
|
71
|
+
* - throws when `session` is null/undefined ("Unauthenticated: no
|
|
72
|
+
* session.") — same refusal message class as `requireTenantId`'s
|
|
73
|
+
* session branch, for consistent error handling across both resolvers.
|
|
74
|
+
* - throws when `session.orgId` is missing or empty ("No active
|
|
75
|
+
* organization on this session...") — human path is cloud fail-closed,
|
|
76
|
+
* same invariant as `requireTenantId`.
|
|
77
|
+
* - throws when `session.userId` is missing or empty.
|
|
78
|
+
* - throws when `session.orgRole` is missing, empty, or not a recognized
|
|
79
|
+
* Clerk org role (unknown roles are refused, never defaulted).
|
|
80
|
+
*
|
|
81
|
+
* Named to make the already-verified precondition explicit at the
|
|
82
|
+
* call-site — see CHANGELOG.md 0.4.0 for the naming history — mirroring
|
|
83
|
+
* `decodeUnverifiedBearer`'s equivalent rename in `./tenancy-domain.ts`.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* // session MUST already be verified upstream, e.g.:
|
|
88
|
+
* // const { orgId, userId, orgRole } = await auth(); // Clerk server-side
|
|
89
|
+
* const { tenant, subject, role } = normalizeVerifiedHumanSession({
|
|
90
|
+
* orgId: "org_abc",
|
|
91
|
+
* userId: "user_123",
|
|
92
|
+
* orgRole: "org:admin",
|
|
93
|
+
* });
|
|
94
|
+
* // -> { tenant: "org_abc", subject: "user_123", role: "admin" }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function normalizeVerifiedHumanSession(session) {
|
|
98
|
+
if (!session) {
|
|
99
|
+
throw new Error("Unauthenticated: no session.");
|
|
100
|
+
}
|
|
101
|
+
const { orgId, userId, orgRole } = session;
|
|
102
|
+
if (typeof orgId !== "string" || orgId.length === 0) {
|
|
103
|
+
throw new Error("No active organization on this session: select an organization before accessing agents.");
|
|
104
|
+
}
|
|
105
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
106
|
+
throw new Error("No user id on this session: cannot resolve subject.");
|
|
107
|
+
}
|
|
108
|
+
if (typeof orgRole !== "string" || orgRole.length === 0) {
|
|
109
|
+
throw new Error("No organization role on this session: role must be presented explicitly, never inferred.");
|
|
110
|
+
}
|
|
111
|
+
const mapped = CLERK_ORG_ROLE_MAP[orgRole];
|
|
112
|
+
if (!mapped) {
|
|
113
|
+
throw new Error(`Unrecognized organization role "${orgRole}": expected one of ${Object.keys(CLERK_ORG_ROLE_MAP).join(", ")}.`);
|
|
114
|
+
}
|
|
115
|
+
return { tenant: orgId, subject: userId, role: mapped };
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=human-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"human-path.js","sourceRoot":"","sources":["../src/human-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC;IAC3C,OAAO;IACP,OAAO;IACP,QAAQ;IACR,QAAQ;CACT,CAAC,CAAC;AA6BH;;;;;;GAMG;AACH,MAAM,kBAAkB,GAAqC;IAC3D,WAAW,EAAE,OAAO;IACpB,KAAK,EAAE,OAAO;IACd,WAAW,EAAE,OAAO;IACpB,KAAK,EAAE,OAAO;IACd,YAAY,EAAE,QAAQ;IACtB,MAAM,EAAE,QAAQ;IAChB,YAAY,EAAE,QAAQ;IACtB,MAAM,EAAE,QAAQ;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,UAAU,6BAA6B,CAC3C,OAA4C;IAE5C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAE3C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CACb,yFAAyF,CAC1F,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IAED,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CACb,0FAA0F,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,mCAAmC,OAAO,sBAAsB,MAAM,CAAC,IAAI,CACzE,kBAAkB,CACnB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAChB,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1D,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @vantageos/cloud-identity — public surface.
|
|
3
3
|
*
|
|
4
|
-
* Framework-agnostic
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Framework-agnostic building blocks for multi-tenant authorization:
|
|
5
|
+
* tenant resolution, membership requirement, row filtering and bearer
|
|
6
|
+
* validation.
|
|
7
7
|
*/
|
|
8
8
|
export { timingSafeEqual } from "./crypto.js";
|
|
9
|
-
export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, type ScopeFilterable, } from "./scope-filter.js";
|
|
9
|
+
export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, 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";
|
|
13
13
|
export type { Workspace, WorkspaceMember, WorkspaceRole, TenantContext, ScopeViolationPayload, BearerPayload, } from "./tenancy-domain.js";
|
|
14
|
+
export { requireTenantId } from "./org-guard.js";
|
|
15
|
+
export type { SessionIdentity, TenantSource, DeploymentMode, } from "./org-guard.js";
|
|
16
|
+
export { humanAccountRoleSchema, normalizeVerifiedHumanSession } from "./human-path.js";
|
|
17
|
+
export type { HumanAccountRole, ClerkSessionLike, ResolvedHumanIdentity, } from "./human-path.js";
|
|
14
18
|
//# 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":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,KAAK,eAAe,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,SAAS,EACT,eAAe,EACf,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,aAAa,GACd,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,KAAK,eAAe,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,SAAS,EACT,eAAe,EACf,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EACV,eAAe,EACf,YAAY,EACZ,cAAc,GACf,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,sBAAsB,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AACxF,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @vantageos/cloud-identity — public surface.
|
|
3
3
|
*
|
|
4
|
-
* Framework-agnostic
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Framework-agnostic building blocks for multi-tenant authorization:
|
|
5
|
+
* tenant resolution, membership requirement, row filtering and bearer
|
|
6
|
+
* validation.
|
|
7
7
|
*/
|
|
8
8
|
export { timingSafeEqual } from "./crypto.js";
|
|
9
|
-
export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, } from "./scope-filter.js";
|
|
9
|
+
export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, 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";
|
|
13
|
+
// Organization-membership guard (unified session + bearer + self-host contract)
|
|
14
|
+
export { requireTenantId } from "./org-guard.js";
|
|
15
|
+
// 0.4.0 — Human-path resolver (Clerk-shaped session -> { tenant, subject, role })
|
|
16
|
+
export { humanAccountRoleSchema, normalizeVerifiedHumanSession } from "./human-path.js";
|
|
13
17
|
//# 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,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,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,MAAM,gBAAgB,CAAC;AAOjD,kFAAkF;AAClF,OAAO,EAAE,sBAAsB,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vantageos/cloud-identity — Organization-membership guard.
|
|
3
|
+
* (See package.json "version" for the release this shipped in.)
|
|
4
|
+
*
|
|
5
|
+
* Closes the second measured 0.2.0 defect: the published package exposed no
|
|
6
|
+
* "must belong to an organization" gate anywhere (`grep -rlE
|
|
7
|
+
* "requireOrg|orgRequired|orgId" package/dist/*.js` returned no matches on
|
|
8
|
+
* the 0.2.0 tarball). Absence of an organization must REFUSE, never fall
|
|
9
|
+
* through to full access — the same defect class as the scope-filter fix in
|
|
10
|
+
* `./scope-filter.ts`.
|
|
11
|
+
*
|
|
12
|
+
* The semantics are reused verbatim from a Convex implementation of the same
|
|
13
|
+
* guard rather than reinvented — a second implementation of one authorization
|
|
14
|
+
* check is a second chance to get it wrong. That original:
|
|
15
|
+
*
|
|
16
|
+
* - takes a Convex `QueryCtx | MutationCtx | ActionCtx`
|
|
17
|
+
* - derives identity via `ctx.auth.getUserIdentity()`
|
|
18
|
+
* - throws when there is no session
|
|
19
|
+
* - throws when `identity.org_id` is not a non-empty string
|
|
20
|
+
* - returns the org id
|
|
21
|
+
*
|
|
22
|
+
* ADAPTATION (named per the brief's requirement to flag any semantic
|
|
23
|
+
* deviation): this package is framework-agnostic and has no `ctx.auth`. The
|
|
24
|
+
* caller pre-resolves whichever identity/bearer object it already has
|
|
25
|
+
* (Clerk session identity for the human path, or the package's own
|
|
26
|
+
* `TenantContext` — see `./tenancy-domain.ts` — for the machine/bearer path)
|
|
27
|
+
* and passes it in as a `TenantSource`. The refuse-on-absence /
|
|
28
|
+
* refuse-on-empty semantics are otherwise identical to the source function;
|
|
29
|
+
* no other behavioural change was made.
|
|
30
|
+
*
|
|
31
|
+
* This is the membership half of the package's contract; tenant resolution,
|
|
32
|
+
* row filtering and identifier validation are covered by
|
|
33
|
+
* `getEffectiveTenantId`, `passesScopeFilter`/`scopeFilterList` and
|
|
34
|
+
* `validateMasterBearer` respectively. Framework adapters wrap
|
|
35
|
+
* `requireTenantId`; they must never reimplement its checks.
|
|
36
|
+
*/
|
|
37
|
+
import type { TenantContext } from "./tenancy-domain.js";
|
|
38
|
+
/**
|
|
39
|
+
* Minimal shape of a resolved human-path session identity. Intentionally
|
|
40
|
+
* framework-agnostic: the caller derives this from Clerk (or any other
|
|
41
|
+
* session provider) before calling `requireTenantId` — this package never
|
|
42
|
+
* imports a session SDK.
|
|
43
|
+
*/
|
|
44
|
+
export type SessionIdentity = {
|
|
45
|
+
orgId?: string | null;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Named deployment mode — 0.4.0. `cloud` requires an organization/workspace
|
|
49
|
+
* on every request (the existing, unchanged `session`/`bearer` behaviour
|
|
50
|
+
* below). `self-host` is single-tenant: the tenant id is DECLARED via
|
|
51
|
+
* explicit configuration, never inferred from the absence of an
|
|
52
|
+
* organization. See the `self-host` branch of `TenantSource` and
|
|
53
|
+
* `requireTenantId` for the refuse-on-missing-config semantics.
|
|
54
|
+
*/
|
|
55
|
+
export type DeploymentMode = "cloud" | "self-host";
|
|
56
|
+
/**
|
|
57
|
+
* Discriminated union covering both VantagePeers Cloud entry paths behind
|
|
58
|
+
* one contract:
|
|
59
|
+
* - `session`: human path, via a connection/login provider (Clerk, etc.).
|
|
60
|
+
* - `bearer`: machine path, via a resolved, ALREADY-VERIFIED token
|
|
61
|
+
* (`TenantContext` — see `decodeUnverifiedBearer` / the planned signed-JWT
|
|
62
|
+
* verifier for how a `TenantContext` gets produced).
|
|
63
|
+
* - `self-host`: single-tenant path (0.4.0, additive). The tenant id is
|
|
64
|
+
* presented explicitly by configuration, not inferred: `requireTenantId`
|
|
65
|
+
* returns `tenantId` only when it is a non-empty string, and throws
|
|
66
|
+
* otherwise — a right is never granted by absence, here either.
|
|
67
|
+
*/
|
|
68
|
+
export type TenantSource = {
|
|
69
|
+
kind: "session";
|
|
70
|
+
identity: SessionIdentity | null | undefined;
|
|
71
|
+
} | {
|
|
72
|
+
kind: "bearer";
|
|
73
|
+
context: TenantContext;
|
|
74
|
+
} | {
|
|
75
|
+
kind: "self-host";
|
|
76
|
+
tenantId: string | null | undefined;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the caller's tenant id, refusing when no organization/workspace is
|
|
80
|
+
* attached — for EITHER entry path, behind one contract.
|
|
81
|
+
*
|
|
82
|
+
* - `kind: "session"` and `identity` is null/undefined → throws
|
|
83
|
+
* ("Unauthenticated: no session.")
|
|
84
|
+
* - `kind: "session"` and `identity.orgId` is missing or empty → throws
|
|
85
|
+
* ("No active organization on this session...")
|
|
86
|
+
* - `kind: "bearer"` and `context.workspaceId` is missing or empty → throws
|
|
87
|
+
* ("No workspace on this bearer-resolved tenant context...")
|
|
88
|
+
* - otherwise → returns the tenant id (orgId, or workspaceId)
|
|
89
|
+
*
|
|
90
|
+
* A right is never granted by absence: every branch above refuses by
|
|
91
|
+
* throwing, there is no fallback branch that returns a value when
|
|
92
|
+
* organization/workspace information is missing.
|
|
93
|
+
*/
|
|
94
|
+
export declare function requireTenantId(source: TenantSource): string;
|
|
95
|
+
//# sourceMappingURL=org-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-guard.d.ts","sourceRoot":"","sources":["../src/org-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,WAAW,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,CAAC;AAE/D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA+B5D"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vantageos/cloud-identity — Organization-membership guard.
|
|
3
|
+
* (See package.json "version" for the release this shipped in.)
|
|
4
|
+
*
|
|
5
|
+
* Closes the second measured 0.2.0 defect: the published package exposed no
|
|
6
|
+
* "must belong to an organization" gate anywhere (`grep -rlE
|
|
7
|
+
* "requireOrg|orgRequired|orgId" package/dist/*.js` returned no matches on
|
|
8
|
+
* the 0.2.0 tarball). Absence of an organization must REFUSE, never fall
|
|
9
|
+
* through to full access — the same defect class as the scope-filter fix in
|
|
10
|
+
* `./scope-filter.ts`.
|
|
11
|
+
*
|
|
12
|
+
* The semantics are reused verbatim from a Convex implementation of the same
|
|
13
|
+
* guard rather than reinvented — a second implementation of one authorization
|
|
14
|
+
* check is a second chance to get it wrong. That original:
|
|
15
|
+
*
|
|
16
|
+
* - takes a Convex `QueryCtx | MutationCtx | ActionCtx`
|
|
17
|
+
* - derives identity via `ctx.auth.getUserIdentity()`
|
|
18
|
+
* - throws when there is no session
|
|
19
|
+
* - throws when `identity.org_id` is not a non-empty string
|
|
20
|
+
* - returns the org id
|
|
21
|
+
*
|
|
22
|
+
* ADAPTATION (named per the brief's requirement to flag any semantic
|
|
23
|
+
* deviation): this package is framework-agnostic and has no `ctx.auth`. The
|
|
24
|
+
* caller pre-resolves whichever identity/bearer object it already has
|
|
25
|
+
* (Clerk session identity for the human path, or the package's own
|
|
26
|
+
* `TenantContext` — see `./tenancy-domain.ts` — for the machine/bearer path)
|
|
27
|
+
* and passes it in as a `TenantSource`. The refuse-on-absence /
|
|
28
|
+
* refuse-on-empty semantics are otherwise identical to the source function;
|
|
29
|
+
* no other behavioural change was made.
|
|
30
|
+
*
|
|
31
|
+
* This is the membership half of the package's contract; tenant resolution,
|
|
32
|
+
* row filtering and identifier validation are covered by
|
|
33
|
+
* `getEffectiveTenantId`, `passesScopeFilter`/`scopeFilterList` and
|
|
34
|
+
* `validateMasterBearer` respectively. Framework adapters wrap
|
|
35
|
+
* `requireTenantId`; they must never reimplement its checks.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the caller's tenant id, refusing when no organization/workspace is
|
|
39
|
+
* attached — for EITHER entry path, behind one contract.
|
|
40
|
+
*
|
|
41
|
+
* - `kind: "session"` and `identity` is null/undefined → throws
|
|
42
|
+
* ("Unauthenticated: no session.")
|
|
43
|
+
* - `kind: "session"` and `identity.orgId` is missing or empty → throws
|
|
44
|
+
* ("No active organization on this session...")
|
|
45
|
+
* - `kind: "bearer"` and `context.workspaceId` is missing or empty → throws
|
|
46
|
+
* ("No workspace on this bearer-resolved tenant context...")
|
|
47
|
+
* - otherwise → returns the tenant id (orgId, or workspaceId)
|
|
48
|
+
*
|
|
49
|
+
* A right is never granted by absence: every branch above refuses by
|
|
50
|
+
* throwing, there is no fallback branch that returns a value when
|
|
51
|
+
* organization/workspace information is missing.
|
|
52
|
+
*/
|
|
53
|
+
export function requireTenantId(source) {
|
|
54
|
+
if (source.kind === "session") {
|
|
55
|
+
if (!source.identity) {
|
|
56
|
+
throw new Error("Unauthenticated: no session.");
|
|
57
|
+
}
|
|
58
|
+
const orgId = source.identity.orgId;
|
|
59
|
+
if (typeof orgId !== "string" || orgId.length === 0) {
|
|
60
|
+
throw new Error("No active organization on this session: select an organization before accessing agents.");
|
|
61
|
+
}
|
|
62
|
+
return orgId;
|
|
63
|
+
}
|
|
64
|
+
if (source.kind === "self-host") {
|
|
65
|
+
const tenantId = source.tenantId;
|
|
66
|
+
if (typeof tenantId !== "string" || tenantId.length === 0) {
|
|
67
|
+
throw new Error("No tenant id configured for self-host mode: declare a tenantId explicitly — it is never inferred from absence.");
|
|
68
|
+
}
|
|
69
|
+
return tenantId;
|
|
70
|
+
}
|
|
71
|
+
const workspaceId = source.context?.workspaceId;
|
|
72
|
+
if (typeof workspaceId !== "string" || workspaceId.length === 0) {
|
|
73
|
+
throw new Error("No workspace on this bearer-resolved tenant context: the bearer must resolve to a non-empty workspaceId.");
|
|
74
|
+
}
|
|
75
|
+
return workspaceId;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=org-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-guard.js","sourceRoot":"","sources":["../src/org-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAyCH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,MAAoB;IAClD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClD,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CACb,yFAAyF,CAC1F,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1D,MAAM,IAAI,KAAK,CACb,gHAAgH,CACjH,CAAC;QACJ,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC;IAChD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
package/dist/scope-filter.d.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Scope-aware row filter.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The behaviour was preserved verbatim from an earlier in-tree
|
|
5
|
+
* implementation; the only change was replacing a framework-specific context
|
|
6
|
+
* type with the framework-agnostic `OAuthCtx` from `./types`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Contract (0.3.0 — BREAKING vs. 0.2.0):
|
|
9
|
+
* - `oauthCtx` is now MANDATORY. A right is never granted by absence: the
|
|
10
|
+
* 0.2.0 behaviour of `oauthCtx === undefined` silently passing everything
|
|
11
|
+
* (as if the caller were master-scoped) was the exact defect class this
|
|
12
|
+
* release closes. Every exported function below now requires `OAuthCtx`
|
|
13
|
+
* at the type level — an omitted argument breaks the caller's build,
|
|
14
|
+
* it never falls through to a more permissive runtime path.
|
|
15
|
+
* - Callers that genuinely need the old legacy-bearer wildcard behaviour
|
|
16
|
+
* MUST import and pass `LEGACY_WILDCARD_CTX` explicitly — the compat
|
|
17
|
+
* path is requested by name, never inferred from a missing argument.
|
|
18
|
+
* - As defense-in-depth against callers that bypass the type system
|
|
19
|
+
* (untyped JS, `as any`), every function also throws at runtime if
|
|
20
|
+
* `oauthCtx` is nullish, instead of silently granting access.
|
|
13
21
|
* - Master scope (scope === "master" OR fromAllowList includes "*") = wildcard pass.
|
|
14
|
-
* - Legacy bearer (oauthCtx === undefined) = wildcard pass (treated as master-
|
|
15
|
-
* equivalent for backward-compatibility with mcpTenants Pi/Tau/Phi paths).
|
|
16
22
|
* - Non-master scope = row passes iff:
|
|
17
23
|
* row.createdBy ∈ oauthCtx.fromAllowList
|
|
18
24
|
* OR
|
|
@@ -24,6 +30,12 @@
|
|
|
24
30
|
* imports — to keep the helpers trivially unit-testable.
|
|
25
31
|
*/
|
|
26
32
|
import type { OAuthCtx } from "./types.js";
|
|
33
|
+
/**
|
|
34
|
+
* Explicit legacy-compat sentinel. Passing this constant reproduces the
|
|
35
|
+
* pre-0.3.0 "no oauthCtx" wildcard behaviour — but the caller must import
|
|
36
|
+
* and pass it BY NAME. It is never inferred from an omitted argument.
|
|
37
|
+
*/
|
|
38
|
+
export declare const LEGACY_WILDCARD_CTX: OAuthCtx;
|
|
27
39
|
/**
|
|
28
40
|
* Row shape accepted by the scope filter. All fields optional because real
|
|
29
41
|
* Convex documents from list_peers / list_messages / etc. don't all carry both.
|
|
@@ -37,35 +49,38 @@ export type ScopeFilterable = {
|
|
|
37
49
|
* Mirrors `isMasterScope` from the source module: `scope === "master"` OR
|
|
38
50
|
* `fromAllowList` contains the `"*"` sentinel.
|
|
39
51
|
*/
|
|
40
|
-
export declare function isMasterScope(ctx: OAuthCtx
|
|
52
|
+
export declare function isMasterScope(ctx: OAuthCtx): boolean;
|
|
41
53
|
/**
|
|
42
54
|
* Core predicate. Returns true when the row is visible to the caller.
|
|
43
55
|
*
|
|
44
56
|
* - Master scope (isMasterScope === true) → true (wildcard)
|
|
45
|
-
* - Legacy bearer (oauthCtx === undefined) → true (back-compat)
|
|
46
57
|
* - row.createdBy ∈ oauthCtx.fromAllowList → true
|
|
47
58
|
* - row.namespace === prefix OR startsWith prefix+'/'
|
|
48
59
|
* for any prefix ∈ oauthCtx.namespaceReadPrefixes → true
|
|
49
60
|
* - otherwise → false
|
|
50
61
|
*
|
|
62
|
+
* `oauthCtx` is mandatory (see module-level contract note above): an omitted
|
|
63
|
+
* argument throws instead of silently granting wildcard access. Callers that
|
|
64
|
+
* need the old legacy-bearer wildcard behaviour must pass `LEGACY_WILDCARD_CTX`
|
|
65
|
+
* explicitly.
|
|
66
|
+
*
|
|
51
67
|
* Substring matches that don't fall on a '/' boundary are explicitly rejected
|
|
52
68
|
* (e.g. namespace="orchestrator/alphabet" does NOT match prefix
|
|
53
69
|
* "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
|
|
54
70
|
*/
|
|
55
|
-
export declare function passesScopeFilter(oauthCtx: OAuthCtx
|
|
71
|
+
export declare function passesScopeFilter(oauthCtx: OAuthCtx, row: ScopeFilterable): boolean;
|
|
56
72
|
/**
|
|
57
73
|
* Filter a list of rows (post-query). Used by list_* tools.
|
|
58
74
|
*/
|
|
59
|
-
export declare function scopeFilterList<T extends ScopeFilterable>(oauthCtx: OAuthCtx
|
|
75
|
+
export declare function scopeFilterList<T extends ScopeFilterable>(oauthCtx: OAuthCtx, rows: T[]): T[];
|
|
60
76
|
/**
|
|
61
77
|
* Assert a single row passes (get_* tools). Returns the row when allowed, null
|
|
62
78
|
* otherwise — callers translate null to a 404-equivalent "not found" MCP error
|
|
63
79
|
* to avoid leaking the difference between "absent" and "filtered out".
|
|
64
80
|
*/
|
|
65
|
-
export declare function scopeFilterGet<T extends ScopeFilterable>(oauthCtx: OAuthCtx
|
|
81
|
+
export declare function scopeFilterGet<T extends ScopeFilterable>(oauthCtx: OAuthCtx, row: T | null | undefined): T | null;
|
|
66
82
|
/**
|
|
67
83
|
* Helper for callers that want a single "is master / legacy" predicate.
|
|
68
|
-
* Mirrors the legacy-bearer-passes-through convention.
|
|
69
84
|
*/
|
|
70
|
-
export declare function isWildcardScope(ctx: OAuthCtx
|
|
85
|
+
export declare function isWildcardScope(ctx: OAuthCtx): boolean;
|
|
71
86
|
//# sourceMappingURL=scope-filter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-filter.d.ts","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"scope-filter.d.ts","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;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;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,eAAe,GACnB,OAAO,CAYT;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,CAAC,EAAE,GACR,CAAC,EAAE,CAGL;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACxB,CAAC,GAAG,IAAI,CAIV;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAGtD"}
|
package/dist/scope-filter.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Scope-aware row filter.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The behaviour was preserved verbatim from an earlier in-tree
|
|
5
|
+
* implementation; the only change was replacing a framework-specific context
|
|
6
|
+
* type with the framework-agnostic `OAuthCtx` from `./types`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Contract (0.3.0 — BREAKING vs. 0.2.0):
|
|
9
|
+
* - `oauthCtx` is now MANDATORY. A right is never granted by absence: the
|
|
10
|
+
* 0.2.0 behaviour of `oauthCtx === undefined` silently passing everything
|
|
11
|
+
* (as if the caller were master-scoped) was the exact defect class this
|
|
12
|
+
* release closes. Every exported function below now requires `OAuthCtx`
|
|
13
|
+
* at the type level — an omitted argument breaks the caller's build,
|
|
14
|
+
* it never falls through to a more permissive runtime path.
|
|
15
|
+
* - Callers that genuinely need the old legacy-bearer wildcard behaviour
|
|
16
|
+
* MUST import and pass `LEGACY_WILDCARD_CTX` explicitly — the compat
|
|
17
|
+
* path is requested by name, never inferred from a missing argument.
|
|
18
|
+
* - As defense-in-depth against callers that bypass the type system
|
|
19
|
+
* (untyped JS, `as any`), every function also throws at runtime if
|
|
20
|
+
* `oauthCtx` is nullish, instead of silently granting access.
|
|
13
21
|
* - Master scope (scope === "master" OR fromAllowList includes "*") = wildcard pass.
|
|
14
|
-
* - Legacy bearer (oauthCtx === undefined) = wildcard pass (treated as master-
|
|
15
|
-
* equivalent for backward-compatibility with mcpTenants Pi/Tau/Phi paths).
|
|
16
22
|
* - Non-master scope = row passes iff:
|
|
17
23
|
* row.createdBy ∈ oauthCtx.fromAllowList
|
|
18
24
|
* OR
|
|
@@ -23,14 +29,30 @@
|
|
|
23
29
|
* NOTE: this module is intentionally framework-agnostic — no McpServer / Hono
|
|
24
30
|
* imports — to keep the helpers trivially unit-testable.
|
|
25
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* Explicit legacy-compat sentinel. Passing this constant reproduces the
|
|
34
|
+
* pre-0.3.0 "no oauthCtx" wildcard behaviour — but the caller must import
|
|
35
|
+
* and pass it BY NAME. It is never inferred from an omitted argument.
|
|
36
|
+
*/
|
|
37
|
+
export const LEGACY_WILDCARD_CTX = {
|
|
38
|
+
scope: "master",
|
|
39
|
+
fromAllowList: ["*"],
|
|
40
|
+
namespaceReadPrefixes: [],
|
|
41
|
+
namespaceWritePrefixes: [],
|
|
42
|
+
};
|
|
43
|
+
function assertOauthCtx(oauthCtx) {
|
|
44
|
+
if (oauthCtx == null) {
|
|
45
|
+
throw new TypeError("oauthCtx is required — pass an explicit OAuthCtx, or LEGACY_WILDCARD_CTX " +
|
|
46
|
+
"if you deliberately want the pre-0.3.0 legacy-bearer wildcard behaviour.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
26
49
|
/**
|
|
27
50
|
* Returns true when the scope profile grants full, wildcard access.
|
|
28
51
|
* Mirrors `isMasterScope` from the source module: `scope === "master"` OR
|
|
29
52
|
* `fromAllowList` contains the `"*"` sentinel.
|
|
30
53
|
*/
|
|
31
54
|
export function isMasterScope(ctx) {
|
|
32
|
-
|
|
33
|
-
return false;
|
|
55
|
+
assertOauthCtx(ctx);
|
|
34
56
|
if (ctx.scope === "master")
|
|
35
57
|
return true;
|
|
36
58
|
return ctx.fromAllowList.includes("*");
|
|
@@ -39,19 +61,22 @@ export function isMasterScope(ctx) {
|
|
|
39
61
|
* Core predicate. Returns true when the row is visible to the caller.
|
|
40
62
|
*
|
|
41
63
|
* - Master scope (isMasterScope === true) → true (wildcard)
|
|
42
|
-
* - Legacy bearer (oauthCtx === undefined) → true (back-compat)
|
|
43
64
|
* - row.createdBy ∈ oauthCtx.fromAllowList → true
|
|
44
65
|
* - row.namespace === prefix OR startsWith prefix+'/'
|
|
45
66
|
* for any prefix ∈ oauthCtx.namespaceReadPrefixes → true
|
|
46
67
|
* - otherwise → false
|
|
47
68
|
*
|
|
69
|
+
* `oauthCtx` is mandatory (see module-level contract note above): an omitted
|
|
70
|
+
* argument throws instead of silently granting wildcard access. Callers that
|
|
71
|
+
* need the old legacy-bearer wildcard behaviour must pass `LEGACY_WILDCARD_CTX`
|
|
72
|
+
* explicitly.
|
|
73
|
+
*
|
|
48
74
|
* Substring matches that don't fall on a '/' boundary are explicitly rejected
|
|
49
75
|
* (e.g. namespace="orchestrator/alphabet" does NOT match prefix
|
|
50
76
|
* "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
|
|
51
77
|
*/
|
|
52
78
|
export function passesScopeFilter(oauthCtx, row) {
|
|
53
|
-
|
|
54
|
-
return true;
|
|
79
|
+
assertOauthCtx(oauthCtx);
|
|
55
80
|
if (isMasterScope(oauthCtx))
|
|
56
81
|
return true;
|
|
57
82
|
const { createdBy, namespace } = row;
|
|
@@ -71,6 +96,7 @@ export function passesScopeFilter(oauthCtx, row) {
|
|
|
71
96
|
* Filter a list of rows (post-query). Used by list_* tools.
|
|
72
97
|
*/
|
|
73
98
|
export function scopeFilterList(oauthCtx, rows) {
|
|
99
|
+
assertOauthCtx(oauthCtx);
|
|
74
100
|
return rows.filter((r) => passesScopeFilter(oauthCtx, r));
|
|
75
101
|
}
|
|
76
102
|
/**
|
|
@@ -79,17 +105,16 @@ export function scopeFilterList(oauthCtx, rows) {
|
|
|
79
105
|
* to avoid leaking the difference between "absent" and "filtered out".
|
|
80
106
|
*/
|
|
81
107
|
export function scopeFilterGet(oauthCtx, row) {
|
|
108
|
+
assertOauthCtx(oauthCtx);
|
|
82
109
|
if (row == null)
|
|
83
110
|
return null;
|
|
84
111
|
return passesScopeFilter(oauthCtx, row) ? row : null;
|
|
85
112
|
}
|
|
86
113
|
/**
|
|
87
114
|
* Helper for callers that want a single "is master / legacy" predicate.
|
|
88
|
-
* Mirrors the legacy-bearer-passes-through convention.
|
|
89
115
|
*/
|
|
90
116
|
export function isWildcardScope(ctx) {
|
|
91
|
-
|
|
92
|
-
return true;
|
|
117
|
+
assertOauthCtx(ctx);
|
|
93
118
|
return isMasterScope(ctx);
|
|
94
119
|
}
|
|
95
120
|
//# sourceMappingURL=scope-filter.js.map
|
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
|
|
1
|
+
{"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;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;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAkB,EAClB,GAAoB;IAEpB,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,SAAS,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACjC,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC;QACjD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAkB,EAClB,IAAS;IAET,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzB,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAkB,EAClB,GAAyB;IAEzB,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzB,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7B,OAAO,iBAAiB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,GAAa;IAC3C,cAAc,CAAC,GAAG,CAAC,CAAC;IACpB,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC"}
|
package/dist/tenancy-domain.d.ts
CHANGED
|
@@ -12,12 +12,8 @@
|
|
|
12
12
|
* - getEffectiveTenantId(ctx, args) — multi-tenant isolation guard
|
|
13
13
|
* - decodeUnverifiedBearer(token) — DECODE-ONLY helper, NOT authentication.
|
|
14
14
|
* The decoded payload is attacker-controlled. Production code MUST use a
|
|
15
|
-
* signed JWT or opaque-token lookup
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Task: k177hejb4tc5em5p70m9hxwn3x88kyp4
|
|
19
|
-
* Mission: k57b4t2q VR Cloud MVP Day 100
|
|
20
|
-
* Orchestrator: Sigma — VantagePeers | 2026-06-13
|
|
15
|
+
* signed JWT or opaque-token lookup before treating the payload as a
|
|
16
|
+
* trust boundary.
|
|
21
17
|
*/
|
|
22
18
|
import { z } from "zod";
|
|
23
19
|
export declare const workspaceRoleSchema: z.ZodEnum<{
|
|
@@ -110,8 +106,8 @@ export declare function getEffectiveTenantId(ctx: TenantContext, args: {
|
|
|
110
106
|
* middleware (the production trust boundary)
|
|
111
107
|
*
|
|
112
108
|
* For the production trust boundary, callers MUST substitute a signed JWT
|
|
113
|
-
* (
|
|
114
|
-
* tenancy table.
|
|
109
|
+
* verifier (not yet exported by this package) or an opaque-token lookup
|
|
110
|
+
* against a Convex tenancy table.
|
|
115
111
|
*
|
|
116
112
|
* Renamed from `resolveBearer` (0.2.0 pre-release) to make the lack of
|
|
117
113
|
* verification impossible to miss at the call-site.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenancy-domain.d.ts","sourceRoot":"","sources":["../src/tenancy-domain.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tenancy-domain.d.ts","sourceRoot":"","sources":["../src/tenancy-domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,mBAAmB;;;;EAAwC,CAAC;AACzE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMhE,eAAO,MAAM,eAAe;;;;;iBAK1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAMxD,eAAO,MAAM,qBAAqB;;;;;;;;;iBAKhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAMpE,eAAO,MAAM,mBAAmB;;;;;;;;iBAI9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMhE,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAG,iBAAiB,CAAU;IAC3C,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;gBAE5B,OAAO,EAAE,qBAAqB;CAS3C;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5B,MAAM,CASR;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,QAAA,MAAM,mBAAmB;;;;;;;;iBAIvB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,CAAC,CAmBxB"}
|