@vantageos/cloud-identity 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
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
+ * Discriminated union covering both VantagePeers Cloud entry paths behind
49
+ * one contract:
50
+ * - `session`: human path, via a connection/login provider (Clerk, etc.).
51
+ * - `bearer`: machine path, via a resolved, ALREADY-VERIFIED token
52
+ * (`TenantContext` — see `decodeUnverifiedBearer` / the planned signed-JWT
53
+ * verifier for how a `TenantContext` gets produced).
54
+ */
55
+ export type TenantSource = {
56
+ kind: "session";
57
+ identity: SessionIdentity | null | undefined;
58
+ } | {
59
+ kind: "bearer";
60
+ context: TenantContext;
61
+ };
62
+ /**
63
+ * Resolve the caller's tenant id, refusing when no organization/workspace is
64
+ * attached — for EITHER entry path, behind one contract.
65
+ *
66
+ * - `kind: "session"` and `identity` is null/undefined → throws
67
+ * ("Unauthenticated: no session.")
68
+ * - `kind: "session"` and `identity.orgId` is missing or empty → throws
69
+ * ("No active organization on this session...")
70
+ * - `kind: "bearer"` and `context.workspaceId` is missing or empty → throws
71
+ * ("No workspace on this bearer-resolved tenant context...")
72
+ * - otherwise → returns the tenant id (orgId, or workspaceId)
73
+ *
74
+ * A right is never granted by absence: every branch above refuses by
75
+ * throwing, there is no fallback branch that returns a value when
76
+ * organization/workspace information is missing.
77
+ */
78
+ export declare function requireTenantId(source: TenantSource): string;
79
+ //# 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,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,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAqB5D"}
@@ -0,0 +1,70 @@
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
+ const workspaceId = source.context?.workspaceId;
65
+ if (typeof workspaceId !== "string" || workspaceId.length === 0) {
66
+ throw new Error("No workspace on this bearer-resolved tenant context: the bearer must resolve to a non-empty workspaceId.");
67
+ }
68
+ return workspaceId;
69
+ }
70
+ //# 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;AA0BH;;;;;;;;;;;;;;;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,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"}
@@ -1,18 +1,24 @@
1
1
  /**
2
- * Sprint S3.1 B2 — scope-aware filter helpers for VP MCP tools.
2
+ * Scope-aware row filter.
3
3
  *
4
- * Ported from `vantage-memory/mcp-server/src/scope-filter.ts`. Behaviour is
5
- * preserved verbatim; only the type dependency on the in-tree `OAuthContext`
6
- * is replaced by the framework-agnostic `OAuthCtx` from `./types`.
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
- * Doctrine references:
9
- * - decisions/doctrine-scope-aware-filter-2026-05-26.md (D3 base)
10
- * - memory j579y6f31g7xzgtgdnpgetdmjx87ztyj (D9-D14 extension)
11
- *
12
- * Contract:
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 | undefined): boolean;
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 | undefined, row: ScopeFilterable): boolean;
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 | undefined, rows: T[]): T[];
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 | undefined, row: T | null | undefined): T | null;
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 | undefined): boolean;
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;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;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,SAAS,GAAG,OAAO,CAIhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,GAAG,EAAE,eAAe,GACnB,OAAO,CAYT;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,IAAI,EAAE,CAAC,EAAE,GACR,CAAC,EAAE,CAEL;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,GAAG,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACxB,CAAC,GAAG,IAAI,CAGV;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAGlE"}
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"}
@@ -1,18 +1,24 @@
1
1
  /**
2
- * Sprint S3.1 B2 — scope-aware filter helpers for VP MCP tools.
2
+ * Scope-aware row filter.
3
3
  *
4
- * Ported from `vantage-memory/mcp-server/src/scope-filter.ts`. Behaviour is
5
- * preserved verbatim; only the type dependency on the in-tree `OAuthContext`
6
- * is replaced by the framework-agnostic `OAuthCtx` from `./types`.
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
- * Doctrine references:
9
- * - decisions/doctrine-scope-aware-filter-2026-05-26.md (D3 base)
10
- * - memory j579y6f31g7xzgtgdnpgetdmjx87ztyj (D9-D14 extension)
11
- *
12
- * Contract:
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
- if (!ctx)
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
- if (!oauthCtx)
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
- if (!ctx)
92
- return true;
117
+ assertOauthCtx(ctx);
93
118
  return isMasterScope(ctx);
94
119
  }
95
120
  //# sourceMappingURL=scope-filter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAaH;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,GAAyB;IACrD,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,IAAI,GAAG,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,GAAG,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAA8B,EAC9B,GAAoB;IAEpB,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,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,QAA8B,EAC9B,IAAS;IAET,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,QAA8B,EAC9B,GAAyB;IAEzB,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;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAAyB;IACvD,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC"}
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"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @vantageos/cloud-identity 0.2.0 — Domain-tenancy layer.
3
+ *
4
+ * Additive-only: does not modify any 0.1.0 exports.
5
+ *
6
+ * Provides:
7
+ * - WorkspaceRole literal union + Zod schema
8
+ * - Workspace type + Zod schema
9
+ * - WorkspaceMember type + Zod schema
10
+ * - TenantContext type + Zod schema
11
+ * - ScopeViolationError class (code: "SCOPE_VIOLATION")
12
+ * - getEffectiveTenantId(ctx, args) — multi-tenant isolation guard
13
+ * - decodeUnverifiedBearer(token) — DECODE-ONLY helper, NOT authentication.
14
+ * The decoded payload is attacker-controlled. Production code MUST use a
15
+ * signed JWT or opaque-token lookup before treating the payload as a
16
+ * trust boundary.
17
+ */
18
+ import { z } from "zod";
19
+ export declare const workspaceRoleSchema: z.ZodEnum<{
20
+ Admin: "Admin";
21
+ Editor: "Editor";
22
+ Viewer: "Viewer";
23
+ }>;
24
+ export type WorkspaceRole = z.infer<typeof workspaceRoleSchema>;
25
+ export declare const workspaceSchema: z.ZodObject<{
26
+ id: z.ZodString;
27
+ name: z.ZodString;
28
+ createdAt: z.ZodNumber;
29
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
30
+ }, z.core.$strip>;
31
+ export type Workspace = z.infer<typeof workspaceSchema>;
32
+ export declare const workspaceMemberSchema: z.ZodObject<{
33
+ userId: z.ZodString;
34
+ workspaceId: z.ZodString;
35
+ role: z.ZodEnum<{
36
+ Admin: "Admin";
37
+ Editor: "Editor";
38
+ Viewer: "Viewer";
39
+ }>;
40
+ joinedAt: z.ZodNumber;
41
+ }, z.core.$strip>;
42
+ export type WorkspaceMember = z.infer<typeof workspaceMemberSchema>;
43
+ export declare const tenantContextSchema: z.ZodObject<{
44
+ workspaceId: z.ZodString;
45
+ userId: z.ZodString;
46
+ roles: z.ZodArray<z.ZodEnum<{
47
+ Admin: "Admin";
48
+ Editor: "Editor";
49
+ Viewer: "Viewer";
50
+ }>>;
51
+ }, z.core.$strip>;
52
+ export type TenantContext = z.infer<typeof tenantContextSchema>;
53
+ export interface ScopeViolationPayload {
54
+ requestedTenantId: string;
55
+ contextTenantId: string;
56
+ reason: string;
57
+ }
58
+ /**
59
+ * Thrown by `getEffectiveTenantId` when `args.workspaceId` does not match the
60
+ * workspaceId encoded in the bearer-resolved `TenantContext`. This error is the
61
+ * canonical cross-tenant isolation signal across the VantagePeers Cloud fleet.
62
+ *
63
+ * Callers MUST translate this to a 403 or MCP permission-denied — never 404.
64
+ */
65
+ export declare class ScopeViolationError extends Error {
66
+ readonly code: "SCOPE_VIOLATION";
67
+ readonly payload: ScopeViolationPayload;
68
+ constructor(payload: ScopeViolationPayload);
69
+ }
70
+ /**
71
+ * Resolve the effective tenant workspace ID from a `TenantContext` + request args.
72
+ *
73
+ * If `args.workspaceId` differs from the context's `workspaceId` (derived from
74
+ * the bearer token), a `ScopeViolationError` is thrown immediately.
75
+ *
76
+ * This is the primary multi-tenant isolation gate — call it at the top of every
77
+ * MCP tool handler that accepts a `workspaceId` argument.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const tenantId = getEffectiveTenantId(ctx, args);
82
+ * // safe to use tenantId for DB queries — caller's scope is verified
83
+ * ```
84
+ */
85
+ export declare function getEffectiveTenantId(ctx: TenantContext, args: {
86
+ workspaceId: string;
87
+ }): string;
88
+ /**
89
+ * @security ⚠️ DECODE-ONLY. NOT AUTHENTICATION. NOT A TRUST BOUNDARY.
90
+ *
91
+ * Decodes a `base64(JSON({ userId, workspaceId, roles[] }))` payload and
92
+ * validates its SHAPE with Zod. It performs NO signature verification, NO
93
+ * issuer check, NO expiry check, NO replay-attack protection. The decoded
94
+ * payload is attacker-controlled and MUST NOT be used as the source of
95
+ * truth for multi-tenant isolation.
96
+ *
97
+ * Forgery example: any attacker producing
98
+ * base64(JSON({ userId:"x", workspaceId:"victim-org", roles:["Admin"] }))
99
+ * passes this function. Treating the returned `workspaceId` as authoritative
100
+ * — for example feeding it into `getEffectiveTenantId` as the trusted
101
+ * `ctx.workspaceId` — silently grants cross-tenant access.
102
+ *
103
+ * Use only as:
104
+ * - test fixture / harness helper
105
+ * - decoder for a bearer that was ALREADY verified by an upstream signed-JWT
106
+ * middleware (the production trust boundary)
107
+ *
108
+ * For the production trust boundary, callers MUST substitute a signed JWT
109
+ * verifier (not yet exported by this package) or an opaque-token lookup
110
+ * against a Convex tenancy table.
111
+ *
112
+ * Renamed from `resolveBearer` (0.2.0 pre-release) to make the lack of
113
+ * verification impossible to miss at the call-site.
114
+ */
115
+ declare const bearerPayloadSchema: z.ZodObject<{
116
+ userId: z.ZodString;
117
+ workspaceId: z.ZodString;
118
+ roles: z.ZodArray<z.ZodEnum<{
119
+ Admin: "Admin";
120
+ Editor: "Editor";
121
+ Viewer: "Viewer";
122
+ }>>;
123
+ }, z.core.$strip>;
124
+ export type BearerPayload = z.infer<typeof bearerPayloadSchema>;
125
+ export declare function decodeUnverifiedBearer(token: string): Promise<BearerPayload>;
126
+ export {};
127
+ //# sourceMappingURL=tenancy-domain.d.ts.map
@@ -0,0 +1 @@
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"}