@vantageos/cloud-identity 0.3.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 CHANGED
@@ -3,6 +3,47 @@
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.4.0]
7
+
8
+ **Additive.** Two new primitives so one identity layer covers both deployment
9
+ shapes and the human sign-in path — no separate self-host layer, no change to
10
+ the Cloud/machine path. Serves Tau's `vantage-starter` self-host install and
11
+ the Hephaistos human-path onboarding, both stalled on this surface.
12
+
13
+ ### Added
14
+
15
+ - **Named deployment mode** — `DeploymentMode` (`"cloud" | "self-host"`) and a
16
+ `{ kind: "self-host"; tenantId }` branch on `TenantSource` / `requireTenantId`
17
+ (`src/org-guard.ts`). `cloud` keeps the existing fail-closed behaviour
18
+ unchanged (org/workspace required on every request — invariant #1123 holds,
19
+ 0.4.0 reopens nothing). `self-host` is single-tenant: `requireTenantId`
20
+ returns the configured `tenantId` only when it is a non-empty string, and
21
+ **throws** otherwise — the tenant is presented explicitly by configuration,
22
+ never inferred from the absence of an organization.
23
+ - **Human-path resolver** — `normalizeVerifiedHumanSession()` (`src/human-path.ts`,
24
+ new `./human-path` subpath export) maps an already-*verified*, framework-agnostic
25
+ Clerk-shaped session (`{ orgId, userId, orgRole }`) to
26
+ `{ tenant, subject, role }`. `role` is a new union `HumanAccountRole`
27
+ (`owner | admin | member | client`), distinct from `workspaceRoleSchema`
28
+ (`Admin | Editor | Viewer`) and never conflated. No Clerk SDK is imported.
29
+ Refuses (throws) on no session, no org, no user id, or an unrecognized/absent
30
+ org role — presented, never inferred. **Naming fix, still pre-publish:** this
31
+ function was originally named `resolveHumanIdentity`, which invited passing
32
+ in an unverified, client-supplied object (e.g. `req.body`) and would make
33
+ that object the trusted source of tenant/subject/role — a
34
+ privilege-escalation risk. Renamed to `normalizeVerifiedHumanSession` before
35
+ 0.4.0 reaches npm (current published latest is 0.3.0) to make the
36
+ already-verified precondition explicit at the call-site, mirroring
37
+ `decodeUnverifiedBearer`'s rename from `resolveBearer` in 0.2.0. Not a
38
+ breaking change — 0.4.0 has no npm consumer yet.
39
+
40
+ ### Notes
41
+
42
+ - Purely additive: every 0.3.0 export (`passesScopeFilter`, `scopeFilterList`,
43
+ `scopeFilterGet`, `validateMasterBearer`, `getEffectiveTenantId`,
44
+ `decodeUnverifiedBearer`, `requireTenantId`'s `session`/`bearer` branches, …)
45
+ is behaviorally unchanged. Full suite 87/87, `tsc --noEmit` 0.
46
+
6
47
  ## [Unreleased] - 0.3.0
7
48
 
8
49
  **BREAKING.** Closes two measured 0.2.0 defects — a right must never be
package/README.md CHANGED
@@ -153,7 +153,64 @@ want to catch by type, and `BearerPayload`, the type returned by
153
153
  - `SessionIdentity` — the human-path shape `requireTenantId` accepts under
154
154
  `{ kind: "session", identity }` (see "Both caller paths, one contract").
155
155
  - `TenantSource` — the discriminated union `requireTenantId` accepts:
156
- `{ kind: "session", identity }` or `{ kind: "bearer", context }`.
156
+ `{ kind: "session", identity }`, `{ kind: "bearer", context }`, or
157
+ `{ kind: "self-host", tenantId }`.
158
+ - `DeploymentMode` — the named `"cloud" | "self-host"` literal union (0.4.0).
159
+
160
+ **Deployment mode and the human path (0.4.0, additive)**
161
+
162
+ - `DeploymentMode` (`"cloud" | "self-host"`) names the two ways this package
163
+ can be run. `cloud` keeps `requireTenantId`'s existing session/bearer
164
+ behaviour exactly as-is — organization/workspace REQUIRED, fail-closed.
165
+ `self-host` is single-tenant: pass `{ kind: "self-host", tenantId }` to
166
+ `requireTenantId`, and it returns `tenantId` when it is a non-empty string.
167
+ The self-host tenant id must be DECLARED via configuration — if you select
168
+ self-host mode without configuring a tenant id, `requireTenantId` throws
169
+ rather than returning a default. A right is never granted by absence,
170
+ whichever mode you are in.
171
+
172
+ ```ts
173
+ import { requireTenantId } from "@vantageos/cloud-identity";
174
+
175
+ // self-host: the tenant id is configured explicitly, once, at startup.
176
+ requireTenantId({ kind: "self-host", tenantId: "the-one-tenant" });
177
+ // -> "the-one-tenant"
178
+
179
+ requireTenantId({ kind: "self-host", tenantId: undefined });
180
+ // -> throws: "No tenant id configured for self-host mode..."
181
+ ```
182
+
183
+ - `normalizeVerifiedHumanSession(session)` — **normalizes, does not
184
+ authenticate.** Maps an already-*verified* (not merely well-shaped),
185
+ framework-agnostic Clerk session object (`{ orgId, userId, orgRole }`) to
186
+ `{ tenant, subject, role }`. This package never imports a Clerk SDK; the
187
+ caller MUST verify the session upstream (e.g. Clerk's server-side `auth()`)
188
+ and pass in the already-verified object — never an unverified,
189
+ client-supplied object such as `req.body`, which would make the caller's
190
+ input the trusted source of tenant/subject/role. Refuses (throws) when the
191
+ session has no organization, no user id, or an org role this package does
192
+ not recognize — never defaults silently; these are shape checks, not a
193
+ verification step.
194
+ - `humanAccountRoleSchema` (type `HumanAccountRole`) — the new
195
+ `"owner" | "admin" | "member" | "client"` role union returned by
196
+ `normalizeVerifiedHumanSession`. Distinct from `workspaceRoleSchema`
197
+ (`Admin | Editor | Viewer`) — one is an organization-account role, the
198
+ other is workspace membership; they are never conflated.
199
+ - `ClerkSessionLike` — the minimal input shape `normalizeVerifiedHumanSession` accepts:
200
+ `{ orgId?, userId?, orgRole? }`.
201
+ - `ResolvedHumanIdentity` — the `{ tenant, subject, role }` return type of
202
+ `normalizeVerifiedHumanSession`.
203
+
204
+ ```ts
205
+ import { normalizeVerifiedHumanSession } from "@vantageos/cloud-identity";
206
+
207
+ normalizeVerifiedHumanSession({
208
+ orgId: "org_abc",
209
+ userId: "user_123",
210
+ orgRole: "org:admin",
211
+ });
212
+ // -> { tenant: "org_abc", subject: "user_123", role: "admin" }
213
+ ```
157
214
 
158
215
  ## What this package does not do
159
216
 
@@ -172,6 +229,49 @@ Worth reading before you rely on it.
172
229
  have already fetched. If fetching them was itself expensive or unsafe, filter
173
230
  earlier, in your query.
174
231
 
232
+ ## Upgrading to 0.4.0
233
+
234
+ **This release is purely additive — nothing you already call changes.** Every
235
+ 0.3.0 export keeps its exact behaviour; 0.4.0 only adds two primitives, so an
236
+ upgrade from 0.3.0 compiles and runs unchanged until you choose to adopt them.
237
+
238
+ What is new:
239
+
240
+ - **A named deployment mode.** `requireTenantId` gains a `self-host` source
241
+ alongside `session` and `bearer`. `cloud` is unchanged — an org-less
242
+ identity is still refused. `self-host` is single-tenant and returns the
243
+ tenant you **declare**; it throws when none is configured, never inferring
244
+ one from absence.
245
+
246
+ ```js
247
+ import { requireTenantId } from "@vantageos/cloud-identity";
248
+
249
+ // cloud (unchanged): refuses when the session has no org
250
+ requireTenantId({ kind: "session", identity });
251
+ // self-host: returns the DECLARED tenant, throws if it is empty
252
+ requireTenantId({ kind: "self-host", tenantId: process.env.TENANT_ID });
253
+ ```
254
+
255
+ - **A human-path normalizer.** `normalizeVerifiedHumanSession` maps an
256
+ **already-verified** Clerk-shaped session into `{ tenant, subject, role }`
257
+ (`role` ∈ `owner | admin | member | client`).
258
+
259
+ ```js
260
+ import { normalizeVerifiedHumanSession } from "@vantageos/cloud-identity";
261
+
262
+ // session MUST already be verified upstream, e.g. Clerk auth() server-side:
263
+ const { tenant, subject, role } = normalizeVerifiedHumanSession(session);
264
+ ```
265
+
266
+ ⚠️ It **normalizes, it does not authenticate.** Passing an unverified,
267
+ client-controlled object (a `req.body`, a query payload) makes that object
268
+ the trusted source of tenant, subject and role — a privilege escalation.
269
+ Verify the session upstream first; only pass the object your provider
270
+ already verified. (Same discipline as `decodeUnverifiedBearer`.)
271
+
272
+ Nothing to change on upgrade, nothing removed. Adopt the new primitives when
273
+ you need them.
274
+
175
275
  ## Upgrading to 0.3.0
176
276
 
177
277
  **This release changes a default, and it breaks compilation on purpose.**
@@ -0,0 +1,95 @@
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
+ export declare const humanAccountRoleSchema: z.ZodEnum<{
23
+ owner: "owner";
24
+ admin: "admin";
25
+ member: "member";
26
+ client: "client";
27
+ }>;
28
+ export type HumanAccountRole = z.infer<typeof humanAccountRoleSchema>;
29
+ /**
30
+ * The minimal shape this package needs out of an already-resolved Clerk
31
+ * session (e.g. the object returned by Clerk's own `auth()` in a Next.js
32
+ * server context). No Clerk SDK type is imported — this is a structural
33
+ * shape the caller supplies directly.
34
+ */
35
+ export type ClerkSessionLike = {
36
+ orgId?: string | null;
37
+ userId?: string | null;
38
+ orgRole?: string | null;
39
+ };
40
+ export type ResolvedHumanIdentity = {
41
+ tenant: string;
42
+ subject: string;
43
+ role: HumanAccountRole;
44
+ };
45
+ /**
46
+ * @security ⚠️ DECODE/NORMALIZE-ONLY. NOT AUTHENTICATION. NOT VERIFICATION.
47
+ *
48
+ * This function does NOT authenticate a request and does NOT verify
49
+ * anything. It NORMALIZES a session that the caller has ALREADY verified
50
+ * upstream — e.g. via Clerk's own server-side `auth()` in a Next.js/Convex
51
+ * server context — into this package's `{ tenant, subject, role }` contract.
52
+ *
53
+ * Passing an unverified, client-supplied object here (for example
54
+ * `req.body`, a query-string payload, or anything else an attacker
55
+ * controls) makes THAT object the trusted source of `tenant`, `subject`,
56
+ * AND `role` — a privilege-escalation bug. The shape checks below (missing
57
+ * orgId/userId/orgRole) only guard against malformed input; they perform
58
+ * NO signature check, NO issuer check, NO session-validity check.
59
+ *
60
+ * Callers MUST verify the session upstream (Clerk `auth()` server-side, or
61
+ * an equivalent signed-session verifier) BEFORE calling this function. Only
62
+ * pass in the object Clerk itself already verified — never pass through an
63
+ * unverified request body.
64
+ *
65
+ * Resolves an already-verified, framework-agnostic Clerk session object into
66
+ * the package's `{ tenant, subject, role }` contract.
67
+ *
68
+ * - throws when `session` is null/undefined ("Unauthenticated: no
69
+ * session.") — same refusal message class as `requireTenantId`'s
70
+ * session branch, for consistent error handling across both resolvers.
71
+ * - throws when `session.orgId` is missing or empty ("No active
72
+ * organization on this session...") — human path is cloud fail-closed,
73
+ * same invariant as `requireTenantId`.
74
+ * - throws when `session.userId` is missing or empty.
75
+ * - throws when `session.orgRole` is missing, empty, or not a recognized
76
+ * Clerk org role (unknown roles are refused, never defaulted).
77
+ *
78
+ * Named to make the already-verified precondition explicit at the
79
+ * call-site — see CHANGELOG.md 0.4.0 for the naming history — mirroring
80
+ * `decodeUnverifiedBearer`'s equivalent rename in `./tenancy-domain.ts`.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * // session MUST already be verified upstream, e.g.:
85
+ * // const { orgId, userId, orgRole } = await auth(); // Clerk server-side
86
+ * const { tenant, subject, role } = normalizeVerifiedHumanSession({
87
+ * orgId: "org_abc",
88
+ * userId: "user_123",
89
+ * orgRole: "org:admin",
90
+ * });
91
+ * // -> { tenant: "org_abc", subject: "user_123", role: "admin" }
92
+ * ```
93
+ */
94
+ export declare function normalizeVerifiedHumanSession(session: ClerkSessionLike | null | undefined): ResolvedHumanIdentity;
95
+ //# sourceMappingURL=human-path.d.ts.map
@@ -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
@@ -12,5 +12,7 @@ export type { OAuthCtx, ScopeProfile, NamespacePrefix, FromAllowListEntry, Valid
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
14
  export { requireTenantId } from "./org-guard.js";
15
- export type { SessionIdentity, TenantSource } 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";
16
18
  //# 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,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,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,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
@@ -10,6 +10,8 @@ export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWi
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 contract)
13
+ // Organization-membership guard (unified session + bearer + self-host contract)
14
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";
15
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,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,oEAAoE;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,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,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"}
@@ -44,6 +44,15 @@ import type { TenantContext } from "./tenancy-domain.js";
44
44
  export type SessionIdentity = {
45
45
  orgId?: string | null;
46
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";
47
56
  /**
48
57
  * Discriminated union covering both VantagePeers Cloud entry paths behind
49
58
  * one contract:
@@ -51,6 +60,10 @@ export type SessionIdentity = {
51
60
  * - `bearer`: machine path, via a resolved, ALREADY-VERIFIED token
52
61
  * (`TenantContext` — see `decodeUnverifiedBearer` / the planned signed-JWT
53
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.
54
67
  */
55
68
  export type TenantSource = {
56
69
  kind: "session";
@@ -58,6 +71,9 @@ export type TenantSource = {
58
71
  } | {
59
72
  kind: "bearer";
60
73
  context: TenantContext;
74
+ } | {
75
+ kind: "self-host";
76
+ tenantId: string | null | undefined;
61
77
  };
62
78
  /**
63
79
  * Resolve the caller's tenant id, refusing when no organization/workspace is
@@ -1 +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"}
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"}
package/dist/org-guard.js CHANGED
@@ -61,6 +61,13 @@ export function requireTenantId(source) {
61
61
  }
62
62
  return orgId;
63
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
+ }
64
71
  const workspaceId = source.context?.workspaceId;
65
72
  if (typeof workspaceId !== "string" || workspaceId.length === 0) {
66
73
  throw new Error("No workspace on this bearer-resolved tenant context: the bearer must resolve to a non-empty workspaceId.");
@@ -1 +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
+ {"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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/cloud-identity",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",
@@ -29,6 +29,10 @@
29
29
  "./tenancy-domain": {
30
30
  "types": "./dist/tenancy-domain.d.ts",
31
31
  "import": "./dist/tenancy-domain.js"
32
+ },
33
+ "./human-path": {
34
+ "types": "./dist/human-path.d.ts",
35
+ "import": "./dist/human-path.js"
32
36
  }
33
37
  },
34
38
  "files": [