@vantageos/cloud-identity 0.1.0 → 0.2.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 CHANGED
@@ -3,6 +3,30 @@
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.2.0] - 2026-06-13
7
+
8
+ Additive release — domain-tenancy layer. No breaking changes. All 0.1.0 exports unchanged.
9
+
10
+ ### Added
11
+
12
+ - `WorkspaceRole` literal union `"Admin" | "Editor" | "Viewer"` + `workspaceRoleSchema` (Zod).
13
+ - `Workspace` type + `workspaceSchema` (Zod) — `{ id, name, createdAt, metadata? }`.
14
+ - `WorkspaceMember` type + `workspaceMemberSchema` (Zod) — `{ userId, workspaceId, role, joinedAt }`.
15
+ - `TenantContext` type + `tenantContextSchema` (Zod) — `{ workspaceId, userId, roles[] }`.
16
+ - `ScopeViolationError` class — `code: "SCOPE_VIOLATION"`, payload `{ requestedTenantId, contextTenantId, reason }`. Canonical cross-tenant isolation signal — translate to 403 or MCP permission-denied.
17
+ - `getEffectiveTenantId(ctx, args)` — multi-tenant isolation guard. Throws `ScopeViolationError` when `args.workspaceId !== ctx.workspaceId`; returns `ctx.workspaceId` on match.
18
+ - `decodeUnverifiedBearer(token)` — async DECODER for `base64(JSON({ userId, workspaceId, roles[] }))` into `BearerPayload`. Validates SHAPE via Zod and throws on shape or decode failure. **⚠️ NOT authentication. NOT a trust boundary.** Performs NO signature verification, NO issuer check, NO expiry check. The decoded payload is attacker-controlled — see the "Security: bearer decoding" section of README. Use as a test fixture or as a decoder for a bearer already verified by an upstream signed-JWT / opaque-token middleware. The production trust boundary (signed JWT + verification) is planned for 0.3.0. Renamed from the original `resolveBearer` (0.2.0 pre-release) per Eta security review — the new name makes the lack of verification impossible to miss at the call-site.
19
+ - `BearerPayload` type (inferred from Zod schema).
20
+ - `ScopeViolationPayload` interface.
21
+ - New subpath export: `@vantageos/cloud-identity/tenancy-domain`.
22
+ - `zod` added as a production dependency (schema validation for domain types).
23
+
24
+ ### Notes
25
+
26
+ - All new symbols are also re-exported from the main `@vantageos/cloud-identity` entry point.
27
+ - `decodeUnverifiedBearer` uses base64-JSON as a lightweight format and is intentionally NOT an authentication primitive. Production callers MUST run a SIGNATURE-VERIFYING middleware (signed JWT / Clerk session / opaque-token lookup) before constructing the `TenantContext` passed to `getEffectiveTenantId`.
28
+ - `getEffectiveTenantId` expects a `TenantContext` whose `workspaceId` came from a signature-verified source. Feeding it the output of `decodeUnverifiedBearer` directly is a security bug — see README "Security: bearer decoding".
29
+
6
30
  ## [0.1.0] - 2026-06-04
7
31
 
8
32
  Initial release.
package/README.md CHANGED
@@ -17,7 +17,8 @@ npm install @vantageos/cloud-identity
17
17
  | `@vantageos/cloud-identity/crypto` | `timingSafeEqual` | Constant-time byte comparison (no branch-timing leak). |
18
18
  | `@vantageos/cloud-identity/scope-filter` | `passesScopeFilter`, `scopeFilterList`, `scopeFilterGet` | Row-level visibility filter on `(createdBy, namespace)` using the caller's OAuth scope. |
19
19
  | `@vantageos/cloud-identity/bearer-validation` | `validateMasterBearer` | Bearer-token parsing + sha256 constant-time match against a configured master secret. |
