@vantageos/cloud-identity 0.3.0 → 0.5.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,71 @@
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.5.0]
7
+
8
+ **Additive — grant-aware scope filter.** Closes the other pole of the
9
+ fail-closed defect class: 0.3.0/0.4.0 closed "a right granted by ABSENCE"
10
+ (missing `oauthCtx` silently passing everything); this closes "a right
11
+ REFUSED by absence" — a per-row grant (a named mission agent/pilot, a
12
+ mandate's `fulfilledBy`) that IS present on the row but that
13
+ `passesScopeFilter`/`scopeFilterList`/`scopeFilterGet` were structurally
14
+ blind to, because they only ever consulted `createdBy`/`namespace`.
15
+
16
+ ### Added
17
+
18
+ - `passesScopeFilter`, `scopeFilterList`, `scopeFilterGet` (`src/scope-filter.ts`)
19
+ now accept an optional `grantFields: readonly string[]` parameter, defaulting
20
+ to `[]`. Each entry names a property on the row that the CALLER declares as
21
+ a per-row grant — the field's runtime value is treated as a single identity
22
+ (`string`, equality match) or multiple identities (`string[]`, any-element
23
+ match) against `oauthCtx.fromAllowList`. The package never hardcodes which
24
+ fields are grants for which table; that declaration is caller-supplied data.
25
+ - **Fail-closed regression preserved byte-for-byte:** omitting `grantFields`
26
+ (or passing `[]`) reproduces 0.4.0 behaviour exactly — this is a pure
27
+ widening, no existing caller's behaviour changes without an explicit
28
+ opt-in declaration at its own call site.
29
+
30
+ ## [0.4.0]
31
+
32
+ **Additive.** Two new primitives so one identity layer covers both deployment
33
+ shapes and the human sign-in path — no separate self-host layer, no change to
34
+ the Cloud/machine path. Serves Tau's `vantage-starter` self-host install and
35
+ the Hephaistos human-path onboarding, both stalled on this surface.
36
+
37
+ ### Added
38
+
39
+ - **Named deployment mode** — `DeploymentMode` (`"cloud" | "self-host"`) and a
40
+ `{ kind: "self-host"; tenantId }` branch on `TenantSource` / `requireTenantId`
41
+ (`src/org-guard.ts`). `cloud` keeps the existing fail-closed behaviour
42
+ unchanged (org/workspace required on every request — invariant #1123 holds,
43
+ 0.4.0 reopens nothing). `self-host` is single-tenant: `requireTenantId`
44
+ returns the configured `tenantId` only when it is a non-empty string, and
45
+ **throws** otherwise — the tenant is presented explicitly by configuration,
46
+ never inferred from the absence of an organization.
47
+ - **Human-path resolver** — `normalizeVerifiedHumanSession()` (`src/human-path.ts`,
48
+ new `./human-path` subpath export) maps an already-*verified*, framework-agnostic
49
+ Clerk-shaped session (`{ orgId, userId, orgRole }`) to
50
+ `{ tenant, subject, role }`. `role` is a new union `HumanAccountRole`
51
+ (`owner | admin | member | client`), distinct from `workspaceRoleSchema`
52
+ (`Admin | Editor | Viewer`) and never conflated. No Clerk SDK is imported.
53
+ Refuses (throws) on no session, no org, no user id, or an unrecognized/absent
54
+ org role — presented, never inferred. **Naming fix, still pre-publish:** this
55
+ function was originally named `resolveHumanIdentity`, which invited passing
56
+ in an unverified, client-supplied object (e.g. `req.body`) and would make
57
+ that object the trusted source of tenant/subject/role — a
58
+ privilege-escalation risk. Renamed to `normalizeVerifiedHumanSession` before
59
+ 0.4.0 reaches npm (current published latest is 0.3.0) to make the
60
+ already-verified precondition explicit at the call-site, mirroring
61
+ `decodeUnverifiedBearer`'s rename from `resolveBearer` in 0.2.0. Not a
62
+ breaking change — 0.4.0 has no npm consumer yet.
63
+
64
+ ### Notes
65
+
66
+ - Purely additive: every 0.3.0 export (`passesScopeFilter`, `scopeFilterList`,
67
+ `scopeFilterGet`, `validateMasterBearer`, `getEffectiveTenantId`,
68
+ `decodeUnverifiedBearer`, `requireTenantId`'s `session`/`bearer` branches, …)
69
+ is behaviorally unchanged. Full suite 87/87, `tsc --noEmit` 0.
70
+
6
71
  ## [Unreleased] - 0.3.0
7
72
 
8
73
  **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"}
@@ -5,6 +5,31 @@
5
5
  * implementation; the only change was replacing a framework-specific context
6
6
  * type with the framework-agnostic `OAuthCtx` from `./types`.
7
7
  *
8
+ * Contract (0.5.0 — additive, widening only, vs. 0.4.0):
9
+ * - Every exported predicate/filter now accepts an OPTIONAL `grantFields`
10
+ * parameter: `readonly string[]`, defaulting to `[]`. Each entry names a
11
+ * property on the row that the CALLER declares as a per-row grant (e.g.
12
+ * "agents", "pilot", "fulfilledBy"). The package never hardcodes which
13
+ * fields are grants for which table — that declaration is data supplied
14
+ * by the caller at the call site, not business knowledge baked into this
15
+ * shared module (no-hardcoded-business-knowledge).
16
+ * - A row passes for a non-master caller if createdBy/namespace already
17
+ * passed (0.4.0 behaviour, unchanged) OR if ANY declared grant field's
18
+ * value names an identity in `oauthCtx.fromAllowList` — the field may be
19
+ * a single string (e.g. "pilot") or a string[] (e.g. "agents"): a string
20
+ * value matches by equality, an array value matches if any element is in
21
+ * `fromAllowList`.
22
+ * - `grantFields` defaulting to `[]` means a caller that declares NO grants
23
+ * gets EXACTLY the 0.4.0 behaviour, byte-for-byte — this is a pure
24
+ * widening, no existing caller's behaviour changes without an explicit
25
+ * opt-in declaration at its own call site.
26
+ * - This closes the OTHER pole of the fail-closed defect: 0.3.0/0.4.0
27
+ * closed "a right granted by ABSENCE" (missing oauthCtx silently passing
28
+ * everything); this closes "a right REFUSED by absence" — a grant that
29
+ * IS present on the row (a named agent, pilot, or fulfiller) but that the
30
+ * filter was structurally blind to because it only ever consulted
31
+ * createdBy/namespace.
32
+ *
8
33
  * Contract (0.3.0 — BREAKING vs. 0.2.0):
9
34
  * - `oauthCtx` is now MANDATORY. A right is never granted by absence: the
10
35
  * 0.2.0 behaviour of `oauthCtx === undefined` silently passing everything
@@ -50,6 +75,18 @@ export type ScopeFilterable = {
50
75
  * `fromAllowList` contains the `"*"` sentinel.
51
76
  */
52
77
  export declare function isMasterScope(ctx: OAuthCtx): boolean;
78
+ /**
79
+ * Names of row properties the CALLER declares as per-row grants. Each named
80
+ * property's runtime value must be a `string` (single identity) or
81
+ * `string[]` (multiple identities) to be consulted — any other runtime shape
82
+ * is silently skipped (not a match), never thrown, since these are duck-typed
83
+ * against arbitrary Convex documents.
84
+ *
85
+ * This is DATA supplied by the call site, never a list hardcoded inside this
86
+ * package: the package does not and must not know that "agents" means
87
+ * missions or "fulfilledBy" means mandates.
88
+ */
89
+ export type GrantFieldDeclaration = readonly string[];
53
90
  /**
54
91
  * Core predicate. Returns true when the row is visible to the caller.
55
92
  *
@@ -57,6 +94,9 @@ export declare function isMasterScope(ctx: OAuthCtx): boolean;
57
94
  * - row.createdBy ∈ oauthCtx.fromAllowList → true
58
95
  * - row.namespace === prefix OR startsWith prefix+'/'
59
96
  * for any prefix ∈ oauthCtx.namespaceReadPrefixes → true
97
+ * - row[grantField] (for any declared grantField) names an identity in
98
+ * oauthCtx.fromAllowList, where the field is a string (equality) or a
99
+ * string[] (any-element membership) → true
60
100
  * - otherwise → false
61
101
  *
62
102
  * `oauthCtx` is mandatory (see module-level contract note above): an omitted
@@ -64,21 +104,32 @@ export declare function isMasterScope(ctx: OAuthCtx): boolean;
64
104
  * need the old legacy-bearer wildcard behaviour must pass `LEGACY_WILDCARD_CTX`
65
105
  * explicitly.
66
106
  *
107
+ * `grantFields` (0.5.0, optional, defaults to `[]`) lets a caller widen the
108
+ * predicate for its own table without this package hardcoding which fields
109
+ * are grants — see the module-level 0.5.0 contract note above. Omitting it
110
+ * (or passing `[]`) reproduces 0.4.0 behaviour exactly.
111
+ *
67
112
  * Substring matches that don't fall on a '/' boundary are explicitly rejected
68
113
  * (e.g. namespace="orchestrator/alphabet" does NOT match prefix
69
114
  * "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
70
115
  */
71
- export declare function passesScopeFilter(oauthCtx: OAuthCtx, row: ScopeFilterable): boolean;
116
+ export declare function passesScopeFilter<T extends ScopeFilterable>(oauthCtx: OAuthCtx, row: T, grantFields?: GrantFieldDeclaration): boolean;
72
117
  /**
73
118
  * Filter a list of rows (post-query). Used by list_* tools.
119
+ *
120
+ * `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
121
+ * behaviour).
74
122
  */
75
- export declare function scopeFilterList<T extends ScopeFilterable>(oauthCtx: OAuthCtx, rows: T[]): T[];
123
+ export declare function scopeFilterList<T extends ScopeFilterable>(oauthCtx: OAuthCtx, rows: T[], grantFields?: GrantFieldDeclaration): T[];
76
124
  /**
77
125
  * Assert a single row passes (get_* tools). Returns the row when allowed, null
78
126
  * otherwise — callers translate null to a 404-equivalent "not found" MCP error
79
127
  * to avoid leaking the difference between "absent" and "filtered out".
128
+ *
129
+ * `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
130
+ * behaviour).
80
131
  */
81
- export declare function scopeFilterGet<T extends ScopeFilterable>(oauthCtx: OAuthCtx, row: T | null | undefined): T | null;
132
+ export declare function scopeFilterGet<T extends ScopeFilterable>(oauthCtx: OAuthCtx, row: T | null | undefined, grantFields?: GrantFieldDeclaration): T | null;
82
133
  /**
83
134
  * Helper for callers that want a single "is master / legacy" predicate.
84
135
  */
@@ -1 +1 @@
1
- {"version":3,"file":"scope-filter.d.ts","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAKjC,CAAC;AAWF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAIpD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,eAAe,GACnB,OAAO,CAYT;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,CAAC,EAAE,GACR,CAAC,EAAE,CAGL;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACxB,CAAC,GAAG,IAAI,CAIV;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAGtD"}
1
+ {"version":3,"file":"scope-filter.d.ts","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAKjC,CAAC;AAWF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAIpD;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAAG,SAAS,MAAM,EAAE,CAAC;AActD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,eAAe,EACzD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,EACN,WAAW,GAAE,qBAAuC,GACnD,OAAO,CAgBT;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,CAAC,EAAE,EACT,WAAW,GAAE,qBAAuC,GACnD,CAAC,EAAE,CAGL;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EACzB,WAAW,GAAE,qBAAuC,GACnD,CAAC,GAAG,IAAI,CAIV;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAGtD"}
@@ -5,6 +5,31 @@
5
5
  * implementation; the only change was replacing a framework-specific context
6
6
  * type with the framework-agnostic `OAuthCtx` from `./types`.
7
7
  *
8
+ * Contract (0.5.0 — additive, widening only, vs. 0.4.0):
9
+ * - Every exported predicate/filter now accepts an OPTIONAL `grantFields`
10
+ * parameter: `readonly string[]`, defaulting to `[]`. Each entry names a
11
+ * property on the row that the CALLER declares as a per-row grant (e.g.
12
+ * "agents", "pilot", "fulfilledBy"). The package never hardcodes which
13
+ * fields are grants for which table — that declaration is data supplied
14
+ * by the caller at the call site, not business knowledge baked into this
15
+ * shared module (no-hardcoded-business-knowledge).
16
+ * - A row passes for a non-master caller if createdBy/namespace already
17
+ * passed (0.4.0 behaviour, unchanged) OR if ANY declared grant field's
18
+ * value names an identity in `oauthCtx.fromAllowList` — the field may be
19
+ * a single string (e.g. "pilot") or a string[] (e.g. "agents"): a string
20
+ * value matches by equality, an array value matches if any element is in
21
+ * `fromAllowList`.
22
+ * - `grantFields` defaulting to `[]` means a caller that declares NO grants
23
+ * gets EXACTLY the 0.4.0 behaviour, byte-for-byte — this is a pure
24
+ * widening, no existing caller's behaviour changes without an explicit
25
+ * opt-in declaration at its own call site.
26
+ * - This closes the OTHER pole of the fail-closed defect: 0.3.0/0.4.0
27
+ * closed "a right granted by ABSENCE" (missing oauthCtx silently passing
28
+ * everything); this closes "a right REFUSED by absence" — a grant that
29
+ * IS present on the row (a named agent, pilot, or fulfiller) but that the
30
+ * filter was structurally blind to because it only ever consulted
31
+ * createdBy/namespace.
32
+ *
8
33
  * Contract (0.3.0 — BREAKING vs. 0.2.0):
9
34
  * - `oauthCtx` is now MANDATORY. A right is never granted by absence: the
10
35
  * 0.2.0 behaviour of `oauthCtx === undefined` silently passing everything
@@ -57,6 +82,15 @@ export function isMasterScope(ctx) {
57
82
  return true;
58
83
  return ctx.fromAllowList.includes("*");
59
84
  }
85
+ const NO_GRANT_FIELDS = [];
86
+ function grantFieldMatches(value, fromAllowList) {
87
+ if (typeof value === "string")
88
+ return fromAllowList.includes(value);
89
+ if (Array.isArray(value)) {
90
+ return value.some((entry) => typeof entry === "string" && fromAllowList.includes(entry));
91
+ }
92
+ return false;
93
+ }
60
94
  /**
61
95
  * Core predicate. Returns true when the row is visible to the caller.
62
96
  *
@@ -64,6 +98,9 @@ export function isMasterScope(ctx) {
64
98
  * - row.createdBy ∈ oauthCtx.fromAllowList → true
65
99
  * - row.namespace === prefix OR startsWith prefix+'/'
66
100
  * for any prefix ∈ oauthCtx.namespaceReadPrefixes → true
101
+ * - row[grantField] (for any declared grantField) names an identity in
102
+ * oauthCtx.fromAllowList, where the field is a string (equality) or a
103
+ * string[] (any-element membership) → true
67
104
  * - otherwise → false
68
105
  *
69
106
  * `oauthCtx` is mandatory (see module-level contract note above): an omitted
@@ -71,11 +108,16 @@ export function isMasterScope(ctx) {
71
108
  * need the old legacy-bearer wildcard behaviour must pass `LEGACY_WILDCARD_CTX`
72
109
  * explicitly.
73
110
  *
111
+ * `grantFields` (0.5.0, optional, defaults to `[]`) lets a caller widen the
112
+ * predicate for its own table without this package hardcoding which fields
113
+ * are grants — see the module-level 0.5.0 contract note above. Omitting it
114
+ * (or passing `[]`) reproduces 0.4.0 behaviour exactly.
115
+ *
74
116
  * Substring matches that don't fall on a '/' boundary are explicitly rejected
75
117
  * (e.g. namespace="orchestrator/alphabet" does NOT match prefix
76
118
  * "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
77
119
  */
78
- export function passesScopeFilter(oauthCtx, row) {
120
+ export function passesScopeFilter(oauthCtx, row, grantFields = NO_GRANT_FIELDS) {
79
121
  assertOauthCtx(oauthCtx);
80
122
  if (isMasterScope(oauthCtx))
81
123
  return true;
@@ -90,25 +132,36 @@ export function passesScopeFilter(oauthCtx, row) {
90
132
  return true;
91
133
  }
92
134
  }
135
+ for (const field of grantFields) {
136
+ const value = row[field];
137
+ if (grantFieldMatches(value, oauthCtx.fromAllowList))
138
+ return true;
139
+ }
93
140
  return false;
94
141
  }
95
142
  /**
96
143
  * Filter a list of rows (post-query). Used by list_* tools.
144
+ *
145
+ * `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
146
+ * behaviour).
97
147
  */
98
- export function scopeFilterList(oauthCtx, rows) {
148
+ export function scopeFilterList(oauthCtx, rows, grantFields = NO_GRANT_FIELDS) {
99
149
  assertOauthCtx(oauthCtx);
100
- return rows.filter((r) => passesScopeFilter(oauthCtx, r));
150
+ return rows.filter((r) => passesScopeFilter(oauthCtx, r, grantFields));
101
151
  }
102
152
  /**
103
153
  * Assert a single row passes (get_* tools). Returns the row when allowed, null
104
154
  * otherwise — callers translate null to a 404-equivalent "not found" MCP error
105
155
  * to avoid leaking the difference between "absent" and "filtered out".
156
+ *
157
+ * `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
158
+ * behaviour).
106
159
  */
107
- export function scopeFilterGet(oauthCtx, row) {
160
+ export function scopeFilterGet(oauthCtx, row, grantFields = NO_GRANT_FIELDS) {
108
161
  assertOauthCtx(oauthCtx);
109
162
  if (row == null)
110
163
  return null;
111
- return passesScopeFilter(oauthCtx, row) ? row : null;
164
+ return passesScopeFilter(oauthCtx, row, grantFields) ? row : null;
112
165
  }
113
166
  /**
114
167
  * Helper for callers that want a single "is master / legacy" predicate.
@@ -1 +1 @@
1
- {"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAa;IAC3C,KAAK,EAAE,QAAQ;IACf,aAAa,EAAE,CAAC,GAAG,CAAC;IACpB,qBAAqB,EAAE,EAAE;IACzB,sBAAsB,EAAE,EAAE;CAC3B,CAAC;AAEF,SAAS,cAAc,CAAC,QAAkB;IACxC,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,IAAI,SAAS,CACjB,2EAA2E;YACzE,0EAA0E,CAC7E,CAAC;IACJ,CAAC;AACH,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,GAAa;IACzC,cAAc,CAAC,GAAG,CAAC,CAAC;IACpB,IAAI,GAAG,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,GAAG,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAkB,EAClB,GAAoB;IAEpB,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzB,IAAI,aAAa,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC;IACrC,IAAI,SAAS,IAAI,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IACzE,IAAI,SAAS,EAAE,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,qBAAqB,EAAE,CAAC;YAC/C,IAAI,SAAS,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACjC,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC;QACjD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAkB,EAClB,IAAS;IAET,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzB,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAkB,EAClB,GAAyB;IAEzB,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzB,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7B,OAAO,iBAAiB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,GAAa;IAC3C,cAAc,CAAC,GAAG,CAAC,CAAC;IACpB,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC"}
1
+ {"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.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.0",
3
+ "version": "0.5.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": [