@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 +41 -0
- package/README.md +101 -1
- package/dist/human-path.d.ts +95 -0
- package/dist/human-path.d.ts.map +1 -0
- package/dist/human-path.js +117 -0
- package/dist/human-path.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/org-guard.d.ts +16 -0
- package/dist/org-guard.d.ts.map +1 -1
- package/dist/org-guard.js +7 -0
- package/dist/org-guard.js.map +1 -1
- package/package.json +5 -1
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 }
|
|
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
|
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,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,
|
|
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,
|
|
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"}
|
package/dist/org-guard.d.ts
CHANGED
|
@@ -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
|
package/dist/org-guard.d.ts.map
CHANGED
|
@@ -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/
|
|
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.");
|
package/dist/org-guard.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-guard.js","sourceRoot":"","sources":["../src/org-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;
|
|
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
|
+
"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": [
|