@venturekit/mcp 0.0.0-dev.20260608102211

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.
@@ -0,0 +1,63 @@
1
+ -- @venturekit/mcp — initial schema: mcp_tokens.
2
+ --
3
+ -- Storage for the bearer tokens an MCP server issues to agents
4
+ -- (Cascade/Windsurf workflows, CI bots, scripts). Ships WITH the
5
+ -- package so any app that depends on `@venturekit/mcp` gets token
6
+ -- management out of the box — `vk migrate` auto-discovers this file via
7
+ -- the package's `vk.migrations` field (mirrors @venturekit-pro/audit's
8
+ -- `audit_events`).
9
+ --
10
+ -- # Generic by construction
11
+ --
12
+ -- The package owns the MCP protocol, not any host's tenancy model, so
13
+ -- the owner key is a free-form `scope` (text) — NOT a foreign key into
14
+ -- some `tenants` table that only the CMS has. A host maps `scope` to
15
+ -- whatever it owns: the VentureKit CMS uses the tenant slug (which is
16
+ -- also the token's `vk_<scope>_…` segment, letting the verifier resolve
17
+ -- the owner before hashing). `created_by` is likewise a bare text audit
18
+ -- field (a Cognito sub, a user id, a CI run id — the host decides), with
19
+ -- no FK.
20
+ --
21
+ -- # Security
22
+ --
23
+ -- Only a hash of the plaintext + a short lookup `prefix` are stored; the
24
+ -- plaintext is shown once at issuance and is unrecoverable. The hash is
25
+ -- scrypt (see `store.ts:hashTokenSecret`) — self-describing
26
+ -- (`scrypt$N$r$p$salt$dk`) so params can evolve without a column change.
27
+
28
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
29
+
30
+ CREATE TABLE IF NOT EXISTS mcp_tokens (
31
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
32
+ -- Owner key. Host-defined; the CMS uses the tenant slug. Also the
33
+ -- `vk_<scope>_…` segment of the plaintext, so the verifier can narrow
34
+ -- to one owner before hashing.
35
+ scope text NOT NULL,
36
+ -- Human label shown in the issuing UI ("Cascade workflow", "CI bot").
37
+ label text NOT NULL,
38
+ -- First chars of the plaintext (`vk_<scope>_…`) — the lookup key the
39
+ -- verifier narrows on before hash-comparing candidates.
40
+ prefix varchar(24) NOT NULL,
41
+ -- scrypt hash of the full plaintext (`scrypt$N$r$p$salt$dk`).
42
+ token_hash text NOT NULL,
43
+ -- Comma-separated MCP tool whitelist; NULL = every registered tool.
44
+ allowed_tools text,
45
+ last_used_at timestamptz,
46
+ revoked_at timestamptz,
47
+ created_at timestamptz NOT NULL DEFAULT now(),
48
+ -- Free-form audit field (no FK): a user id / Cognito sub / CI id.
49
+ created_by text
50
+ );
51
+
52
+ -- Label is unique per scope: stops accidental dupes in the UI and
53
+ -- bounds the verifier's prefix-collision hash loop.
54
+ CREATE UNIQUE INDEX IF NOT EXISTS mcp_tokens_scope_label_unique
55
+ ON mcp_tokens (scope, lower(label));
56
+
57
+ -- Verifier hot path: WHERE scope = $1 AND prefix = $2 AND revoked_at IS NULL.
58
+ CREATE INDEX IF NOT EXISTS mcp_tokens_lookup_idx
59
+ ON mcp_tokens (scope, prefix) WHERE revoked_at IS NULL;
60
+
61
+ -- Owner list: WHERE scope = $1 ORDER BY created_at DESC.
62
+ CREATE INDEX IF NOT EXISTS mcp_tokens_scope_created_idx
63
+ ON mcp_tokens (scope, created_at DESC);
package/dist/path.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Migrations-dir helper.
3
+ *
4
+ * `vk migrate` auto-discovers a package's migrations via its
5
+ * `vk.migrations` field, so most apps need nothing here. This export is
6
+ * the explicit escape hatch: a host that disables auto-discovery can
7
+ * wire it into `vk.config.ts:extraMigrationsDirs`:
8
+ *
9
+ * ```ts
10
+ * import { getMcpMigrationsDir } from '@venturekit/mcp';
11
+ * export default defineVenture({ extraMigrationsDirs: [getMcpMigrationsDir()] });
12
+ * ```
13
+ *
14
+ * The directory is bundled into the build (via `files: ["src/migrations/*.sql"]`
15
+ * in package.json, mirrored into `dist/migrations/` by the build script).
16
+ * Apps consuming the source via a workspace symlink read from
17
+ * `src/migrations/`. Both layouts resolve to a `migrations/` dir next to
18
+ * this module, so a single path works for both.
19
+ */
20
+ /** Absolute path to the `mcp_tokens` migration files (`vk_mcp_*.sql`). */
21
+ export declare function getMcpMigrationsDir(): string;
22
+ //# sourceMappingURL=path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,0EAA0E;AAC1E,wBAAgB,mBAAmB,IAAI,MAAM,CAG5C"}
package/dist/path.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Migrations-dir helper.
3
+ *
4
+ * `vk migrate` auto-discovers a package's migrations via its
5
+ * `vk.migrations` field, so most apps need nothing here. This export is
6
+ * the explicit escape hatch: a host that disables auto-discovery can
7
+ * wire it into `vk.config.ts:extraMigrationsDirs`:
8
+ *
9
+ * ```ts
10
+ * import { getMcpMigrationsDir } from '@venturekit/mcp';
11
+ * export default defineVenture({ extraMigrationsDirs: [getMcpMigrationsDir()] });
12
+ * ```
13
+ *
14
+ * The directory is bundled into the build (via `files: ["src/migrations/*.sql"]`
15
+ * in package.json, mirrored into `dist/migrations/` by the build script).
16
+ * Apps consuming the source via a workspace symlink read from
17
+ * `src/migrations/`. Both layouts resolve to a `migrations/` dir next to
18
+ * this module, so a single path works for both.
19
+ */
20
+ import * as path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ /** Absolute path to the `mcp_tokens` migration files (`vk_mcp_*.sql`). */
23
+ export function getMcpMigrationsDir() {
24
+ const here = path.dirname(fileURLToPath(import.meta.url));
25
+ return path.join(here, 'migrations');
26
+ }
27
+ //# sourceMappingURL=path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB;IACjC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;AACvC,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Transport-agnostic MCP JSON-RPC dispatcher.
3
+ *
4
+ * Implements the slice of the MCP wire protocol an HTTP/JSON-RPC client
5
+ * (e.g. Windsurf/Cascade's built-in HTTP MCP transport) needs:
6
+ * `initialize`, `notifications/initialized`, `ping`, `tools/list`,
7
+ * `tools/call`. It is deliberately stateless (one request -> one
8
+ * response), which suits a Lambda or any request/response transport.
9
+ *
10
+ * Everything app-specific — authentication, rate limiting, audit
11
+ * wrapping, per-call context assembly — is injected via
12
+ * {@link McpServerConfig} hooks, so this loop carries no host concern.
13
+ */
14
+ import type { McpHandlerInput, McpHandlerOutput, McpServerConfig } from './types.js';
15
+ /**
16
+ * Handle a single MCP JSON-RPC request against `config`. Never throws for
17
+ * an expected client/auth/rate-limit failure — those become a structured
18
+ * `{ statusCode, response }` pair. A tool that throws is reported per the
19
+ * MCP spec as a *successful* response with `isError: true`.
20
+ */
21
+ export declare function handleMcpRequest<TContext, TPrincipal>(config: McpServerConfig<TContext, TPrincipal>, input: McpHandlerInput): Promise<McpHandlerOutput>;
22
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAEV,eAAe,EACf,gBAAgB,EAChB,eAAe,EAEhB,MAAM,YAAY,CAAC;AA+DpB;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,UAAU,EACzD,MAAM,EAAE,eAAe,CAAC,QAAQ,EAAE,UAAU,CAAC,EAC7C,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,gBAAgB,CAAC,CAwI3B"}
package/dist/server.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Transport-agnostic MCP JSON-RPC dispatcher.
3
+ *
4
+ * Implements the slice of the MCP wire protocol an HTTP/JSON-RPC client
5
+ * (e.g. Windsurf/Cascade's built-in HTTP MCP transport) needs:
6
+ * `initialize`, `notifications/initialized`, `ping`, `tools/list`,
7
+ * `tools/call`. It is deliberately stateless (one request -> one
8
+ * response), which suits a Lambda or any request/response transport.
9
+ *
10
+ * Everything app-specific — authentication, rate limiting, audit
11
+ * wrapping, per-call context assembly — is injected via
12
+ * {@link McpServerConfig} hooks, so this loop carries no host concern.
13
+ */
14
+ import { zodToJsonSchema } from './json-schema.js';
15
+ const DEFAULT_PROTOCOL_VERSION = '2025-03-26';
16
+ function rpcError(id, code, message, data) {
17
+ return {
18
+ jsonrpc: '2.0',
19
+ id,
20
+ error: { code, message, ...(data === undefined ? {} : { data }) },
21
+ };
22
+ }
23
+ /**
24
+ * Translate an arbitrary thrown value into JSON-RPC error fields. Honors
25
+ * a numeric `statusCode` / `rpcCode` and a structured `data` when the
26
+ * error carries them (so a host's bespoke error classes need not extend
27
+ * {@link McpError}); also lifts a bare `retryAfterSec` into `data`.
28
+ */
29
+ function mapError(err, fallbackStatus, fallbackRpc) {
30
+ const e = (typeof err === 'object' && err !== null ? err : {});
31
+ const statusCode = typeof e.statusCode === 'number' ? e.statusCode : fallbackStatus;
32
+ const rpcCode = typeof e.rpcCode === 'number' ? e.rpcCode : fallbackRpc;
33
+ const message = err instanceof Error ? err.message : String(err);
34
+ let data = e.data;
35
+ if (data === undefined && typeof e.retryAfterSec === 'number') {
36
+ data = { retryAfterSec: e.retryAfterSec };
37
+ }
38
+ return { statusCode, rpcCode, message, data };
39
+ }
40
+ function toToolDescriptor(tool) {
41
+ return {
42
+ name: tool.name,
43
+ description: tool.description,
44
+ inputSchema: zodToJsonSchema(tool.inputSchema),
45
+ };
46
+ }
47
+ function normalizeId(value) {
48
+ return typeof value === 'number' || typeof value === 'string' ? value : null;
49
+ }
50
+ /**
51
+ * Handle a single MCP JSON-RPC request against `config`. Never throws for
52
+ * an expected client/auth/rate-limit failure — those become a structured
53
+ * `{ statusCode, response }` pair. A tool that throws is reported per the
54
+ * MCP spec as a *successful* response with `isError: true`.
55
+ */
56
+ export async function handleMcpRequest(config, input) {
57
+ const body = input.body;
58
+ if (!body || body.jsonrpc !== '2.0' || typeof body.method !== 'string') {
59
+ return {
60
+ statusCode: 400,
61
+ response: rpcError(normalizeId(body?.id), -32600, 'Invalid JSON-RPC request'),
62
+ };
63
+ }
64
+ const id = normalizeId(body.id);
65
+ const method = body.method;
66
+ const params = (body.params ?? {});
67
+ // Authenticate up-front for every method (incl. `initialize`) so the
68
+ // principal is validated before any work and a single auth decision
69
+ // governs the whole request.
70
+ let auth;
71
+ try {
72
+ auth = await config.authenticate(input.authHeader);
73
+ }
74
+ catch (err) {
75
+ const f = mapError(err, 401, -32001);
76
+ return { statusCode: f.statusCode, response: rpcError(id, f.rpcCode, f.message, f.data) };
77
+ }
78
+ const protocolVersion = config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION;
79
+ switch (method) {
80
+ case 'initialize':
81
+ return {
82
+ statusCode: 200,
83
+ response: {
84
+ jsonrpc: '2.0',
85
+ id,
86
+ result: {
87
+ protocolVersion,
88
+ capabilities: { tools: {} },
89
+ serverInfo: config.serverInfo,
90
+ },
91
+ },
92
+ };
93
+ case 'notifications/initialized':
94
+ return { statusCode: 204, response: { jsonrpc: '2.0', id, result: {} } };
95
+ case 'ping':
96
+ return { statusCode: 200, response: { jsonrpc: '2.0', id, result: {} } };
97
+ case 'tools/list': {
98
+ const tools = Object.values(config.tools)
99
+ .filter((t) => !auth.allowedTools || auth.allowedTools.has(t.name))
100
+ .map(toToolDescriptor);
101
+ return { statusCode: 200, response: { jsonrpc: '2.0', id, result: { tools } } };
102
+ }
103
+ case 'tools/call': {
104
+ const callParams = params;
105
+ const toolName = String(callParams.name ?? '');
106
+ const tool = config.tools[toolName];
107
+ if (!tool) {
108
+ return { statusCode: 404, response: rpcError(id, -32601, `Unknown tool '${toolName}'`) };
109
+ }
110
+ if (auth.allowedTools && !auth.allowedTools.has(toolName)) {
111
+ return {
112
+ statusCode: 403,
113
+ response: rpcError(id, -32603, `Token not allowed to call '${toolName}'`),
114
+ };
115
+ }
116
+ // Rate-limit BEFORE schema validation + dispatch so a flooded
117
+ // principal can't burn quota by sending malformed args.
118
+ if (config.rateLimit) {
119
+ try {
120
+ await config.rateLimit(auth.principal, toolName);
121
+ }
122
+ catch (err) {
123
+ const f = mapError(err, 429, -32029);
124
+ return { statusCode: f.statusCode, response: rpcError(id, f.rpcCode, f.message, f.data) };
125
+ }
126
+ }
127
+ const parsed = tool.inputSchema.safeParse(callParams.arguments ?? {});
128
+ if (!parsed.success) {
129
+ return {
130
+ statusCode: 422,
131
+ response: rpcError(id, -32602, 'Invalid tool arguments', {
132
+ issues: parsed.error.issues,
133
+ }),
134
+ };
135
+ }
136
+ try {
137
+ const ctx = await config.createContext(auth.principal, toolName);
138
+ const invoke = () => tool.run(parsed.data, ctx);
139
+ const result = config.wrapToolCall
140
+ ? await config.wrapToolCall({ toolName, input: parsed.data, principal: auth.principal }, invoke)
141
+ : await invoke();
142
+ return {
143
+ statusCode: 200,
144
+ response: {
145
+ jsonrpc: '2.0',
146
+ id,
147
+ result: {
148
+ content: [{ type: 'text', text: JSON.stringify(result) }],
149
+ structuredContent: result,
150
+ isError: false,
151
+ },
152
+ },
153
+ };
154
+ }
155
+ catch (err) {
156
+ // Per the MCP spec, tool execution failures are wrapped in a
157
+ // *successful* response with `isError: true` rather than a
158
+ // JSON-RPC error — the agent reads the message and recovers.
159
+ const message = err instanceof Error ? err.message : String(err);
160
+ return {
161
+ statusCode: 200,
162
+ response: {
163
+ jsonrpc: '2.0',
164
+ id,
165
+ result: { content: [{ type: 'text', text: message }], isError: true },
166
+ },
167
+ };
168
+ }
169
+ }
170
+ default:
171
+ return {
172
+ statusCode: 404,
173
+ response: rpcError(id, -32601, `Method '${method}' not implemented`),
174
+ };
175
+ }
176
+ }
177
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,wBAAwB,GAAG,YAAY,CAAC;AAE9C,SAAS,QAAQ,CACf,EAA0B,EAC1B,IAAY,EACZ,OAAe,EACf,IAAc;IAEd,OAAO;QACL,OAAO,EAAE,KAAK;QACd,EAAE;QACF,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;KAClE,CAAC;AACJ,CAAC;AASD;;;;;GAKG;AACH,SAAS,QAAQ,CACf,GAAY,EACZ,cAAsB,EACtB,WAAmB;IAEnB,MAAM,CAAC,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAmB,CAAC;IACjF,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC;IACpF,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC;IACxE,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,IAAI,IAAI,GAAY,CAAC,CAAC,IAAI,CAAC;IAC3B,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;QAC9D,IAAI,GAAG,EAAE,aAAa,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC;IAC5C,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAChD,CAAC;AAED,SAAS,gBAAgB,CAAC,IAA4B;IAKpD,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC;KAC/C,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/E,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAA6C,EAC7C,KAAsB;IAEtB,MAAM,IAAI,GAAG,KAAK,CAAC,IAGN,CAAC;IAEd,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACvE,OAAO;YACL,UAAU,EAAE,GAAG;YACf,QAAQ,EAAE,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,0BAA0B,CAAC;SAC9E,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,GAAG,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAA4B,CAAC;IAE9D,qEAAqE;IACrE,oEAAoE;IACpE,6BAA6B;IAC7B,IAAI,IAAI,CAAC;IACT,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;QACrC,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;IAC5F,CAAC;IAED,MAAM,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,wBAAwB,CAAC;IAE3E,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,YAAY;YACf,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,QAAQ,EAAE;oBACR,OAAO,EAAE,KAAK;oBACd,EAAE;oBACF,MAAM,EAAE;wBACN,eAAe;wBACf,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;wBAC3B,UAAU,EAAE,MAAM,CAAC,UAAU;qBAC9B;iBACF;aACF,CAAC;QAEJ,KAAK,2BAA2B;YAC9B,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;QAE3E,KAAK,MAAM;YACT,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;QAE3E,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;iBACtC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;iBAClE,GAAG,CAAC,gBAAgB,CAAC,CAAC;YACzB,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QAClF,CAAC;QAED,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,MAAM,UAAU,GAAG,MAAiD,CAAC;YACrE,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;YAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACpC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,iBAAiB,QAAQ,GAAG,CAAC,EAAE,CAAC;YAC3F,CAAC;YACD,IAAI,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC1D,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,8BAA8B,QAAQ,GAAG,CAAC;iBAC1E,CAAC;YACJ,CAAC;YAED,8DAA8D;YAC9D,wDAAwD;YACxD,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;gBACrB,IAAI,CAAC;oBACH,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;gBACnD,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;oBACrC,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC5F,CAAC;YACH,CAAC;YAED,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;YACtE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,wBAAwB,EAAE;wBACvD,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM;qBAC5B,CAAC;iBACH,CAAC;YACJ,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;gBACjE,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBAChD,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY;oBAChC,CAAC,CAAC,MAAM,MAAM,CAAC,YAAY,CACvB,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,EAC3D,MAAM,CACP;oBACH,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC;gBACnB,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,QAAQ,EAAE;wBACR,OAAO,EAAE,KAAK;wBACd,EAAE;wBACF,MAAM,EAAE;4BACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;4BACzD,iBAAiB,EAAE,MAAM;4BACzB,OAAO,EAAE,KAAK;yBACf;qBACF;iBACF,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,6DAA6D;gBAC7D,2DAA2D;gBAC3D,6DAA6D;gBAC7D,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjE,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,QAAQ,EAAE;wBACR,OAAO,EAAE,KAAK;wBACd,EAAE;wBACF,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE;qBACtE;iBACF,CAAC;YACJ,CAAC;QACH,CAAC;QAED;YACE,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,WAAW,MAAM,mBAAmB,CAAC;aACrE,CAAC;IACN,CAAC;AACH,CAAC"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Postgres-backed token store for MCP bearer credentials.
3
+ *
4
+ * Ships with the package so a host gets MCP token management out of the
5
+ * box: a single `mcp_tokens` table (migration
6
+ * `migrations/vk_mcp_0001_tokens.sql`, auto-discovered by `vk migrate`)
7
+ * plus the CRUD + verify functions here. The host supplies a
8
+ * {@link Querier} (e.g. `@venturekit/data`'s `query`) and maps `scope`
9
+ * to whatever it owns — a tenant slug, a user id, a project id.
10
+ *
11
+ * # Security
12
+ * - Only a scrypt hash + the lookup `prefix` are persisted. The
13
+ * plaintext is returned once from {@link createMcpToken} and never
14
+ * stored.
15
+ * - {@link verifyMcpToken} narrows by `(scope, prefix)` then runs a
16
+ * constant-time scrypt compare over the (normally single) candidate.
17
+ * - Hashing is `node:crypto` scrypt — no third-party dependency.
18
+ */
19
+ /**
20
+ * Minimal querier shape — a function that runs a parameterized SQL
21
+ * statement and returns rows. Matches `@venturekit/data`'s `Querier`
22
+ * so a consumer can pass `query` directly, while staying structural so
23
+ * any Postgres driver can be adapted.
24
+ */
25
+ export type Querier = <T = Record<string, unknown>[]>(sql: string, params?: unknown[]) => Promise<T>;
26
+ /**
27
+ * Persisted token row (snake_case columns mapped to camelCase, which is
28
+ * what `@venturekit/data` returns). The plaintext is NEVER a column.
29
+ */
30
+ export interface McpTokenRow {
31
+ id: string;
32
+ /** Host-defined owner key (the CMS uses the tenant slug). */
33
+ scope: string;
34
+ label: string;
35
+ /** Non-secret lookup prefix (first chars of the plaintext). */
36
+ prefix: string;
37
+ /** scrypt hash (`scrypt$N$r$p$salt$dk`). */
38
+ tokenHash: string;
39
+ /** Comma-separated tool whitelist; `null` = every tool. */
40
+ allowedTools: string | null;
41
+ lastUsedAt: Date | null;
42
+ revokedAt: Date | null;
43
+ createdAt: Date;
44
+ createdBy: string | null;
45
+ }
46
+ export interface CreateMcpTokenInput {
47
+ /** Owner key — becomes the token's `vk_<scope>_…` segment. */
48
+ scope: string;
49
+ label: string;
50
+ /** Tool whitelist; empty/omitted → token may call every tool. */
51
+ allowedTools?: string[] | null;
52
+ /** Free-form audit id (user id / Cognito sub / CI id). */
53
+ createdBy?: string | null;
54
+ }
55
+ export interface CreatedMcpToken {
56
+ row: McpTokenRow;
57
+ /** The full plaintext token. Surface it once; it is unrecoverable after. */
58
+ token: string;
59
+ }
60
+ /** scrypt-hash a token plaintext into a self-describing `scrypt$…` string. */
61
+ export declare function hashTokenSecret(plaintext: string): Promise<string>;
62
+ /** Constant-time verify a plaintext against a stored `scrypt$…` hash. */
63
+ export declare function verifyTokenSecret(plaintext: string, stored: string): Promise<boolean>;
64
+ /** List every token for an owner, newest-first (incl. revoked). */
65
+ export declare function listMcpTokens(q: Querier, scope: string): Promise<McpTokenRow[]>;
66
+ /** Mint a token for `scope`. Returns the persisted row + the plaintext (once). */
67
+ export declare function createMcpToken(q: Querier, input: CreateMcpTokenInput): Promise<CreatedMcpToken>;
68
+ /**
69
+ * Revoke a token (idempotent — re-revoking preserves the original
70
+ * `revoked_at`). Returns `null` when the id doesn't exist for `scope`.
71
+ */
72
+ export declare function revokeMcpToken(q: Querier, scope: string, id: string): Promise<McpTokenRow | null>;
73
+ /**
74
+ * Resolve a plaintext bearer token to its active row, or `null` if the
75
+ * token is malformed, unknown, or revoked. Parses the scope from the
76
+ * token, narrows by `(scope, prefix)`, scrypt-verifies each candidate,
77
+ * and stamps `last_used_at` on a match (best-effort; failures don't
78
+ * block auth). The host is responsible for any further checks (e.g.
79
+ * mapping `scope` to a tenant and rejecting suspended ones).
80
+ */
81
+ export declare function verifyMcpToken(q: Querier, plaintext: string): Promise<McpTokenRow | null>;
82
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAsBH;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAClD,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,OAAO,EAAE,KACf,OAAO,CAAC,CAAC,CAAC,CAAC;AAEhB;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC/B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,WAAW,CAAC;IACjB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;CACf;AAID,8EAA8E;AAC9E,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQxE;AAED,yEAAyE;AACzE,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAY3F;AAID,mEAAmE;AACnE,wBAAsB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAKrF;AAID,kFAAkF;AAClF,wBAAsB,cAAc,CAClC,CAAC,EAAE,OAAO,EACV,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,eAAe,CAAC,CAe1B;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,CAAC,EAAE,OAAO,EACV,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAS7B;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAoB/F"}
package/dist/store.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Postgres-backed token store for MCP bearer credentials.
3
+ *
4
+ * Ships with the package so a host gets MCP token management out of the
5
+ * box: a single `mcp_tokens` table (migration
6
+ * `migrations/vk_mcp_0001_tokens.sql`, auto-discovered by `vk migrate`)
7
+ * plus the CRUD + verify functions here. The host supplies a
8
+ * {@link Querier} (e.g. `@venturekit/data`'s `query`) and maps `scope`
9
+ * to whatever it owns — a tenant slug, a user id, a project id.
10
+ *
11
+ * # Security
12
+ * - Only a scrypt hash + the lookup `prefix` are persisted. The
13
+ * plaintext is returned once from {@link createMcpToken} and never
14
+ * stored.
15
+ * - {@link verifyMcpToken} narrows by `(scope, prefix)` then runs a
16
+ * constant-time scrypt compare over the (normally single) candidate.
17
+ * - Hashing is `node:crypto` scrypt — no third-party dependency.
18
+ */
19
+ import { randomBytes, scrypt as scryptCb, timingSafeEqual, } from 'node:crypto';
20
+ import { promisify } from 'node:util';
21
+ import { generateBearerToken, bearerTokenPrefix, parseScopedToken } from './tokens.js';
22
+ const scrypt = promisify(scryptCb);
23
+ // scrypt cost parameters. N=16384 keeps a single hash ~50-80ms (on par
24
+ // with bcrypt cost 12) while staying under Node's default 32MB maxmem
25
+ // (128 * N * r = 16MB).
26
+ const SCRYPT_N = 16384;
27
+ const SCRYPT_R = 8;
28
+ const SCRYPT_P = 1;
29
+ const SCRYPT_KEYLEN = 32;
30
+ // ─── Hashing ────────────────────────────────────────────────────────
31
+ /** scrypt-hash a token plaintext into a self-describing `scrypt$…` string. */
32
+ export async function hashTokenSecret(plaintext) {
33
+ const salt = randomBytes(16);
34
+ const dk = (await scrypt(plaintext, salt, SCRYPT_KEYLEN, {
35
+ N: SCRYPT_N,
36
+ r: SCRYPT_R,
37
+ p: SCRYPT_P,
38
+ }));
39
+ return `scrypt$${SCRYPT_N}$${SCRYPT_R}$${SCRYPT_P}$${salt.toString('base64')}$${dk.toString('base64')}`;
40
+ }
41
+ /** Constant-time verify a plaintext against a stored `scrypt$…` hash. */
42
+ export async function verifyTokenSecret(plaintext, stored) {
43
+ const parts = stored.split('$');
44
+ if (parts.length !== 6 || parts[0] !== 'scrypt')
45
+ return false;
46
+ const n = Number(parts[1]);
47
+ const r = Number(parts[2]);
48
+ const p = Number(parts[3]);
49
+ if (!Number.isInteger(n) || !Number.isInteger(r) || !Number.isInteger(p))
50
+ return false;
51
+ const salt = Buffer.from(parts[4], 'base64');
52
+ const expected = Buffer.from(parts[5], 'base64');
53
+ if (expected.length === 0)
54
+ return false;
55
+ const dk = (await scrypt(plaintext, salt, expected.length, { N: n, r, p }));
56
+ return dk.length === expected.length && timingSafeEqual(dk, expected);
57
+ }
58
+ // ─── Read ───────────────────────────────────────────────────────────
59
+ /** List every token for an owner, newest-first (incl. revoked). */
60
+ export async function listMcpTokens(q, scope) {
61
+ return q(`SELECT * FROM mcp_tokens WHERE scope = $1 ORDER BY created_at DESC`, [scope]);
62
+ }
63
+ // ─── Write ──────────────────────────────────────────────────────────
64
+ /** Mint a token for `scope`. Returns the persisted row + the plaintext (once). */
65
+ export async function createMcpToken(q, input) {
66
+ const { token, prefix } = generateBearerToken({ scope: input.scope });
67
+ const tokenHash = await hashTokenSecret(token);
68
+ const allowedTools = input.allowedTools && input.allowedTools.length > 0 ? input.allowedTools.join(',') : null;
69
+ const rows = await q(`INSERT INTO mcp_tokens (scope, label, prefix, token_hash, allowed_tools, created_by)
70
+ VALUES ($1, $2, $3, $4, $5, $6)
71
+ RETURNING *`, [input.scope, input.label, prefix, tokenHash, allowedTools, input.createdBy ?? null]);
72
+ const row = rows[0];
73
+ if (!row)
74
+ throw new Error('[mcp/createMcpToken] INSERT ... RETURNING returned no rows');
75
+ return { row, token };
76
+ }
77
+ /**
78
+ * Revoke a token (idempotent — re-revoking preserves the original
79
+ * `revoked_at`). Returns `null` when the id doesn't exist for `scope`.
80
+ */
81
+ export async function revokeMcpToken(q, scope, id) {
82
+ const rows = await q(`UPDATE mcp_tokens
83
+ SET revoked_at = COALESCE(revoked_at, NOW())
84
+ WHERE scope = $1 AND id = $2
85
+ RETURNING *`, [scope, id]);
86
+ return rows[0] ?? null;
87
+ }
88
+ /**
89
+ * Resolve a plaintext bearer token to its active row, or `null` if the
90
+ * token is malformed, unknown, or revoked. Parses the scope from the
91
+ * token, narrows by `(scope, prefix)`, scrypt-verifies each candidate,
92
+ * and stamps `last_used_at` on a match (best-effort; failures don't
93
+ * block auth). The host is responsible for any further checks (e.g.
94
+ * mapping `scope` to a tenant and rejecting suspended ones).
95
+ */
96
+ export async function verifyMcpToken(q, plaintext) {
97
+ const parsed = parseScopedToken(plaintext);
98
+ if (!parsed)
99
+ return null;
100
+ const prefix = bearerTokenPrefix(plaintext);
101
+ const candidates = await q(`SELECT * FROM mcp_tokens WHERE scope = $1 AND prefix = $2 AND revoked_at IS NULL`, [parsed.scope, prefix]);
102
+ for (const row of candidates) {
103
+ if (await verifyTokenSecret(plaintext, row.tokenHash)) {
104
+ // Best-effort touch — never block a valid auth on a stats write.
105
+ Promise.resolve(q(`UPDATE mcp_tokens SET last_used_at = NOW() WHERE id = $1`, [row.id])).catch(() => { });
106
+ return row;
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Opaque bearer-token primitives for MCP servers.
3
+ *
4
+ * An MCP server authenticates agents with a long-lived bearer token. A
5
+ * host issues one, stores only a hash + a short lookup prefix, and shows
6
+ * the plaintext exactly once. These helpers own the parts that are
7
+ * identical across every host — the token format, secure generation,
8
+ * and the prefix/parse helpers — while leaving hashing and storage to
9
+ * the host, which knows its own DB and crypto policy.
10
+ *
11
+ * Token shape: `<scheme>_<scope>_<secret>`, e.g. `vk_acme_Xy3…`.
12
+ * - `scheme` namespaces the token (default `vk`).
13
+ * - `scope` is an opaque routing hint (e.g. a tenant slug) the host can
14
+ * read *before* hashing, to narrow a DB lookup to one tenant. It is
15
+ * optional; without it the token is `<scheme>_<secret>`.
16
+ * - `secret` is URL-safe base64 (`A-Za-z0-9_-`) random entropy.
17
+ *
18
+ * `node:crypto` is the only import (a Node builtin), so the package keeps
19
+ * its no-third-party-runtime-dependency property.
20
+ */
21
+ export interface GenerateBearerTokenOptions {
22
+ /** Token namespace, e.g. `vk`. Default `vk`. Must be lowercase alnum/dash. */
23
+ scheme?: string;
24
+ /**
25
+ * Opaque routing hint (e.g. a tenant slug) embedded before the secret
26
+ * so the host can resolve the owner before hashing. Lowercase
27
+ * alnum/dash. Omit for an unscoped `<scheme>_<secret>` token.
28
+ */
29
+ scope?: string;
30
+ /**
31
+ * Random entropy in bytes, base64url-encoded into the secret. Default
32
+ * 24 bytes (192 bits → 32 chars). Minimum 16.
33
+ */
34
+ byteLength?: number;
35
+ }
36
+ export interface GeneratedBearerToken {
37
+ /** The full plaintext token. Show it once; never store it. */
38
+ token: string;
39
+ /** Stable lookup prefix (see {@link bearerTokenPrefix}); safe to store/display. */
40
+ prefix: string;
41
+ }
42
+ /** Chars kept from the front of a token as its (non-secret) lookup key. */
43
+ export declare const DEFAULT_PREFIX_LENGTH = 10;
44
+ /**
45
+ * Mint a new bearer token + its lookup prefix. The secret is drawn from
46
+ * a CSPRNG (`node:crypto`); the host should hash {@link GeneratedBearerToken.token}
47
+ * (bcrypt/argon2/SHA-256 — its choice) and persist only the hash + prefix.
48
+ */
49
+ export declare function generateBearerToken(opts?: GenerateBearerTokenOptions): GeneratedBearerToken;
50
+ /**
51
+ * The first `length` characters of a token. Stored alongside the hash so
52
+ * a lookup is `WHERE prefix = ?` (one indexed read) rather than a scan +
53
+ * hash-compare over every row. NOT a secret — safe to show in a UI.
54
+ */
55
+ export declare function bearerTokenPrefix(token: string, length?: number): string;
56
+ export interface ParsedScopedToken {
57
+ scheme: string;
58
+ scope: string;
59
+ secret: string;
60
+ }
61
+ /**
62
+ * Parse a `<scheme>_<scope>_<secret>` token into its parts, or `null` if
63
+ * the shape doesn't match (callers should fail closed on `null`). The
64
+ * secret must be at least 16 URL-safe chars, which rejects obviously
65
+ * truncated or malformed tokens before any DB / hash work.
66
+ */
67
+ export declare function parseScopedToken(token: string): ParsedScopedToken | null;
68
+ /**
69
+ * Extract the token from an `Authorization` header value, stripping a
70
+ * leading `Bearer ` (case-insensitive). Returns the raw value when no
71
+ * scheme prefix is present, or `null` for an empty/missing header.
72
+ */
73
+ export declare function parseBearer(header: string | null | undefined): string | null;
74
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,WAAW,0BAA0B;IACzC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAKxC;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,GAAE,0BAA+B,GACpC,oBAAoB,CAgBtB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAwB,GAAG,MAAM,CAEvF;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CASxE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAW5E"}