@vantageos/cloud-identity 0.1.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 ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@vantageos/cloud-identity` are documented here.
4
+ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## [0.1.0] - 2026-06-04
7
+
8
+ Initial release.
9
+
10
+ ### Added
11
+ - `crypto.timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean` — constant-time byte comparison with branch-timing-leak mitigation on length mismatch.
12
+ - `scope-filter.passesScopeFilter / scopeFilterList / scopeFilterGet` — row-level visibility filter honouring `fromAllowList` + `namespaceReadPrefixes` with master pass-through and legacy-bearer back-compat.
13
+ - `bearer-validation.validateMasterBearer` — `Authorization: Bearer <token>` parsing + sha256 constant-time match against a master secret.
14
+ - Public type surface: `OAuthCtx`, `ScopeProfile`, `NamespacePrefix`, `FromAllowListEntry`, `ValidateMasterBearerResult`.
15
+
16
+ ### Notes
17
+ - `getEffectiveWorkspaceId` deferred to 0.2.0 (depends on `vantageos-crm` workspace helper, owned by Theta).
18
+ - No `provenance: true` in `publishConfig` — provenance only from CI per fleet friction lesson `npm-publish-provenance-only-from-ci-not-local-host`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VantagePeers Cloud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @vantageos/cloud-identity
2
+
3
+ Framework-agnostic identity + scope-filter primitives for **VantagePeers Cloud** (multi-tenant MCP).
4
+
5
+ Consumed by `vantage-peers-mcp` and `@vantageos/vantage-crm-mcp` — provides the small, testable building blocks that gate cross-tenant data access in the MCP transport layer.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @vantageos/cloud-identity
11
+ ```
12
+
13
+ ## Modules
14
+
15
+ | Module | Symbols | Purpose |
16
+ | --- | --- | --- |
17
+ | `@vantageos/cloud-identity/crypto` | `timingSafeEqual` | Constant-time byte comparison (no branch-timing leak). |
18
+ | `@vantageos/cloud-identity/scope-filter` | `passesScopeFilter`, `scopeFilterList`, `scopeFilterGet` | Row-level visibility filter on `(createdBy, namespace)` using the caller's OAuth scope. |
19
+ | `@vantageos/cloud-identity/bearer-validation` | `validateMasterBearer` | Bearer-token parsing + sha256 constant-time match against a configured master secret. |
20
+ | `@vantageos/cloud-identity/types` | `OAuthCtx`, `ScopeProfile`, `NamespacePrefix`, `FromAllowListEntry`, `ValidateMasterBearerResult` | Public type surface. |
21
+
22
+ ## 0.1.0 scope
23
+
24
+ This release ships the **transport-agnostic** core:
25
+
26
+ - `crypto.timingSafeEqual`
27
+ - `scope-filter.{passesScopeFilter, scopeFilterList, scopeFilterGet}`
28
+ - `bearer-validation.validateMasterBearer`
29
+ - types
30
+
31
+ ## 0.2.0 roadmap
32
+
33
+ - `getEffectiveWorkspaceId` (workspace resolution helper, sourced from `vantageos-crm`).
34
+ - D1 ghost Clerk identity injection.
35
+ - Co-designed in a Theta cycle.
36
+
37
+ ## Usage examples
38
+
39
+ ### Constant-time byte comparison
40
+
41
+ ```ts
42
+ import { timingSafeEqual } from "@vantageos/cloud-identity/crypto";
43
+
44
+ const a = new TextEncoder().encode(presentedHash);
45
+ const b = new TextEncoder().encode(expectedHash);
46
+ if (!(await timingSafeEqual(a, b))) {
47
+ return new Response("forbidden", { status: 403 });
48
+ }
49
+ ```
50
+
51
+ ### Scope-aware row filtering
52
+
53
+ ```ts
54
+ import { scopeFilterList } from "@vantageos/cloud-identity/scope-filter";
55
+ import type { OAuthCtx } from "@vantageos/cloud-identity/types";
56
+
57
+ const ctx: OAuthCtx = {
58
+ fromAllowList: ["alice"],
59
+ namespaceReadPrefixes: ["orchestrator/alpha"],
60
+ namespaceWritePrefixes: [],
61
+ };
62
+ const visible = scopeFilterList(ctx, rowsFromConvex);
63
+ ```
64
+
65
+ ### Master Bearer validation
66
+
67
+ ```ts
68
+ import { validateMasterBearer } from "@vantageos/cloud-identity/bearer-validation";
69
+
70
+ const res = await validateMasterBearer(
71
+ request.headers.get("authorization") ?? undefined,
72
+ process.env.BEARER_SECRET_MASTER ?? "",
73
+ );
74
+ if (!res.ok) {
75
+ return new Response(`unauthorized: ${res.error}`, { status: 401 });
76
+ }
77
+ ```
78
+
79
+ ## Security doctrine
80
+
81
+ Canonical reference: `docs/cloud/security-multi-tenant.md` in the `vantage-peers` repo.
82
+
83
+ ## License
84
+
85
+ MIT — VantagePeers Cloud, 2026.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Master-token Bearer validation.
3
+ *
4
+ * Extracted from the master-gate logic in
5
+ * `vantage-memory/mcp-server/server-http.ts` (`masterOnlyMiddleware`) and
6
+ * HARDENED per @vantageos/cloud-identity 0.1.0 contract:
7
+ * - SHA-256 hash both the presented token AND the configured master secret,
8
+ * then compare the two digests with `timingSafeEqual` (constant time).
9
+ * - The original middleware did a direct `token !== masterToken` string
10
+ * compare which is non-constant-time. This brick fixes that leak so any
11
+ * caller using `validateMasterBearer` inherits the safer behaviour.
12
+ *
13
+ * Surface:
14
+ * `validateMasterBearer(authHeader, masterSecret) → ValidateMasterBearerResult`
15
+ *
16
+ * Errors are coarse-grained on purpose — callers should not surface the exact
17
+ * reason to remote clients beyond a generic 401/403.
18
+ */
19
+ import type { ValidateMasterBearerResult } from "./types.js";
20
+ /**
21
+ * Validate an `Authorization: Bearer <token>` header against a configured
22
+ * master secret.
23
+ *
24
+ * - missing header → `{ok: false, error: "missing"}`
25
+ * - bad format / empty token / empty secret → `{ok: false, error: "malformed"}`
26
+ * - token sha256 != master sha256 (constant-time) → `{ok: false, error: "mismatch"}`
27
+ * - match → `{ok: true}`
28
+ *
29
+ * The scheme prefix match is case-insensitive (RFC 7235 §2.1) but the token
30
+ * itself is treated as opaque bytes.
31
+ */
32
+ export declare function validateMasterBearer(authHeader: string | undefined, masterSecret: string): Promise<ValidateMasterBearerResult>;
33
+ //# sourceMappingURL=bearer-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer-validation.d.ts","sourceRoot":"","sources":["../src/bearer-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAU7D;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,0BAA0B,CAAC,CAsBrC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Master-token Bearer validation.
3
+ *
4
+ * Extracted from the master-gate logic in
5
+ * `vantage-memory/mcp-server/server-http.ts` (`masterOnlyMiddleware`) and
6
+ * HARDENED per @vantageos/cloud-identity 0.1.0 contract:
7
+ * - SHA-256 hash both the presented token AND the configured master secret,
8
+ * then compare the two digests with `timingSafeEqual` (constant time).
9
+ * - The original middleware did a direct `token !== masterToken` string
10
+ * compare which is non-constant-time. This brick fixes that leak so any
11
+ * caller using `validateMasterBearer` inherits the safer behaviour.
12
+ *
13
+ * Surface:
14
+ * `validateMasterBearer(authHeader, masterSecret) → ValidateMasterBearerResult`
15
+ *
16
+ * Errors are coarse-grained on purpose — callers should not surface the exact
17
+ * reason to remote clients beyond a generic 401/403.
18
+ */
19
+ import { timingSafeEqual } from "./crypto.js";
20
+ const BEARER_PREFIX_LOWER = "bearer ";
21
+ async function sha256Bytes(input) {
22
+ const data = new TextEncoder().encode(input);
23
+ const digest = await crypto.subtle.digest("SHA-256", data);
24
+ return new Uint8Array(digest);
25
+ }
26
+ /**
27
+ * Validate an `Authorization: Bearer <token>` header against a configured
28
+ * master secret.
29
+ *
30
+ * - missing header → `{ok: false, error: "missing"}`
31
+ * - bad format / empty token / empty secret → `{ok: false, error: "malformed"}`
32
+ * - token sha256 != master sha256 (constant-time) → `{ok: false, error: "mismatch"}`
33
+ * - match → `{ok: true}`
34
+ *
35
+ * The scheme prefix match is case-insensitive (RFC 7235 §2.1) but the token
36
+ * itself is treated as opaque bytes.
37
+ */
38
+ export async function validateMasterBearer(authHeader, masterSecret) {
39
+ if (authHeader === undefined || authHeader === null || authHeader === "") {
40
+ return { ok: false, error: "missing" };
41
+ }
42
+ if (!masterSecret || masterSecret.length === 0) {
43
+ return { ok: false, error: "malformed" };
44
+ }
45
+ // Case-insensitive scheme match without lower-casing the token bytes.
46
+ const lower = authHeader.toLowerCase();
47
+ if (!lower.startsWith(BEARER_PREFIX_LOWER)) {
48
+ return { ok: false, error: "malformed" };
49
+ }
50
+ const token = authHeader.slice(BEARER_PREFIX_LOWER.length).trim();
51
+ if (token.length === 0) {
52
+ return { ok: false, error: "malformed" };
53
+ }
54
+ const [tokenDigest, secretDigest] = await Promise.all([
55
+ sha256Bytes(token),
56
+ sha256Bytes(masterSecret),
57
+ ]);
58
+ const ok = await timingSafeEqual(tokenDigest, secretDigest);
59
+ return ok ? { ok: true } : { ok: false, error: "mismatch" };
60
+ }
61
+ //# sourceMappingURL=bearer-validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer-validation.js","sourceRoot":"","sources":["../src/bearer-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,MAAM,mBAAmB,GAAG,SAAS,CAAC;AAEtC,KAAK,UAAU,WAAW,CAAC,KAAa;IACtC,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3D,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,UAA8B,EAC9B,YAAoB;IAEpB,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,KAAK,EAAE,EAAE,CAAC;QACzE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IACzC,CAAC;IACD,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAC3C,CAAC;IACD,sEAAsE;IACtE,MAAM,KAAK,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;IACvC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAC3C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAC3C,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAClE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAC3C,CAAC;IACD,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACpD,WAAW,CAAC,KAAK,CAAC;QAClB,WAAW,CAAC,YAAY,CAAC;KAC1B,CAAC,CAAC;IACH,MAAM,EAAE,GAAG,MAAM,eAAe,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IAC5D,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Constant-time byte-array comparison.
3
+ *
4
+ * Ported from `vantage-memory/mcp-server/src/crypto.ts` (Day 47 master-token
5
+ * gate, Eta F1 MAJOR PR #621). Surface adapted from `(string, string)` to
6
+ * `(Uint8Array, Uint8Array)` per @vantageos/cloud-identity 0.1.0 contract so
7
+ * the helper is framework-agnostic and reusable by any caller (hash bytes,
8
+ * raw token bytes, HMAC tags, etc.).
9
+ *
10
+ * Algorithm (identical to source):
11
+ * 1. Length mismatch → still run a dummy Web Crypto HMAC over equal-length
12
+ * input to avoid a branch-timing leak, then return false.
13
+ * 2. Equal length → XOR-accumulate diff across all bytes, return diff === 0.
14
+ *
15
+ * Web Crypto (`crypto.subtle`) is used for the dummy work (not Node's
16
+ * `crypto.timingSafeEqual`) for portability across runtimes (Node 20+, Bun,
17
+ * Deno, edge runtimes) and parity with the original Convex implementation.
18
+ */
19
+ export declare function timingSafeEqual(a: Uint8Array, b: Uint8Array): Promise<boolean>;
20
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,eAAe,CACnC,CAAC,EAAE,UAAU,EACb,CAAC,EAAE,UAAU,GACZ,OAAO,CAAC,OAAO,CAAC,CAoBlB"}
package/dist/crypto.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Constant-time byte-array comparison.
3
+ *
4
+ * Ported from `vantage-memory/mcp-server/src/crypto.ts` (Day 47 master-token
5
+ * gate, Eta F1 MAJOR PR #621). Surface adapted from `(string, string)` to
6
+ * `(Uint8Array, Uint8Array)` per @vantageos/cloud-identity 0.1.0 contract so
7
+ * the helper is framework-agnostic and reusable by any caller (hash bytes,
8
+ * raw token bytes, HMAC tags, etc.).
9
+ *
10
+ * Algorithm (identical to source):
11
+ * 1. Length mismatch → still run a dummy Web Crypto HMAC over equal-length
12
+ * input to avoid a branch-timing leak, then return false.
13
+ * 2. Equal length → XOR-accumulate diff across all bytes, return diff === 0.
14
+ *
15
+ * Web Crypto (`crypto.subtle`) is used for the dummy work (not Node's
16
+ * `crypto.timingSafeEqual`) for portability across runtimes (Node 20+, Bun,
17
+ * Deno, edge runtimes) and parity with the original Convex implementation.
18
+ */
19
+ export async function timingSafeEqual(a, b) {
20
+ if (a.length !== b.length) {
21
+ // Still do a comparison on equal-length buffers to avoid branch-timing leak.
22
+ const dummy = new Uint8Array(a.length);
23
+ const keyMaterial = a.length > 0 ? a : new Uint8Array([0]);
24
+ const aKey = await crypto.subtle.importKey("raw", keyMaterial, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
25
+ await crypto.subtle.sign("HMAC", aKey, dummy);
26
+ return false;
27
+ }
28
+ let diff = 0;
29
+ for (let i = 0; i < a.length; i++) {
30
+ diff |= a[i] ^ b[i];
31
+ }
32
+ return diff === 0;
33
+ }
34
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,CAAa,EACb,CAAa;IAEb,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1B,6EAA6E;QAC7E,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACxC,KAAK,EACL,WAAsC,EACtC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAC;QACF,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAgC,CAAC,CAAC;QACzE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;IACxB,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @vantageos/cloud-identity — public surface.
3
+ *
4
+ * Framework-agnostic identity + scope-filter primitives for VantagePeers Cloud
5
+ * (multi-tenant MCP). Consumed by `vantage-peers-mcp` and
6
+ * `@vantageos/vantage-crm-mcp`.
7
+ */
8
+ export { timingSafeEqual } from "./crypto.js";
9
+ export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, type ScopeFilterable, } from "./scope-filter.js";
10
+ export { validateMasterBearer } from "./bearer-validation.js";
11
+ export type { OAuthCtx, ScopeProfile, NamespacePrefix, FromAllowListEntry, ValidateMasterBearerResult, } from "./types.js";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,KAAK,eAAe,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @vantageos/cloud-identity — public surface.
3
+ *
4
+ * Framework-agnostic identity + scope-filter primitives for VantagePeers Cloud
5
+ * (multi-tenant MCP). Consumed by `vantage-peers-mcp` and
6
+ * `@vantageos/vantage-crm-mcp`.
7
+ */
8
+ export { timingSafeEqual } from "./crypto.js";
9
+ export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, } from "./scope-filter.js";
10
+ export { validateMasterBearer } from "./bearer-validation.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,GAEhB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Sprint S3.1 B2 — scope-aware filter helpers for VP MCP tools.
3
+ *
4
+ * Ported from `vantage-memory/mcp-server/src/scope-filter.ts`. Behaviour is
5
+ * preserved verbatim; only the type dependency on the in-tree `OAuthContext`
6
+ * is replaced by the framework-agnostic `OAuthCtx` from `./types`.
7
+ *
8
+ * Doctrine references:
9
+ * - decisions/doctrine-scope-aware-filter-2026-05-26.md (D3 base)
10
+ * - memory j579y6f31g7xzgtgdnpgetdmjx87ztyj (D9-D14 extension)
11
+ *
12
+ * Contract:
13
+ * - Master scope (scope === "master" OR fromAllowList includes "*") = wildcard pass.
14
+ * - Legacy bearer (oauthCtx === undefined) = wildcard pass (treated as master-
15
+ * equivalent for backward-compatibility with mcpTenants Pi/Tau/Phi paths).
16
+ * - Non-master scope = row passes iff:
17
+ * row.createdBy ∈ oauthCtx.fromAllowList
18
+ * OR
19
+ * row.namespace startsWith one of oauthCtx.namespaceReadPrefixes
20
+ * (prefix matched as exact-equal OR followed by '/' boundary)
21
+ * - Row missing BOTH `createdBy` and `namespace` = denied for non-master.
22
+ *
23
+ * NOTE: this module is intentionally framework-agnostic — no McpServer / Hono
24
+ * imports — to keep the helpers trivially unit-testable.
25
+ */
26
+ import type { OAuthCtx } from "./types.js";
27
+ /**
28
+ * Row shape accepted by the scope filter. All fields optional because real
29
+ * Convex documents from list_peers / list_messages / etc. don't all carry both.
30
+ */
31
+ export type ScopeFilterable = {
32
+ createdBy?: string;
33
+ namespace?: string;
34
+ };
35
+ /**
36
+ * Returns true when the scope profile grants full, wildcard access.
37
+ * Mirrors `isMasterScope` from the source module: `scope === "master"` OR
38
+ * `fromAllowList` contains the `"*"` sentinel.
39
+ */
40
+ export declare function isMasterScope(ctx: OAuthCtx | undefined): boolean;
41
+ /**
42
+ * Core predicate. Returns true when the row is visible to the caller.
43
+ *
44
+ * - Master scope (isMasterScope === true) → true (wildcard)
45
+ * - Legacy bearer (oauthCtx === undefined) → true (back-compat)
46
+ * - row.createdBy ∈ oauthCtx.fromAllowList → true
47
+ * - row.namespace === prefix OR startsWith prefix+'/'
48
+ * for any prefix ∈ oauthCtx.namespaceReadPrefixes → true
49
+ * - otherwise → false
50
+ *
51
+ * Substring matches that don't fall on a '/' boundary are explicitly rejected
52
+ * (e.g. namespace="orchestrator/alphabet" does NOT match prefix
53
+ * "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
54
+ */
55
+ export declare function passesScopeFilter(oauthCtx: OAuthCtx | undefined, row: ScopeFilterable): boolean;
56
+ /**
57
+ * Filter a list of rows (post-query). Used by list_* tools.
58
+ */
59
+ export declare function scopeFilterList<T extends ScopeFilterable>(oauthCtx: OAuthCtx | undefined, rows: T[]): T[];
60
+ /**
61
+ * Assert a single row passes (get_* tools). Returns the row when allowed, null
62
+ * otherwise — callers translate null to a 404-equivalent "not found" MCP error
63
+ * to avoid leaking the difference between "absent" and "filtered out".
64
+ */
65
+ export declare function scopeFilterGet<T extends ScopeFilterable>(oauthCtx: OAuthCtx | undefined, row: T | null | undefined): T | null;
66
+ /**
67
+ * Helper for callers that want a single "is master / legacy" predicate.
68
+ * Mirrors the legacy-bearer-passes-through convention.
69
+ */
70
+ export declare function isWildcardScope(ctx: OAuthCtx | undefined): boolean;
71
+ //# sourceMappingURL=scope-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-filter.d.ts","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;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,SAAS,GAAG,OAAO,CAIhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,GAAG,EAAE,eAAe,GACnB,OAAO,CAYT;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,IAAI,EAAE,CAAC,EAAE,GACR,CAAC,EAAE,CAEL;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,GAAG,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACxB,CAAC,GAAG,IAAI,CAGV;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAGlE"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Sprint S3.1 B2 — scope-aware filter helpers for VP MCP tools.
3
+ *
4
+ * Ported from `vantage-memory/mcp-server/src/scope-filter.ts`. Behaviour is
5
+ * preserved verbatim; only the type dependency on the in-tree `OAuthContext`
6
+ * is replaced by the framework-agnostic `OAuthCtx` from `./types`.
7
+ *
8
+ * Doctrine references:
9
+ * - decisions/doctrine-scope-aware-filter-2026-05-26.md (D3 base)
10
+ * - memory j579y6f31g7xzgtgdnpgetdmjx87ztyj (D9-D14 extension)
11
+ *
12
+ * Contract:
13
+ * - Master scope (scope === "master" OR fromAllowList includes "*") = wildcard pass.
14
+ * - Legacy bearer (oauthCtx === undefined) = wildcard pass (treated as master-
15
+ * equivalent for backward-compatibility with mcpTenants Pi/Tau/Phi paths).
16
+ * - Non-master scope = row passes iff:
17
+ * row.createdBy ∈ oauthCtx.fromAllowList
18
+ * OR
19
+ * row.namespace startsWith one of oauthCtx.namespaceReadPrefixes
20
+ * (prefix matched as exact-equal OR followed by '/' boundary)
21
+ * - Row missing BOTH `createdBy` and `namespace` = denied for non-master.
22
+ *
23
+ * NOTE: this module is intentionally framework-agnostic — no McpServer / Hono
24
+ * imports — to keep the helpers trivially unit-testable.
25
+ */
26
+ /**
27
+ * Returns true when the scope profile grants full, wildcard access.
28
+ * Mirrors `isMasterScope` from the source module: `scope === "master"` OR
29
+ * `fromAllowList` contains the `"*"` sentinel.
30
+ */
31
+ export function isMasterScope(ctx) {
32
+ if (!ctx)
33
+ return false;
34
+ if (ctx.scope === "master")
35
+ return true;
36
+ return ctx.fromAllowList.includes("*");
37
+ }
38
+ /**
39
+ * Core predicate. Returns true when the row is visible to the caller.
40
+ *
41
+ * - Master scope (isMasterScope === true) → true (wildcard)
42
+ * - Legacy bearer (oauthCtx === undefined) → true (back-compat)
43
+ * - row.createdBy ∈ oauthCtx.fromAllowList → true
44
+ * - row.namespace === prefix OR startsWith prefix+'/'
45
+ * for any prefix ∈ oauthCtx.namespaceReadPrefixes → true
46
+ * - otherwise → false
47
+ *
48
+ * Substring matches that don't fall on a '/' boundary are explicitly rejected
49
+ * (e.g. namespace="orchestrator/alphabet" does NOT match prefix
50
+ * "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
51
+ */
52
+ export function passesScopeFilter(oauthCtx, row) {
53
+ if (!oauthCtx)
54
+ return true;
55
+ if (isMasterScope(oauthCtx))
56
+ return true;
57
+ const { createdBy, namespace } = row;
58
+ if (createdBy && oauthCtx.fromAllowList.includes(createdBy))
59
+ return true;
60
+ if (namespace) {
61
+ for (const p of oauthCtx.namespaceReadPrefixes) {
62
+ if (namespace === p)
63
+ return true;
64
+ if (namespace.startsWith(`${p}/`))
65
+ return true;
66
+ }
67
+ }
68
+ return false;
69
+ }
70
+ /**
71
+ * Filter a list of rows (post-query). Used by list_* tools.
72
+ */
73
+ export function scopeFilterList(oauthCtx, rows) {
74
+ return rows.filter((r) => passesScopeFilter(oauthCtx, r));
75
+ }
76
+ /**
77
+ * Assert a single row passes (get_* tools). Returns the row when allowed, null
78
+ * otherwise — callers translate null to a 404-equivalent "not found" MCP error
79
+ * to avoid leaking the difference between "absent" and "filtered out".
80
+ */
81
+ export function scopeFilterGet(oauthCtx, row) {
82
+ if (row == null)
83
+ return null;
84
+ return passesScopeFilter(oauthCtx, row) ? row : null;
85
+ }
86
+ /**
87
+ * Helper for callers that want a single "is master / legacy" predicate.
88
+ * Mirrors the legacy-bearer-passes-through convention.
89
+ */
90
+ export function isWildcardScope(ctx) {
91
+ if (!ctx)
92
+ return true;
93
+ return isMasterScope(ctx);
94
+ }
95
+ //# sourceMappingURL=scope-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAaH;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,GAAyB;IACrD,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,IAAI,GAAG,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,GAAG,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAA8B,EAC9B,GAAoB;IAEpB,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,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,QAA8B,EAC9B,IAAS;IAET,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,QAA8B,EAC9B,GAAyB;IAEzB,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;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAAyB;IACvD,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Public type surface for @vantageos/cloud-identity.
3
+ *
4
+ * Framework-agnostic — no Hono, no Convex, no MCP SDK imports.
5
+ */
6
+ /**
7
+ * Scope profile name. Master = wildcard. Tenant = scoped via fromAllowList
8
+ * and namespace prefixes. Extensible string for future profiles.
9
+ */
10
+ export type ScopeProfile = "master" | "tenant" | (string & {});
11
+ /**
12
+ * Namespace prefix (matched as exact-equal OR followed by '/' boundary).
13
+ */
14
+ export type NamespacePrefix = string;
15
+ /**
16
+ * Identifier allowed on a row's `createdBy` field for non-master scopes.
17
+ */
18
+ export type FromAllowListEntry = string;
19
+ /**
20
+ * Minimal OAuth context attached to an inbound MCP request.
21
+ *
22
+ * - `fromAllowList`: row.createdBy must be in this list for non-master callers
23
+ * to see a row. `["*"]` is the master wildcard convention.
24
+ * - `namespaceReadPrefixes`: row.namespace must equal one of these OR start
25
+ * with prefix + '/' for the row to be readable by non-master callers.
26
+ * - `namespaceWritePrefixes`: same semantics for write paths (enforced by the
27
+ * caller, this brick only exposes the field on the type).
28
+ * - `scope`: optional scope profile name. `"master"` triggers wildcard pass.
29
+ */
30
+ export type OAuthCtx = {
31
+ fromAllowList: FromAllowListEntry[];
32
+ namespaceReadPrefixes: NamespacePrefix[];
33
+ namespaceWritePrefixes: NamespacePrefix[];
34
+ scope?: ScopeProfile;
35
+ };
36
+ /**
37
+ * Result of `validateMasterBearer`.
38
+ *
39
+ * - `ok: true` → token matched master secret in constant time.
40
+ * - `ok: false` with `error: "missing"` → no Authorization header.
41
+ * - `ok: false` with `error: "malformed"` → header present but not parseable
42
+ * as `Bearer <non-empty-token>`, OR master secret is empty.
43
+ * - `ok: false` with `error: "mismatch"` → token parsed but did not match.
44
+ */
45
+ export type ValidateMasterBearerResult = {
46
+ ok: true;
47
+ error?: undefined;
48
+ } | {
49
+ ok: false;
50
+ error: "missing" | "malformed" | "mismatch";
51
+ };
52
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAE/D;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAExC;;;;;;;;;;GAUG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,qBAAqB,EAAE,eAAe,EAAE,CAAC;IACzC,sBAAsB,EAAE,eAAe,EAAE,CAAC;IAC1C,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,0BAA0B,GAClC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,CAAA;CAAE,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Public type surface for @vantageos/cloud-identity.
3
+ *
4
+ * Framework-agnostic — no Hono, no Convex, no MCP SDK imports.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@vantageos/cloud-identity",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic identity + scope-filter primitives for VantagePeers Cloud (multi-tenant MCP).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./crypto": {
14
+ "types": "./dist/crypto.d.ts",
15
+ "import": "./dist/crypto.js"
16
+ },
17
+ "./scope-filter": {
18
+ "types": "./dist/scope-filter.d.ts",
19
+ "import": "./dist/scope-filter.js"
20
+ },
21
+ "./bearer-validation": {
22
+ "types": "./dist/bearer-validation.d.ts",
23
+ "import": "./dist/bearer-validation.js"
24
+ },
25
+ "./types": {
26
+ "types": "./dist/types.d.ts",
27
+ "import": "./dist/types.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "CHANGELOG.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "lint": "tsc --noEmit",
41
+ "prepublishOnly": "npm run build && npm test"
42
+ },
43
+ "keywords": [
44
+ "vantagepeers",
45
+ "mcp",
46
+ "oauth",
47
+ "scope",
48
+ "multi-tenant",
49
+ "identity"
50
+ ],
51
+ "author": "VantagePeers Cloud",
52
+ "license": "MIT",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/vantageos-agency/cloud-identity.git"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/vantageos-agency/cloud-identity/issues"
59
+ },
60
+ "homepage": "https://github.com/vantageos-agency/cloud-identity#readme",
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "engines": {
65
+ "node": ">=20"
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "^20.11.0",
69
+ "typescript": "^5.4.0",
70
+ "vitest": "^1.6.0"
71
+ }
72
+ }