@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 +24 -0
- package/README.md +109 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/tenancy-domain.d.ts +131 -0
- package/dist/tenancy-domain.d.ts.map +1 -0
- package/dist/tenancy-domain.js +152 -0
- package/dist/tenancy-domain.js.map +1 -0
- package/package.json +8 -1
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
|
|
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
|
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"}
|
|
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.
|
|
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
|
}
|