20
- | `@vantageos/cloud-identity/types` | `OAuthCtx`, `ScopeProfile`, `NamespacePrefix`, `FromAllowListEntry`, `ValidateMasterBearerResult` | Public type surface. |
20
+ | `@vantageos/cloud-identity/types` | `OAuthCtx`, `ScopeProfile`, `NamespacePrefix`, `FromAllowListEntry`, `ValidateMasterBearerResult` | Public type surface (0.1.0). |
21
+ | `@vantageos/cloud-identity/tenancy-domain` | `Workspace`, `WorkspaceMember`, `WorkspaceRole`, `TenantContext`, `workspaceSchema`, `workspaceMemberSchema`, `workspaceRoleSchema`, `tenantContextSchema`, `ScopeViolationError`, `getEffectiveTenantId`, `decodeUnverifiedBearer` | Domain-tenancy layer (0.2.0) — multi-tenant isolation guard + Zod schemas + bearer DECODER (not auth — see security note). |
21
22
 
22
23
  ## 0.1.0 scope
23
24
 
@@ -28,10 +29,21 @@ This release ships the **transport-agnostic** core:
28
29
  - `bearer-validation.validateMasterBearer`
29
30
  - types
30
31
 
31
- ## 0.2.0 roadmap
32
+ ## 0.2.0 domain-tenancy layer
33
+
34
+ Released 2026-06-13. Additive — no breaking changes.
35
+
36
+ New in 0.2.0:
37
+ - `WorkspaceRole` literal union + Zod schema
38
+ - `Workspace` + `WorkspaceMember` + `TenantContext` types + Zod schemas
39
+ - `ScopeViolationError` — canonical cross-tenant isolation error
40
+ - `getEffectiveTenantId(ctx, args)` — multi-tenant guard (call at the top of every tool handler)
41
+ - `decodeUnverifiedBearer(token)` — DECODE-ONLY helper. ⚠️ NOT authentication. NOT a trust boundary. See [Security: bearer decoding](#security-bearer-decoding) below.
42
+
43
+ ## 0.3.0 roadmap
32
44
 
33
- - `getEffectiveWorkspaceId` (workspace resolution helper, sourced from `vantageos-crm`).
34
45
  - D1 ghost Clerk identity injection.
46
+ - Signed-JWT bearer verification (replacing the decode-only helper as the production trust boundary).
35
47
  - Co-designed in a Theta cycle.
36
48
 
37
49
  ## Usage examples
@@ -76,6 +88,100 @@ if (!res.ok) {
76
88
  }
77
89
  ```
78
90
 
91
+ ### Domain-tenancy layer (0.2.0)
92
+
93
+ #### getEffectiveTenantId — multi-tenant isolation guard
94
+
95
+ ```ts
96
+ import { getEffectiveTenantId, ScopeViolationError } from "@vantageos/cloud-identity/tenancy-domain";
97
+ import type { TenantContext } from "@vantageos/cloud-identity/tenancy-domain";
98
+
99
+ // 1. Obtain ctx from your VERIFIED auth middleware. Examples:
100
+ // - Clerk session (clerkClient.verifyToken + claims → TenantContext)
101
+ // - signed-JWT middleware (jose / jsonwebtoken with public key)
102
+ // - Convex tenancy-table lookup against an opaque token
103
+ // Whatever path: ctx.workspaceId MUST come from a SIGNATURE-VERIFIED source.
104
+ const ctx: TenantContext = await verifiedAuthMiddleware(request);
105
+
106
+ // 2. At the top of every tool handler that receives a workspaceId argument:
107
+ try {
108
+ const tenantId = getEffectiveTenantId(ctx, args);
109
+ // tenantId is safe — caller's verified ctx matches the requested workspace
110
+ const rows = await db.list({ workspaceId: tenantId });
111
+ } catch (err) {
112
+ if (err instanceof ScopeViolationError) {
113
+ // Translate to 403 — do NOT expose details to caller
114
+ return new Response("forbidden", { status: 403 });
115
+ }
116
+ throw err;
117
+ }
118
+ ```
119
+
120
+ #### decodeUnverifiedBearer — DECODE-ONLY helper
121
+
122
+ > ⚠️ **NOT AUTHENTICATION. NOT A TRUST BOUNDARY.** See the [Security: bearer decoding](#security-bearer-decoding) section below.
123
+
124
+ ```ts
125
+ import { decodeUnverifiedBearer } from "@vantageos/cloud-identity/tenancy-domain";
126
+
127
+ // Use cases:
128
+ // (a) tests / fixtures
129
+ // (b) shape-inspection of a payload that was ALREADY verified upstream
130
+ //
131
+ // Do NOT feed the returned `workspaceId` into getEffectiveTenantId as the
132
+ // trusted `ctx.workspaceId` — the payload is attacker-controlled.
133
+
134
+ const token = Buffer.from(JSON.stringify({
135
+ userId: "user_alice",
136
+ workspaceId: "ws_prod",
137
+ roles: ["Admin"],
138
+ })).toString("base64");
139
+
140
+ const { userId, workspaceId, roles } = await decodeUnverifiedBearer(token);
141
+ ```
142
+
143
+ #### Zod schemas — validate workspace objects at boundaries
144
+
145
+ ```ts
146
+ import { workspaceSchema, tenantContextSchema } from "@vantageos/cloud-identity/tenancy-domain";
147
+
148
+ // Validate at API boundary
149
+ const ws = workspaceSchema.parse(requestBody);
150
+
151
+ // Validate TenantContext from an external JWT payload
152
+ const ctx = tenantContextSchema.parse(jwtPayload);
153
+ ```
154
+
155
+ ## Security: bearer decoding
156
+
157
+ `decodeUnverifiedBearer` performs **no signature verification, no issuer check, no expiry check, no replay-attack protection**. The decoded payload is **attacker-controlled**.
158
+
159
+ Forgery example:
160
+
161
+ ```ts
162
+ // Any attacker can produce this token — no key, no signature required.
163
+ const forged = Buffer.from(JSON.stringify({
164
+ userId: "x",
165
+ workspaceId: "victim-org", // crosses tenant boundary
166
+ roles: ["Admin"], // elevates privileges
167
+ })).toString("base64");
168
+
169
+ const payload = await decodeUnverifiedBearer(forged); // accepts it
170
+ ```
171
+
172
+ Feeding the decoded `workspaceId` into `getEffectiveTenantId` as the trusted `ctx.workspaceId` silently grants cross-tenant access — the multi-tenant isolation guard collapses because both `ctx` and `args` are then attacker-controlled.
173
+
174
+ **Use only as**:
175
+ - a test fixture / harness helper, OR
176
+ - a decoder for a bearer that was ALREADY verified by an upstream signed-JWT or opaque-token middleware (the production trust boundary).
177
+
178
+ **Production trust boundary** is one of:
179
+ - Clerk session verification (planned 0.3.0 D1 ghost identity injection)
180
+ - signed JWT with rotating issuer key (planned 0.3.0)
181
+ - opaque-token lookup against a Convex tenancy table
182
+
183
+ Do **not** publish a service that treats `decodeUnverifiedBearer` as authentication.
184
+
79
185
  ## Security doctrine
80
186
 
81
187
  Canonical reference: `docs/cloud/security-multi-tenant.md` in the `vantage-peers` repo.
package/dist/index.d.ts CHANGED
@@ -9,4 +9,6 @@ export { timingSafeEqual } from "./crypto.js";
9
9
  export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, 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
+ export { workspaceSchema, workspaceMemberSchema, workspaceRoleSchema, tenantContextSchema, ScopeViolationError, getEffectiveTenantId, decodeUnverifiedBearer, } from "./tenancy-domain.js";
13
+ export type { Workspace, WorkspaceMember, WorkspaceRole, TenantContext, ScopeViolationPayload, BearerPayload, } from "./tenancy-domain.js";
12
14
  //# sourceMappingURL=index.d.ts.map
@@ -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"}
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"}
package/dist/index.js CHANGED
@@ -8,4 +8,6 @@
8
8
  export { timingSafeEqual } from "./crypto.js";
9
9
  export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, } from "./scope-filter.js";
10
10
  export { validateMasterBearer } from "./bearer-validation.js";
11
+ // 0.2.0 — Domain-tenancy layer (additive)
12
+ export { workspaceSchema, workspaceMemberSchema, workspaceRoleSchema, tenantContextSchema, ScopeViolationError, getEffectiveTenantId, decodeUnverifiedBearer, } from "./tenancy-domain.js";
11
13
  //# 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,GAEhB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
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,GAEhB,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"}
@@ -0,0 +1,131 @@
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 (planned: 0.3.0) before treating the
16
+ * payload as a trust boundary.
17
+ *
18
+ * Task: k177hejb4tc5em5p70m9hxwn3x88kyp4
19
+ * Mission: k57b4t2q VR Cloud MVP Day 100
20
+ * Orchestrator: Sigma — VantagePeers | 2026-06-13
21
+ */
22
+ import { z } from "zod";
23
+ export declare const workspaceRoleSchema: z.ZodEnum<{
24
+ Admin: "Admin";
25
+ Editor: "Editor";
26
+ Viewer: "Viewer";
27
+ }>;
28
+ export type WorkspaceRole = z.infer<typeof workspaceRoleSchema>;
29
+ export declare const workspaceSchema: z.ZodObject<{
30
+ id: z.ZodString;
31
+ name: z.ZodString;
32
+ createdAt: z.ZodNumber;
33
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
34
+ }, z.core.$strip>;
35
+ export type Workspace = z.infer<typeof workspaceSchema>;
36
+ export declare const workspaceMemberSchema: z.ZodObject<{
37
+ userId: z.ZodString;
38
+ workspaceId: z.ZodString;
39
+ role: z.ZodEnum<{
40
+ Admin: "Admin";
41
+ Editor: "Editor";
42
+ Viewer: "Viewer";
43
+ }>;
44
+ joinedAt: z.ZodNumber;
45
+ }, z.core.$strip>;
46
+ export type WorkspaceMember = z.infer<typeof workspaceMemberSchema>;
47
+ export declare const tenantContextSchema: z.ZodObject<{
48
+ workspaceId: z.ZodString;
49
+ userId: z.ZodString;
50
+ roles: z.ZodArray<z.ZodEnum<{
51
+ Admin: "Admin";
52
+ Editor: "Editor";
53
+ Viewer: "Viewer";
54
+ }>>;
55
+ }, z.core.$strip>;
56
+ export type TenantContext = z.infer<typeof tenantContextSchema>;
57
+ export interface ScopeViolationPayload {
58
+ requestedTenantId: string;
59
+ contextTenantId: string;
60
+ reason: string;
61
+ }
62
+ /**
63
+ * Thrown by `getEffectiveTenantId` when `args.workspaceId` does not match the
64
+ * workspaceId encoded in the bearer-resolved `TenantContext`. This error is the
65
+ * canonical cross-tenant isolation signal across the VantagePeers Cloud fleet.
66
+ *
67
+ * Callers MUST translate this to a 403 or MCP permission-denied — never 404.
68
+ */
69
+ export declare class ScopeViolationError extends Error {
70
+ readonly code: "SCOPE_VIOLATION";
71
+ readonly payload: ScopeViolationPayload;
72
+ constructor(payload: ScopeViolationPayload);
73
+ }
74
+ /**
75
+ * Resolve the effective tenant workspace ID from a `TenantContext` + request args.
76
+ *
77
+ * If `args.workspaceId` differs from the context's `workspaceId` (derived from
78
+ * the bearer token), a `ScopeViolationError` is thrown immediately.
79
+ *
80
+ * This is the primary multi-tenant isolation gate — call it at the top of every
81
+ * MCP tool handler that accepts a `workspaceId` argument.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const tenantId = getEffectiveTenantId(ctx, args);
86
+ * // safe to use tenantId for DB queries — caller's scope is verified
87
+ * ```
88
+ */
89
+ export declare function getEffectiveTenantId(ctx: TenantContext, args: {
90
+ workspaceId: string;
91
+ }): string;
92
+ /**
93
+ * @security ⚠️ DECODE-ONLY. NOT AUTHENTICATION. NOT A TRUST BOUNDARY.
94
+ *
95
+ * Decodes a `base64(JSON({ userId, workspaceId, roles[] }))` payload and
96
+ * validates its SHAPE with Zod. It performs NO signature verification, NO
97
+ * issuer check, NO expiry check, NO replay-attack protection. The decoded
98
+ * payload is attacker-controlled and MUST NOT be used as the source of
99
+ * truth for multi-tenant isolation.
100
+ *
101
+ * Forgery example: any attacker producing
102
+ * base64(JSON({ userId:"x", workspaceId:"victim-org", roles:["Admin"] }))
103
+ * passes this function. Treating the returned `workspaceId` as authoritative
104
+ * — for example feeding it into `getEffectiveTenantId` as the trusted
105
+ * `ctx.workspaceId` — silently grants cross-tenant access.
106
+ *
107
+ * Use only as:
108
+ * - test fixture / harness helper
109
+ * - decoder for a bearer that was ALREADY verified by an upstream signed-JWT
110
+ * middleware (the production trust boundary)
111
+ *
112
+ * For the production trust boundary, callers MUST substitute a signed JWT
113
+ * (planned export in 0.3.0) or an opaque-token lookup against a Convex
114
+ * tenancy table.
115
+ *
116
+ * Renamed from `resolveBearer` (0.2.0 pre-release) to make the lack of
117
+ * verification impossible to miss at the call-site.
118
+ */
119
+ declare const bearerPayloadSchema: z.ZodObject<{
120
+ userId: z.ZodString;
121
+ workspaceId: z.ZodString;
122
+ roles: z.ZodArray<z.ZodEnum<{
123
+ Admin: "Admin";
124
+ Editor: "Editor";
125
+ Viewer: "Viewer";
126
+ }>>;
127
+ }, z.core.$strip>;
128
+ export type BearerPayload = z.infer<typeof bearerPayloadSchema>;
129
+ export declare function decodeUnverifiedBearer(token: string): Promise<BearerPayload>;
130
+ export {};
131
+ //# 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;;;;;;;;;;;;;;;;;;;;GAoBG;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"}
@@ -0,0 +1,152 @@
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 (planned: 0.3.0) before treating the
16
+ * payload as a trust boundary.
17
+ *
18
+ * Task: k177hejb4tc5em5p70m9hxwn3x88kyp4
19
+ * Mission: k57b4t2q VR Cloud MVP Day 100
20
+ * Orchestrator: Sigma — VantagePeers | 2026-06-13
21
+ */
22
+ import { z } from "zod";
23
+ // ---------------------------------------------------------------------------
24
+ // WorkspaceRole
25
+ // ---------------------------------------------------------------------------
26
+ export const workspaceRoleSchema = z.enum(["Admin", "Editor", "Viewer"]);
27
+ // ---------------------------------------------------------------------------
28
+ // Workspace
29
+ // ---------------------------------------------------------------------------
30
+ export const workspaceSchema = z.object({
31
+ id: z.string().min(1),
32
+ name: z.string().min(1),
33
+ createdAt: z.number(),
34
+ metadata: z.record(z.string(), z.unknown()).optional(),
35
+ });
36
+ // ---------------------------------------------------------------------------
37
+ // WorkspaceMember
38
+ // ---------------------------------------------------------------------------
39
+ export const workspaceMemberSchema = z.object({
40
+ userId: z.string().min(1),
41
+ workspaceId: z.string().min(1),
42
+ role: workspaceRoleSchema,
43
+ joinedAt: z.number(),
44
+ });
45
+ // ---------------------------------------------------------------------------
46
+ // TenantContext
47
+ // ---------------------------------------------------------------------------
48
+ export const tenantContextSchema = z.object({
49
+ workspaceId: z.string().min(1),
50
+ userId: z.string().min(1),
51
+ roles: z.array(workspaceRoleSchema),
52
+ });
53
+ /**
54
+ * Thrown by `getEffectiveTenantId` when `args.workspaceId` does not match the
55
+ * workspaceId encoded in the bearer-resolved `TenantContext`. This error is the
56
+ * canonical cross-tenant isolation signal across the VantagePeers Cloud fleet.
57
+ *
58
+ * Callers MUST translate this to a 403 or MCP permission-denied — never 404.
59
+ */
60
+ export class ScopeViolationError extends Error {
61
+ code = "SCOPE_VIOLATION";
62
+ payload;
63
+ constructor(payload) {
64
+ super(`SCOPE_VIOLATION: requested tenant "${payload.requestedTenantId}" does not match context tenant "${payload.contextTenantId}" — ${payload.reason}`);
65
+ this.name = "ScopeViolationError";
66
+ this.payload = payload;
67
+ // Restore prototype chain (required for instanceof checks in transpiled targets).
68
+ Object.setPrototypeOf(this, new.target.prototype);
69
+ }
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // getEffectiveTenantId
73
+ // ---------------------------------------------------------------------------
74
+ /**
75
+ * Resolve the effective tenant workspace ID from a `TenantContext` + request args.
76
+ *
77
+ * If `args.workspaceId` differs from the context's `workspaceId` (derived from
78
+ * the bearer token), a `ScopeViolationError` is thrown immediately.
79
+ *
80
+ * This is the primary multi-tenant isolation gate — call it at the top of every
81
+ * MCP tool handler that accepts a `workspaceId` argument.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const tenantId = getEffectiveTenantId(ctx, args);
86
+ * // safe to use tenantId for DB queries — caller's scope is verified
87
+ * ```
88
+ */
89
+ export function getEffectiveTenantId(ctx, args) {
90
+ if (args.workspaceId !== ctx.workspaceId) {
91
+ throw new ScopeViolationError({
92
+ requestedTenantId: args.workspaceId,
93
+ contextTenantId: ctx.workspaceId,
94
+ reason: "args.workspaceId does not match bearer-resolved context workspaceId",
95
+ });
96
+ }
97
+ return ctx.workspaceId;
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // decodeUnverifiedBearer
101
+ // ---------------------------------------------------------------------------
102
+ /**
103
+ * @security ⚠️ DECODE-ONLY. NOT AUTHENTICATION. NOT A TRUST BOUNDARY.
104
+ *
105
+ * Decodes a `base64(JSON({ userId, workspaceId, roles[] }))` payload and
106
+ * validates its SHAPE with Zod. It performs NO signature verification, NO
107
+ * issuer check, NO expiry check, NO replay-attack protection. The decoded
108
+ * payload is attacker-controlled and MUST NOT be used as the source of
109
+ * truth for multi-tenant isolation.
110
+ *
111
+ * Forgery example: any attacker producing
112
+ * base64(JSON({ userId:"x", workspaceId:"victim-org", roles:["Admin"] }))
113
+ * passes this function. Treating the returned `workspaceId` as authoritative
114
+ * — for example feeding it into `getEffectiveTenantId` as the trusted
115
+ * `ctx.workspaceId` — silently grants cross-tenant access.
116
+ *
117
+ * Use only as:
118
+ * - test fixture / harness helper
119
+ * - decoder for a bearer that was ALREADY verified by an upstream signed-JWT
120
+ * middleware (the production trust boundary)
121
+ *
122
+ * For the production trust boundary, callers MUST substitute a signed JWT
123
+ * (planned export in 0.3.0) or an opaque-token lookup against a Convex
124
+ * tenancy table.
125
+ *
126
+ * Renamed from `resolveBearer` (0.2.0 pre-release) to make the lack of
127
+ * verification impossible to miss at the call-site.
128
+ */
129
+ const bearerPayloadSchema = z.object({
130
+ userId: z.string().min(1),
131
+ workspaceId: z.string().min(1),
132
+ roles: z.array(workspaceRoleSchema),
133
+ });
134
+ export async function decodeUnverifiedBearer(token) {
135
+ let raw;
136
+ try {
137
+ raw = Buffer.from(token, "base64").toString("utf-8");
138
+ }
139
+ catch {
140
+ throw new Error(`decodeUnverifiedBearer: failed to base64-decode token`);
141
+ }
142
+ let parsed;
143
+ try {
144
+ parsed = JSON.parse(raw);
145
+ }
146
+ catch {
147
+ throw new Error(`decodeUnverifiedBearer: token payload is not valid JSON`);
148
+ }
149
+ // Zod parse — throws ZodError on schema violations (missing fields, bad roles).
150
+ return bearerPayloadSchema.parse(parsed);
151
+ }
152
+ //# sourceMappingURL=tenancy-domain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenancy-domain.js","sourceRoot":"","sources":["../src/tenancy-domain.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.1.0",
3
+ "version": "0.2.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",
@@ -25,6 +25,10 @@
25
25
  "./types": {
26
26
  "types": "./dist/types.d.ts",
27
27
  "import": "./dist/types.js"
28
+ },
29
+ "./tenancy-domain": {
30
+ "types": "./dist/tenancy-domain.d.ts",
31
+ "import": "./dist/tenancy-domain.js"
28
32
  }
29
33
  },
30
34
  "files": [
@@ -68,5 +72,8 @@
68
72
  "@types/node": "^20.11.0",
69
73
  "typescript": "^5.4.0",
70
74
  "vitest": "^1.6.0"
75
+ },
76
+ "dependencies": {
77
+ "zod": "^4.4.3"
71
78
  }
72
79
  }