@stage-labs/metro 0.1.0-beta.286 → 0.1.0-beta.288
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/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/STAGE.md +11 -0
- package/runtime/marketplace/plugin/bin/policy.mjs +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +6 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +7 -3
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/agent-key.ts +148 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +14 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +2 -72
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/enroll-api.ts +101 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +40 -4
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/metro-api.ts +92 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-preview.ts +200 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-url.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +4 -7
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-relay.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime-discovery.ts +111 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime.ts +246 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-watch.ts +50 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync.ts +161 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +11 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/publication.ts +33 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-id.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-session.ts +10 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +4 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +25 -4
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +10 -20
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +16 -13
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/transfer-store.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-schema.ts +4 -0
- package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +2 -1
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +12 -3
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +3 -0
- package/runtime/node_modules/@metro-labs/http/package.json +10 -2
- package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +110 -0
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +6 -2
- package/runtime/node_modules/@metro-labs/http/src/connector-delivery-validation.ts +115 -0
- package/runtime/node_modules/@metro-labs/http/src/connector-delivery.ts +54 -0
- package/runtime/node_modules/@metro-labs/{daemon/src/connectors/oauth-discovery.ts → http/src/connector-discovery.ts} +31 -14
- package/runtime/node_modules/@metro-labs/http/src/connector-input.ts +73 -0
- package/runtime/node_modules/@metro-labs/{daemon/src/connectors/verify.ts → http/src/connector-mcp.ts} +16 -16
- package/runtime/node_modules/@metro-labs/{daemon/src/connectors/oauth-client.ts → http/src/connector-oauth.ts} +36 -13
- package/runtime/node_modules/@metro-labs/http/src/connector-policy.ts +53 -0
- package/runtime/node_modules/@metro-labs/{daemon/src/connectors/url.ts → http/src/connector-url.ts} +1 -1
- package/runtime/node_modules/@metro-labs/http/src/ed25519.ts +56 -0
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- /package/runtime/node_modules/@metro-labs/{daemon/src/connectors/reach.ts → http/src/connector-reach.ts} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { publishLocalConnectors } from './publication.js';
|
|
3
|
+
import type { RelayTarget } from './relay-target.js';
|
|
4
|
+
export type { RelayTarget } from './relay-target.js';
|
|
4
5
|
import type { ToolPolicy } from '../policy/policy.js';
|
|
5
6
|
import type { ToolGroup } from '@metro-labs/core/stations/types';
|
|
6
7
|
import { listRemoteTools, type RemoteTool } from './tools.js';
|
|
@@ -8,28 +9,23 @@ import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
|
|
|
8
9
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
9
10
|
import {
|
|
10
11
|
authHeaders,
|
|
11
|
-
connectorUrlText,
|
|
12
|
-
ConnectorVerifyError,
|
|
13
|
-
parseConnectorUrl,
|
|
14
12
|
verifyRemoteMcp,
|
|
15
13
|
type ConnectorAuth,
|
|
16
14
|
type OAuthAuth,
|
|
17
15
|
type VerifiedRecord,
|
|
18
|
-
} from '
|
|
16
|
+
} from '@metro-labs/http/connector-mcp';
|
|
17
|
+
import { connectorUrlText, ConnectorVerifyError, parseConnectorUrl } from '@metro-labs/http/connector-url';
|
|
19
18
|
import { oauthExpired, refreshOAuth } from './oauth.js';
|
|
20
|
-
import { advertisesOAuth } from '
|
|
19
|
+
import { advertisesOAuth } from '@metro-labs/http/connector-discovery';
|
|
20
|
+
import { ConnectorError, connectorAuth, connectorClient, connectorName } from '@metro-labs/http/connector-input';
|
|
21
21
|
import {
|
|
22
|
-
ConnectorError,
|
|
23
|
-
connectorAuth,
|
|
24
|
-
connectorClient,
|
|
25
|
-
connectorName,
|
|
26
22
|
readConfig,
|
|
27
23
|
signInState,
|
|
28
24
|
stamp,
|
|
29
25
|
type ConnectorConfig,
|
|
30
26
|
type ConnectorSignIn,
|
|
31
27
|
} from './config.js';
|
|
32
|
-
import type { OAuthClient } from '
|
|
28
|
+
import type { OAuthClient } from '@metro-labs/http/connector-oauth';
|
|
33
29
|
import { agentsDir } from '../agents/files.js';
|
|
34
30
|
import { newId } from '@metro-labs/core/ids';
|
|
35
31
|
import type { LoadedConnector } from '../stations/materialize.js';
|
|
@@ -119,8 +115,7 @@ export function readLocalConnectors(dir = agentsDir()): LocalConnectorRow[] {
|
|
|
119
115
|
function writeRows(dir: string, rows: LocalConnectorRow[]): void {
|
|
120
116
|
writeSecure(filePath(dir), `${JSON.stringify({ version: 1, connectors: rows }, null, 2)}\n`);
|
|
121
117
|
if (dir !== agentsDir()) return;
|
|
122
|
-
|
|
123
|
-
registerConnectors(rows);
|
|
118
|
+
publishLocalConnectors(rows);
|
|
124
119
|
}
|
|
125
120
|
|
|
126
121
|
const missing = (): ConnectorError => new ConnectorError('no such connector', 404);
|
|
@@ -298,7 +293,7 @@ export async function localSetConnectorPolicy(id: string, policy: ToolPolicy, di
|
|
|
298
293
|
|
|
299
294
|
export async function loadConnectorPolicies(dir = agentsDir()): Promise<void> {
|
|
300
295
|
const rows = readLocalConnectors(dir);
|
|
301
|
-
|
|
296
|
+
publishLocalConnectors(rows);
|
|
302
297
|
for (const row of rows) if (row.config.policy !== undefined) await refreshGroups(row.id, dir);
|
|
303
298
|
}
|
|
304
299
|
|
|
@@ -332,11 +327,6 @@ function refreshOnce(row: LocalConnectorRow, auth: OAuthAuth, dir: string): Prom
|
|
|
332
327
|
return job;
|
|
333
328
|
}
|
|
334
329
|
|
|
335
|
-
export type RelayTarget =
|
|
336
|
-
| { kind: 'ok'; url: string; headers: Record<string, string> }
|
|
337
|
-
| { kind: 'missing' }
|
|
338
|
-
| { kind: 'signin' };
|
|
339
|
-
|
|
340
330
|
function staleUsable(auth: OAuthAuth, now = Date.now()): boolean {
|
|
341
331
|
return auth.expiresAt === undefined || auth.expiresAt > now;
|
|
342
332
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
|
-
import {
|
|
3
|
+
import type { FetchLike } from '@metro-labs/core/stations/oauth';
|
|
4
|
+
import { authHeaders, INITIALIZE, mcpPost, payloadOf, type ConnectorAuth } from '@metro-labs/http/connector-mcp';
|
|
5
|
+
import { refused } from '@metro-labs/http/connector-url';
|
|
4
6
|
|
|
5
7
|
export interface RemoteTool {
|
|
6
8
|
name: string;
|
|
@@ -22,8 +24,8 @@ function resultOf(text: string, contentType: string): Record<string, unknown> |
|
|
|
22
24
|
return isRecord(parsed) && isRecord(parsed.result) ? parsed.result : null;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
async function rpc(url: URL, auth: ConnectorAuth, session: Map<string, string>, body: unknown): Promise<Record<string, unknown> | null> {
|
|
26
|
-
const res = await mcpPost(url, auth, body, { timeoutMs: TIMEOUT_MS, headers: Object.fromEntries(session) });
|
|
27
|
+
async function rpc(url: URL, auth: ConnectorAuth, session: Map<string, string>, body: unknown, fetchImpl?: FetchLike): Promise<Record<string, unknown> | null> {
|
|
28
|
+
const res = await mcpPost(url, auth, body, { timeoutMs: TIMEOUT_MS, headers: Object.fromEntries(session), fetch: fetchImpl });
|
|
27
29
|
const id = res.headers.get('mcp-session-id');
|
|
28
30
|
if (id !== null && id !== '') session.set('mcp-session-id', id);
|
|
29
31
|
const text = await res.text().catch(() => '');
|
|
@@ -39,13 +41,14 @@ function toTool(raw: unknown): RemoteTool | null {
|
|
|
39
41
|
return { name: raw.name, title, description: typeof raw.description === 'string' ? raw.description : '', readOnly: annotations.readOnlyHint === true };
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
async function endSession(url: URL, auth: ConnectorAuth, session: Map<string, string
|
|
44
|
+
async function endSession(url: URL, auth: ConnectorAuth, session: Map<string, string>, fetchImpl?: FetchLike): Promise<void> {
|
|
43
45
|
if (!session.has('mcp-session-id')) return;
|
|
44
46
|
try {
|
|
45
|
-
const
|
|
47
|
+
const send = fetchImpl ?? fetch;
|
|
48
|
+
const res = await send(url.toString(), { method: 'DELETE', redirect: 'manual', signal: AbortSignal.timeout(TIMEOUT_MS), headers: { ...authHeaders(auth), ...Object.fromEntries(session) } });
|
|
46
49
|
await res.body?.cancel();
|
|
47
50
|
} catch (err) {
|
|
48
|
-
log.debug({ host: url.hostname, err: errMsg(err) }, 'connector-tools: the remote declined to end the session');
|
|
51
|
+
log.debug(fetchImpl === undefined ? { host: url.hostname, err: errMsg(err) } : {}, 'connector-tools: the remote declined to end the session');
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -57,11 +60,11 @@ function nextCursor(result: Record<string, unknown>, current: string): string |
|
|
|
57
60
|
return typeof next !== 'string' || next === '' || next === current ? null : next;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
async function walkTools(url: URL, auth: ConnectorAuth, session: Map<string, string
|
|
63
|
+
async function walkTools(url: URL, auth: ConnectorAuth, session: Map<string, string>, fetchImpl?: FetchLike): Promise<RemoteTool[]> {
|
|
61
64
|
const tools: RemoteTool[] = [];
|
|
62
65
|
let cursor: string | null = '';
|
|
63
66
|
for (let page = 0; page < MAX_PAGES && cursor !== null; page += 1) {
|
|
64
|
-
const result = await rpc(url, auth, session, { jsonrpc: '2.0', id: 2 + page, method: 'tools/list', params: cursor === '' ? {} : { cursor } });
|
|
67
|
+
const result = await rpc(url, auth, session, { jsonrpc: '2.0', id: 2 + page, method: 'tools/list', params: cursor === '' ? {} : { cursor } }, fetchImpl);
|
|
65
68
|
if (result === null) break;
|
|
66
69
|
tools.push(...toolsOf(result));
|
|
67
70
|
cursor = nextCursor(result, cursor);
|
|
@@ -69,13 +72,13 @@ async function walkTools(url: URL, auth: ConnectorAuth, session: Map<string, str
|
|
|
69
72
|
return tools;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
export async function listRemoteTools(url: URL, auth: ConnectorAuth): Promise<RemoteTool[]> {
|
|
75
|
+
export async function listRemoteTools(url: URL, auth: ConnectorAuth, fetchImpl?: FetchLike): Promise<RemoteTool[]> {
|
|
73
76
|
const session = new Map<string, string>();
|
|
74
|
-
const init = await rpc(url, auth, session, INITIALIZE);
|
|
77
|
+
const init = await rpc(url, auth, session, INITIALIZE, fetchImpl);
|
|
75
78
|
if (init === null || typeof init.protocolVersion !== 'string') throw refused(`${url.hostname} answered, but it does not speak MCP.`);
|
|
76
79
|
session.set('mcp-protocol-version', init.protocolVersion);
|
|
77
|
-
await rpc(url, auth, session, { jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
78
|
-
const tools = await walkTools(url, auth, session);
|
|
79
|
-
await endSession(url, auth, session);
|
|
80
|
+
await rpc(url, auth, session, { jsonrpc: '2.0', method: 'notifications/initialized' }, fetchImpl);
|
|
81
|
+
const tools = await walkTools(url, auth, session, fetchImpl);
|
|
82
|
+
await endSession(url, auth, session, fetchImpl);
|
|
80
83
|
return tools;
|
|
81
84
|
}
|
|
@@ -2,8 +2,9 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
2
2
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
3
|
import { newId, parseId } from '@metro-labs/core/ids';
|
|
4
4
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
|
-
import { connectorAuth, connectorName
|
|
6
|
-
import {
|
|
5
|
+
import { connectorAuth, connectorName } from '@metro-labs/http/connector-input';
|
|
6
|
+
import { readConfig } from './config.js';
|
|
7
|
+
import { parseConnectorUrl } from '@metro-labs/http/connector-url';
|
|
7
8
|
import { normalizePolicy } from '../policy/policy.js';
|
|
8
9
|
import { localImportConnectors, readLocalConnectors, type LocalConnectorRow } from './store.js';
|
|
9
10
|
|
|
@@ -32,6 +32,10 @@ export const frameSchema = {
|
|
|
32
32
|
type: 'object',
|
|
33
33
|
description:
|
|
34
34
|
'Optional: {"url": "https://…"}, a node URL that serves this widget live (2048 characters at most). ' +
|
|
35
|
+
"Stage then sends the frame's actions to that node, not to you, and the node's reply replaces the " +
|
|
36
|
+
'frame on that device, with no message to you. Only an action with "handler": "client" (e.g. ' +
|
|
37
|
+
'{"type": "ask", "handler": "client"}) still comes back to you as a Frame action; without a source, ' +
|
|
38
|
+
"every action does. Refresh is in the frame's three-dot menu, so a node needs no Refresh button. " +
|
|
35
39
|
'Add to dashboard on the frame then adds a live widget loaded from that URL instead of a copy.',
|
|
36
40
|
properties: { url: { type: 'string' } },
|
|
37
41
|
required: ['url'],
|
|
@@ -32,7 +32,8 @@ export const targetKey = (target: PolicyTarget): string =>
|
|
|
32
32
|
target.kind === 'channel' ? `channel:${target.station}/${target.account}` : `connector:${target.id}`;
|
|
33
33
|
|
|
34
34
|
export function decide(policy: ToolPolicy | undefined, tool: PolicyTool): Access {
|
|
35
|
-
|
|
35
|
+
const byTool = policy?.tools !== undefined && Object.hasOwn(policy.tools, tool.name) ? policy.tools[tool.name] : undefined;
|
|
36
|
+
return byTool ?? policy?.[tool.group] ?? 'allow';
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export function strictest(list: readonly Access[]): Access {
|
|
@@ -6,7 +6,8 @@ import { assertAttachOwner, finishGmailUpgrade, startGmailAttach } from './gmail
|
|
|
6
6
|
import { detachWithGmailRevocation } from './gmail-detach.js';
|
|
7
7
|
import { recentSenders } from '../agents/senders.js';
|
|
8
8
|
import { forwardTrainCall } from '../stations/train-call.js';
|
|
9
|
-
import {
|
|
9
|
+
import { publishLocalConnectors } from '../connectors/publication.js';
|
|
10
|
+
import { invalidateOrgConnectorSync, orgConnectorRelaySession } from '../connectors/org-sync-watch.js';
|
|
10
11
|
import type { AgentApiDeps } from '../agents/api.js';
|
|
11
12
|
import { ATTACHABLE, type AccountApiDeps } from '../agents/accounts-api.js';
|
|
12
13
|
import type { SessionApis } from './session-apis.js';
|
|
@@ -14,6 +15,7 @@ import type { ModeInfo } from '@metro-labs/http/mode-api';
|
|
|
14
15
|
import { loadedAgentOf, type AgentBundle, type BundleApiDeps } from '../agents/bundle.js';
|
|
15
16
|
import { METRO_VERSION } from '@metro-labs/core/version';
|
|
16
17
|
import type { ConnectorApiDeps } from '../connectors/api.js';
|
|
18
|
+
import { connectorMigrationPreview } from '../connectors/migration-preview.js';
|
|
17
19
|
import { ConnectorTransfers } from '../connectors/transfer-api.js';
|
|
18
20
|
import { copyConnectorRows } from '../connectors/transfer-store.js';
|
|
19
21
|
import { authenticate } from '../mcp/request-identity.js';
|
|
@@ -121,9 +123,10 @@ function connectorIdsOfLocalAgents(ids: string[]): Promise<Map<string, string[]>
|
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
const connectorApi: ConnectorApiDeps = {
|
|
126
|
+
migrationPreview: () => connectorMigrationPreview(localOwner()),
|
|
124
127
|
listConnectors: async () => {
|
|
125
128
|
const rows = await localListConnectors();
|
|
126
|
-
|
|
129
|
+
publishLocalConnectors(readLocalConnectors());
|
|
127
130
|
return rows;
|
|
128
131
|
},
|
|
129
132
|
createConnector: localCreateConnector,
|
|
@@ -140,6 +143,7 @@ const connectorApi: ConnectorApiDeps = {
|
|
|
140
143
|
|
|
141
144
|
const relayApi: RelayApiDeps = {
|
|
142
145
|
target: (connectorId, force) => localRelayTarget(connectorId, force),
|
|
146
|
+
open: orgConnectorRelaySession,
|
|
143
147
|
identify: (req) => {
|
|
144
148
|
const who = authenticate(req);
|
|
145
149
|
return who?.kind === 'agent' ? { subject: 'agent-key', agentId: who.agentId } : null;
|
|
@@ -202,11 +206,16 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
|
|
|
202
206
|
bundleApi: bundleApi(deps),
|
|
203
207
|
connectorApi,
|
|
204
208
|
connectorTransfers: new ConnectorTransfers({ read: readLocalConnectors, copy: copyConnectorRows }),
|
|
209
|
+
enrollApi: { owner: () => localOwner(), invalidate: invalidateOrgConnectorSync },
|
|
205
210
|
relayApi,
|
|
206
211
|
claudeApi: { liveEvents: deps.liveEvents },
|
|
207
212
|
updateApi: { restart: deps.restart },
|
|
208
213
|
controlApi: { restart: deps.restart, stop: deps.stop },
|
|
209
|
-
ownerApi: { setOwner: (owner) =>
|
|
214
|
+
ownerApi: { setOwner: (owner) => {
|
|
215
|
+
const saved = setLocalOwner(owner);
|
|
216
|
+
invalidateOrgConnectorSync();
|
|
217
|
+
return saved;
|
|
218
|
+
} },
|
|
210
219
|
machineApi: {},
|
|
211
220
|
modelApi: { switchModel: deps.switchModel, owner: localOwner },
|
|
212
221
|
gateway: { config: readModelConfig },
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '../connectors/api.js';
|
|
9
9
|
import type { RelayApiDeps } from '../connectors/relay.js';
|
|
10
10
|
import type { ConnectorTransfers } from '../connectors/transfer-api.js';
|
|
11
|
+
import { handleEnrollRequest, type EnrollApiDeps } from '../connectors/enroll-api.js';
|
|
11
12
|
import { handleClaudeRequest, type ClaudeApiDeps } from '../claude/api.js';
|
|
12
13
|
import type { ModeInfo } from '@metro-labs/http/mode-api';
|
|
13
14
|
import { handleBundleRequest, type BundleApiDeps } from '../agents/bundle.js';
|
|
@@ -37,6 +38,7 @@ export interface SessionApis {
|
|
|
37
38
|
claudeApi?: ClaudeApiDeps;
|
|
38
39
|
connectorApi?: ConnectorApiDeps;
|
|
39
40
|
connectorTransfers?: ConnectorTransfers;
|
|
41
|
+
enrollApi?: EnrollApiDeps;
|
|
40
42
|
relayApi?: RelayApiDeps;
|
|
41
43
|
mode?: () => ModeInfo;
|
|
42
44
|
}
|
|
@@ -54,6 +56,7 @@ export function handleSessionApis(
|
|
|
54
56
|
() => handleRunEventsRequest(req, res),
|
|
55
57
|
() => handleApprovalsRequest(req, res),
|
|
56
58
|
...when(apis.connectorTransfers, (d) => d.handle(req, res)),
|
|
59
|
+
...when(apis.enrollApi, (d) => handleEnrollRequest(req, res, d)),
|
|
57
60
|
...when(apis.connectorApi, (d) => handleConnectorApiRequest(req, res, d)),
|
|
58
61
|
...when(apis.bundleApi, (d) => handleBundleRequest(req, res, d)),
|
|
59
62
|
...when(apis.updateApi, (d) => handleUpdateRequest(req, res, d)),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@metro-labs/http",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "How a metro server authenticates and answers a request: CORS, the JSON helpers, ApiError, the signed-identity header, the identity registry
|
|
5
|
+
"description": "How a metro server authenticates and answers a request: CORS, the JSON helpers, ApiError, the signed-identity header, the identity registry, AWS request signing, the signature a box puts on its requests to api.metro.box, and the connector client both use (url checks, the MCP initialize, OAuth discovery, registration and tokens). Shared by the daemon and api.metro.box, never by a station.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
"./cors": "./src/cors.ts",
|
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
"./return-to": "./src/return-to.ts",
|
|
12
12
|
"./mode-api": "./src/mode-api.ts",
|
|
13
13
|
"./workos-token": "./src/workos-token.ts",
|
|
14
|
-
"./sigv4": "./src/sigv4.ts"
|
|
14
|
+
"./sigv4": "./src/sigv4.ts",
|
|
15
|
+
"./agent-signature": "./src/agent-signature.ts",
|
|
16
|
+
"./connector-url": "./src/connector-url.ts",
|
|
17
|
+
"./connector-reach": "./src/connector-reach.ts",
|
|
18
|
+
"./connector-mcp": "./src/connector-mcp.ts",
|
|
19
|
+
"./connector-discovery": "./src/connector-discovery.ts",
|
|
20
|
+
"./connector-oauth": "./src/connector-oauth.ts",
|
|
21
|
+
"./connector-input": "./src/connector-input.ts",
|
|
22
|
+
"./connector-delivery": "./src/connector-delivery.ts"
|
|
15
23
|
},
|
|
16
24
|
"scripts": {
|
|
17
25
|
"build": "tsc",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash, createPublicKey, diffieHellman, generateKeyPairSync, randomBytes, sign, verify, type KeyObject } from 'node:crypto';
|
|
2
|
+
import { strongEd25519 } from './ed25519.js';
|
|
3
|
+
|
|
4
|
+
export const AGENT_SCHEME = 'MetroAgent';
|
|
5
|
+
export const AGENT_CLOCK_SKEW_MS = 5 * 60_000;
|
|
6
|
+
|
|
7
|
+
const VERSION = 'metro-agent-request/v1';
|
|
8
|
+
const KEY_ID_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
9
|
+
const NONCE_RE = /^[A-Za-z0-9_-]{22}$/;
|
|
10
|
+
const SIGNATURE_RE = /^[A-Za-z0-9_-]{86}$/;
|
|
11
|
+
const TIME_RE = /^[0-9]{1,15}$/;
|
|
12
|
+
const RAW_KEY_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
13
|
+
|
|
14
|
+
export interface AgentRequest {
|
|
15
|
+
method: string;
|
|
16
|
+
host: string;
|
|
17
|
+
path: string;
|
|
18
|
+
body: Uint8Array;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AgentProof {
|
|
22
|
+
keyId: string;
|
|
23
|
+
time: number;
|
|
24
|
+
nonce: string;
|
|
25
|
+
signature: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Curve = 'Ed25519' | 'X25519';
|
|
29
|
+
|
|
30
|
+
const digest = (bytes: Uint8Array): string => createHash('sha256').update(bytes).digest('base64url');
|
|
31
|
+
|
|
32
|
+
export const agentKeyId = (signingKey: string): string => digest(Buffer.from(signingKey, 'base64url'));
|
|
33
|
+
|
|
34
|
+
function publicKeyOf(raw: string, curve: Curve): KeyObject | null {
|
|
35
|
+
if (!RAW_KEY_RE.test(raw) || Buffer.from(raw, 'base64url').toString('base64url') !== raw) return null;
|
|
36
|
+
try {
|
|
37
|
+
const key = createPublicKey({ key: { kty: 'OKP', crv: curve, x: raw }, format: 'jwk' });
|
|
38
|
+
return key.asymmetricKeyType === curve.toLowerCase() ? key : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function signingKeyOf(raw: string): KeyObject | null {
|
|
45
|
+
const key = publicKeyOf(raw, 'Ed25519');
|
|
46
|
+
return key !== null && strongEd25519(Buffer.from(raw, 'base64url')) ? key : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function contributes(key: KeyObject): boolean {
|
|
50
|
+
try {
|
|
51
|
+
return diffieHellman({ privateKey: generateKeyPairSync('x25519').privateKey, publicKey: key }).some((byte) => byte !== 0);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sealingKeyOf(raw: string): KeyObject | null {
|
|
58
|
+
const key = publicKeyOf(raw, 'X25519');
|
|
59
|
+
return key !== null && contributes(key) ? key : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function rawPublicKey(key: KeyObject): string {
|
|
63
|
+
const { x } = key.export({ format: 'jwk' });
|
|
64
|
+
if (typeof x !== 'string') throw new Error('not an OKP public key');
|
|
65
|
+
return x;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function signingInput(request: AgentRequest, proof: Omit<AgentProof, 'signature'>): Buffer {
|
|
69
|
+
return Buffer.from([
|
|
70
|
+
VERSION,
|
|
71
|
+
request.method.toUpperCase(),
|
|
72
|
+
request.host.toLowerCase(),
|
|
73
|
+
request.path,
|
|
74
|
+
proof.keyId,
|
|
75
|
+
String(proof.time),
|
|
76
|
+
proof.nonce,
|
|
77
|
+
digest(request.body),
|
|
78
|
+
].join('\n'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function signAgentRequest(request: AgentRequest, privateKey: KeyObject, keyId: string, time: number): string {
|
|
82
|
+
const nonce = randomBytes(16).toString('base64url');
|
|
83
|
+
const signature = sign(null, signingInput(request, { keyId, time, nonce }), privateKey).toString('base64url');
|
|
84
|
+
return `${AGENT_SCHEME} ${keyId}.${String(time)}.${nonce}.${signature}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const PARTS = [KEY_ID_RE, TIME_RE, NONCE_RE, SIGNATURE_RE];
|
|
88
|
+
|
|
89
|
+
function partsOf(header: string | undefined): string[] | null {
|
|
90
|
+
if (header === undefined || header.length > 256) return null;
|
|
91
|
+
const [scheme, value, extra] = header.trim().split(' ');
|
|
92
|
+
if (scheme !== AGENT_SCHEME || value === undefined || extra !== undefined) return null;
|
|
93
|
+
const parts = value.split('.');
|
|
94
|
+
return parts.length === PARTS.length && parts.every((part, i) => PARTS[i]?.test(part) === true) ? parts : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function readAgentProof(header: string | undefined): AgentProof | null {
|
|
98
|
+
const parts = partsOf(header);
|
|
99
|
+
if (parts === null) return null;
|
|
100
|
+
const [keyId = '', time = '', nonce = '', signature = ''] = parts;
|
|
101
|
+
return { keyId, time: Number(time), nonce, signature };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const withinSkew = (time: number, now: number): boolean => Math.abs(now - time) <= AGENT_CLOCK_SKEW_MS;
|
|
105
|
+
|
|
106
|
+
export function agentSignatureValid(request: AgentRequest, proof: AgentProof, signingKey: string): boolean {
|
|
107
|
+
const key = signingKeyOf(signingKey);
|
|
108
|
+
if (key === null || agentKeyId(signingKey) !== proof.keyId) return false;
|
|
109
|
+
return verify(null, signingInput(request, proof), key, Buffer.from(proof.signature, 'base64url'));
|
|
110
|
+
}
|
|
@@ -60,7 +60,7 @@ export function apiSession(req: IncomingMessage): Promise<ApiSession | null> {
|
|
|
60
60
|
return bearerSessions === null ? Promise.resolve(null) : bearerSessions(req);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export async function
|
|
63
|
+
export async function readBodyBytes(req: IncomingMessage, max = BODY_MAX): Promise<Buffer> {
|
|
64
64
|
const chunks: Buffer[] = [];
|
|
65
65
|
let total = 0;
|
|
66
66
|
let over = false;
|
|
@@ -72,7 +72,11 @@ export async function readJsonBody(req: IncomingMessage, max = BODY_MAX): Promis
|
|
|
72
72
|
else if (total > 2 * max) break;
|
|
73
73
|
}
|
|
74
74
|
if (over) throw new ApiError('request body too large', 413);
|
|
75
|
-
|
|
75
|
+
return Buffer.concat(chunks);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function readJsonBody(req: IncomingMessage, max = BODY_MAX): Promise<unknown> {
|
|
79
|
+
const raw = (await readBodyBytes(req, max)).toString('utf8').trim();
|
|
76
80
|
if (raw === '') return {};
|
|
77
81
|
try {
|
|
78
82
|
return JSON.parse(raw);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { parseId } from '@metro-labs/core/ids';
|
|
2
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
|
+
import { connectorName } from './connector-input.js';
|
|
4
|
+
import { connectorPolicyOf, type ConnectorPolicy } from './connector-policy.js';
|
|
5
|
+
import { parseConnectorUrl } from './connector-url.js';
|
|
6
|
+
import { isOrganizationId } from './workos-token.js';
|
|
7
|
+
|
|
8
|
+
export const CONNECTOR_DELIVERY_LEASE_MS = 90_000;
|
|
9
|
+
export const CONNECTOR_DELIVERY_MAX_BYTES = 1024 * 1024;
|
|
10
|
+
export const DELIVERY_PLAINTEXT_MAX = Math.floor((CONNECTOR_DELIVERY_MAX_BYTES - 1024) * 3 / 4);
|
|
11
|
+
|
|
12
|
+
export interface DeliveryContext {
|
|
13
|
+
owner: string;
|
|
14
|
+
agent: string;
|
|
15
|
+
keyId: string;
|
|
16
|
+
requestId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DeliveredConnector {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
url: string;
|
|
23
|
+
auth: 'none' | 'header' | 'oauth';
|
|
24
|
+
headers: Record<string, string>;
|
|
25
|
+
policy: ConnectorPolicy;
|
|
26
|
+
revision: string;
|
|
27
|
+
state: 'ready' | 'signin' | 'unavailable';
|
|
28
|
+
expiresAt?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DeliverySnapshot {
|
|
32
|
+
version: 1;
|
|
33
|
+
issuedAt: number;
|
|
34
|
+
expiresAt: number;
|
|
35
|
+
connectors: DeliveredConnector[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function deliveryBytes(value: unknown, length?: number): Buffer {
|
|
39
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error('Invalid connector delivery encoding.');
|
|
40
|
+
const bytes = Buffer.from(value, 'base64url');
|
|
41
|
+
if (bytes.toString('base64url') !== value || (length !== undefined && bytes.length !== length)) throw new Error('Invalid connector delivery encoding.');
|
|
42
|
+
return bytes;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function deliveryRecord(value: unknown, fields: string[]): Record<string, unknown> {
|
|
46
|
+
if (!isRecord(value) || Object.keys(value).some((key) => !fields.includes(key))) throw new Error('Invalid connector delivery.');
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function deliveryContextBytes(context: DeliveryContext): Buffer {
|
|
51
|
+
const value = deliveryRecord(context, ['owner', 'agent', 'keyId', 'requestId']);
|
|
52
|
+
if (typeof value.owner !== 'string' || !isOrganizationId(value.owner) || parseId(value.agent) === null) throw new Error('Invalid connector delivery context.');
|
|
53
|
+
deliveryBytes(value.keyId, 32);
|
|
54
|
+
deliveryBytes(value.requestId, 32);
|
|
55
|
+
return Buffer.from(JSON.stringify(['metro-connector-delivery/v1', value.owner, value.agent, value.keyId, value.requestId]));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const timestamp = (value: unknown): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
59
|
+
|
|
60
|
+
function headerValue(name: string, text: unknown, auth: DeliveredConnector['auth']): string {
|
|
61
|
+
if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/.test(name) || ['__proto__', 'constructor', 'prototype'].includes(name) || typeof text !== 'string' || !/^[\x20-\x7e]{1,16384}$/.test(text)) throw new Error('Invalid connector delivery headers.');
|
|
62
|
+
if (auth === 'oauth' && (name !== 'Authorization' || !/^Bearer [\x21-\x7e]+$/.test(text))) throw new Error('Invalid connector delivery access token.');
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function headersOf(value: unknown, auth: DeliveredConnector['auth'], state: DeliveredConnector['state']): Record<string, string> {
|
|
67
|
+
if (!isRecord(value)) throw new Error('Invalid connector delivery headers.');
|
|
68
|
+
const entries = Object.entries(value);
|
|
69
|
+
const expected = auth !== 'none' && state === 'ready' ? 1 : 0;
|
|
70
|
+
if (entries.length !== expected) throw new Error('Invalid connector delivery headers.');
|
|
71
|
+
const out: Record<string, string> = {};
|
|
72
|
+
for (const [name, text] of entries) out[name] = headerValue(name, text, auth);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function authenticationOf(value: unknown): DeliveredConnector['auth'] {
|
|
77
|
+
if (value !== 'none' && value !== 'header' && value !== 'oauth') throw new Error('Invalid delivered connector authentication.');
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stateOf(value: unknown, auth: DeliveredConnector['auth']): DeliveredConnector['state'] {
|
|
82
|
+
if (value !== 'ready' && value !== 'signin' && value !== 'unavailable') throw new Error('Invalid delivered connector state.');
|
|
83
|
+
if (value === 'signin' && auth !== 'oauth') throw new Error('Invalid delivered connector state.');
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function urlOf(value: unknown): string {
|
|
88
|
+
const url = parseConnectorUrl(value);
|
|
89
|
+
if (url.protocol !== 'https:' || url.toString().length > 2048 || typeof value !== 'string') throw new Error('Invalid delivered connector URL.');
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function connectorOf(raw: unknown): DeliveredConnector {
|
|
94
|
+
const value = deliveryRecord(raw, ['id', 'name', 'url', 'auth', 'headers', 'policy', 'revision', 'state', 'expiresAt']);
|
|
95
|
+
const id = parseId(value.id);
|
|
96
|
+
if (id === null || typeof value.name !== 'string' || connectorName(value.name) !== value.name) throw new Error('Invalid delivered connector.');
|
|
97
|
+
const url = urlOf(value.url);
|
|
98
|
+
const auth = authenticationOf(value.auth);
|
|
99
|
+
const state = stateOf(value.state, auth);
|
|
100
|
+
deliveryBytes(value.revision, 32);
|
|
101
|
+
if (Object.hasOwn(value, 'expiresAt') && !timestamp(value.expiresAt)) throw new Error('Invalid delivered connector expiry.');
|
|
102
|
+
return { id, name: value.name, url, auth, state, revision: String(value.revision), headers: headersOf(value.headers, auth, state), policy: connectorPolicyOf(value.policy), ...(timestamp(value.expiresAt) ? { expiresAt: value.expiresAt } : {}) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function deliverySnapshotOf(raw: unknown): DeliverySnapshot {
|
|
106
|
+
const value = deliveryRecord(raw, ['version', 'issuedAt', 'expiresAt', 'connectors']);
|
|
107
|
+
if (value.version !== 1 || !timestamp(value.issuedAt) || !timestamp(value.expiresAt) || value.expiresAt <= value.issuedAt || value.expiresAt - value.issuedAt > CONNECTOR_DELIVERY_LEASE_MS || !Array.isArray(value.connectors)) throw new Error('Invalid connector delivery snapshot.');
|
|
108
|
+
const issuedAt = value.issuedAt;
|
|
109
|
+
const connectors = value.connectors.map((entry: unknown) => connectorOf(entry));
|
|
110
|
+
if (new Set(connectors.map((connector) => connector.id)).size !== connectors.length) throw new Error('Duplicate delivered connector.');
|
|
111
|
+
if (connectors.some((connector) => connector.state === 'ready' && connector.expiresAt !== undefined && connector.expiresAt <= issuedAt)) throw new Error('Expired delivered connector.');
|
|
112
|
+
const snapshot: DeliverySnapshot = { version: 1, issuedAt: value.issuedAt, expiresAt: value.expiresAt, connectors };
|
|
113
|
+
if (Buffer.byteLength(JSON.stringify(snapshot)) > DELIVERY_PLAINTEXT_MAX) throw new Error('Connector delivery is too large.');
|
|
114
|
+
return snapshot;
|
|
115
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, type KeyObject } from 'node:crypto';
|
|
2
|
+
import { rawPublicKey, sealingKeyOf } from './agent-signature.js';
|
|
3
|
+
import { CONNECTOR_DELIVERY_MAX_BYTES, DELIVERY_PLAINTEXT_MAX, deliveryBytes, deliveryContextBytes, deliveryRecord, deliverySnapshotOf, type DeliveryContext, type DeliverySnapshot } from './connector-delivery-validation.js';
|
|
4
|
+
|
|
5
|
+
export { CONNECTOR_DELIVERY_LEASE_MS, CONNECTOR_DELIVERY_MAX_BYTES, deliverySnapshotOf, type DeliveryContext, type DeliverySnapshot, type DeliveredConnector } from './connector-delivery-validation.js';
|
|
6
|
+
export { combineConnectorPolicies, connectorPolicyOf, type ConnectorPolicy } from './connector-policy.js';
|
|
7
|
+
|
|
8
|
+
export interface SealedDelivery {
|
|
9
|
+
version: 1;
|
|
10
|
+
ephemeralKey: string;
|
|
11
|
+
iv: string;
|
|
12
|
+
tag: string;
|
|
13
|
+
ciphertext: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cipherKey(privateKey: KeyObject, publicKey: KeyObject, context: Buffer, ephemeral: string, recipient: string): Buffer {
|
|
17
|
+
const shared = diffieHellman({ privateKey, publicKey });
|
|
18
|
+
if (!shared.some((byte) => byte !== 0)) throw new Error('Invalid connector delivery key.');
|
|
19
|
+
return Buffer.from(hkdfSync('sha256', shared, Buffer.concat([deliveryBytes(ephemeral, 32), deliveryBytes(recipient, 32)]), context, 32));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sealConnectorDelivery(publicKey: string, context: DeliveryContext, snapshot: DeliverySnapshot): SealedDelivery {
|
|
23
|
+
const aad = deliveryContextBytes(context);
|
|
24
|
+
const recipient = sealingKeyOf(publicKey);
|
|
25
|
+
if (recipient === null) throw new Error('Invalid connector delivery key.');
|
|
26
|
+
const bytes = Buffer.from(JSON.stringify(deliverySnapshotOf(snapshot)));
|
|
27
|
+
const pair = generateKeyPairSync('x25519');
|
|
28
|
+
const ephemeralKey = rawPublicKey(pair.publicKey);
|
|
29
|
+
const iv = randomBytes(12);
|
|
30
|
+
const cipher = createCipheriv('aes-256-gcm', cipherKey(pair.privateKey, recipient, aad, ephemeralKey, publicKey), iv);
|
|
31
|
+
cipher.setAAD(aad);
|
|
32
|
+
const ciphertext = Buffer.concat([cipher.update(bytes), cipher.final()]).toString('base64url');
|
|
33
|
+
const sealed: SealedDelivery = { version: 1, ephemeralKey, iv: iv.toString('base64url'), tag: cipher.getAuthTag().toString('base64url'), ciphertext };
|
|
34
|
+
if (Buffer.byteLength(JSON.stringify({ sealed })) > CONNECTOR_DELIVERY_MAX_BYTES) throw new Error('Connector delivery is too large.');
|
|
35
|
+
return sealed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function openConnectorDelivery(privateKey: KeyObject, context: DeliveryContext, envelope: unknown): DeliverySnapshot {
|
|
39
|
+
const aad = deliveryContextBytes(context);
|
|
40
|
+
if (privateKey.type !== 'private' || privateKey.asymmetricKeyType !== 'x25519') throw new Error('Invalid connector delivery key.');
|
|
41
|
+
if (Buffer.byteLength(JSON.stringify({ sealed: envelope })) > CONNECTOR_DELIVERY_MAX_BYTES) throw new Error('Connector delivery is too large.');
|
|
42
|
+
const value = deliveryRecord(envelope, ['version', 'ephemeralKey', 'iv', 'tag', 'ciphertext']);
|
|
43
|
+
if (value.version !== 1 || typeof value.ephemeralKey !== 'string') throw new Error('Invalid connector delivery envelope.');
|
|
44
|
+
const ephemeral = sealingKeyOf(value.ephemeralKey);
|
|
45
|
+
if (ephemeral === null) throw new Error('Invalid connector delivery key.');
|
|
46
|
+
const ciphertext = deliveryBytes(value.ciphertext);
|
|
47
|
+
if (ciphertext.length > DELIVERY_PLAINTEXT_MAX) throw new Error('Connector delivery is too large.');
|
|
48
|
+
const key = cipherKey(privateKey, ephemeral, aad, value.ephemeralKey, rawPublicKey(createPublicKey(privateKey)));
|
|
49
|
+
const decipher = createDecipheriv('aes-256-gcm', key, deliveryBytes(value.iv, 12));
|
|
50
|
+
decipher.setAAD(aad);
|
|
51
|
+
decipher.setAuthTag(deliveryBytes(value.tag, 16));
|
|
52
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
53
|
+
return deliverySnapshotOf(JSON.parse(plaintext.toString('utf8')) as unknown);
|
|
54
|
+
}
|