sealbridge 0.0.1 → 0.2.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/NOTICE +43 -0
- package/README.ja.md +397 -0
- package/README.md +567 -1
- package/dist/cli/dist/acp.d.ts +2 -0
- package/dist/cli/dist/acp.js +6 -0
- package/dist/cli/dist/acp.js.map +1 -0
- package/dist/cli/dist/approval-summary.d.ts +2 -0
- package/dist/cli/dist/approval-summary.js +32 -0
- package/dist/cli/dist/approval-summary.js.map +1 -0
- package/dist/cli/dist/board.d.ts +47 -0
- package/dist/cli/dist/board.js +255 -0
- package/dist/cli/dist/board.js.map +1 -0
- package/dist/cli/dist/contacts.d.ts +18 -0
- package/dist/cli/dist/contacts.js +154 -0
- package/dist/cli/dist/contacts.js.map +1 -0
- package/dist/cli/dist/doctor.d.ts +13 -0
- package/dist/cli/dist/doctor.js +30 -0
- package/dist/cli/dist/doctor.js.map +1 -0
- package/dist/cli/dist/error-messages.d.ts +2 -0
- package/dist/cli/dist/error-messages.js +24 -0
- package/dist/cli/dist/error-messages.js.map +1 -0
- package/dist/cli/dist/index.d.ts +2 -0
- package/dist/cli/dist/index.js +194 -0
- package/dist/cli/dist/index.js.map +1 -0
- package/dist/cli/dist/invite-share.d.ts +16 -0
- package/dist/cli/dist/invite-share.js +163 -0
- package/dist/cli/dist/invite-share.js.map +1 -0
- package/dist/cli/dist/mcp.d.ts +17 -0
- package/dist/cli/dist/mcp.js +105 -0
- package/dist/cli/dist/mcp.js.map +1 -0
- package/dist/cli/dist/network.d.ts +1 -0
- package/dist/cli/dist/network.js +396 -0
- package/dist/cli/dist/network.js.map +1 -0
- package/dist/cli/dist/public-settings.d.ts +27 -0
- package/dist/cli/dist/public-settings.js +33 -0
- package/dist/cli/dist/public-settings.js.map +1 -0
- package/dist/cli/dist/receipt.d.ts +39 -0
- package/dist/cli/dist/receipt.js +90 -0
- package/dist/cli/dist/receipt.js.map +1 -0
- package/dist/cli/dist/receive-hook.d.ts +2 -0
- package/dist/cli/dist/receive-hook.js +35 -0
- package/dist/cli/dist/receive-hook.js.map +1 -0
- package/dist/cli/dist/task-input.d.ts +2 -0
- package/dist/cli/dist/task-input.js +19 -0
- package/dist/cli/dist/task-input.js.map +1 -0
- package/dist/cli/dist/wake-hook.d.ts +3 -0
- package/dist/cli/dist/wake-hook.js +32 -0
- package/dist/cli/dist/wake-hook.js.map +1 -0
- package/dist/examples/two-owner-demo/alice-policy.json +4 -0
- package/dist/examples/two-owner-demo/bob-policy.json +4 -0
- package/dist/examples/two-owner-demo/dist/run-demo.d.ts +71 -0
- package/dist/examples/two-owner-demo/dist/run-demo.js +383 -0
- package/dist/examples/two-owner-demo/dist/run-demo.js.map +1 -0
- package/dist/package.json +1 -0
- package/dist/packages/adapters/dist/command.d.ts +122 -0
- package/dist/packages/adapters/dist/command.js +378 -0
- package/dist/packages/adapters/dist/command.js.map +1 -0
- package/dist/packages/adapters/dist/credential-proxy.d.ts +37 -0
- package/dist/packages/adapters/dist/credential-proxy.js +363 -0
- package/dist/packages/adapters/dist/credential-proxy.js.map +1 -0
- package/dist/packages/adapters/dist/index.d.ts +6 -0
- package/dist/packages/adapters/dist/index.js +6 -0
- package/dist/packages/adapters/dist/index.js.map +1 -0
- package/dist/packages/adapters/dist/mcp.d.ts +32 -0
- package/dist/packages/adapters/dist/mcp.js +87 -0
- package/dist/packages/adapters/dist/mcp.js.map +1 -0
- package/dist/packages/adapters/dist/openai.d.ts +23 -0
- package/dist/packages/adapters/dist/openai.js +81 -0
- package/dist/packages/adapters/dist/openai.js.map +1 -0
- package/dist/packages/adapters/dist/sandbox.d.ts +45 -0
- package/dist/packages/adapters/dist/sandbox.js +60 -0
- package/dist/packages/adapters/dist/sandbox.js.map +1 -0
- package/dist/packages/core/dist/capability.d.ts +36 -0
- package/dist/packages/core/dist/capability.js +203 -0
- package/dist/packages/core/dist/capability.js.map +1 -0
- package/dist/packages/core/dist/contract.d.ts +33 -0
- package/dist/packages/core/dist/contract.js +97 -0
- package/dist/packages/core/dist/contract.js.map +1 -0
- package/dist/packages/core/dist/crypto.d.ts +34 -0
- package/dist/packages/core/dist/crypto.js +329 -0
- package/dist/packages/core/dist/crypto.js.map +1 -0
- package/dist/packages/core/dist/discovery.d.ts +129 -0
- package/dist/packages/core/dist/discovery.js +527 -0
- package/dist/packages/core/dist/discovery.js.map +1 -0
- package/dist/packages/core/dist/edge.d.ts +4 -0
- package/dist/packages/core/dist/edge.js +6 -0
- package/dist/packages/core/dist/edge.js.map +1 -0
- package/dist/packages/core/dist/index.d.ts +9 -0
- package/dist/packages/core/dist/index.js +10 -0
- package/dist/packages/core/dist/index.js.map +1 -0
- package/dist/packages/core/dist/receipts.d.ts +9 -0
- package/dist/packages/core/dist/receipts.js +67 -0
- package/dist/packages/core/dist/receipts.js.map +1 -0
- package/dist/packages/core/dist/relay-auth.d.ts +40 -0
- package/dist/packages/core/dist/relay-auth.js +106 -0
- package/dist/packages/core/dist/relay-auth.js.map +1 -0
- package/dist/packages/core/dist/revocation.d.ts +58 -0
- package/dist/packages/core/dist/revocation.js +112 -0
- package/dist/packages/core/dist/revocation.js.map +1 -0
- package/dist/packages/core/dist/schema.d.ts +11 -0
- package/dist/packages/core/dist/schema.js +48 -0
- package/dist/packages/core/dist/schema.js.map +1 -0
- package/dist/packages/core/dist/types.d.ts +172 -0
- package/dist/packages/core/dist/types.js +2 -0
- package/dist/packages/core/dist/types.js.map +1 -0
- package/dist/packages/ledger/dist/index.d.ts +84 -0
- package/dist/packages/ledger/dist/index.js +269 -0
- package/dist/packages/ledger/dist/index.js.map +1 -0
- package/dist/packages/relay/dist/fetch.d.ts +250 -0
- package/dist/packages/relay/dist/fetch.js +1401 -0
- package/dist/packages/relay/dist/fetch.js.map +1 -0
- package/dist/packages/relay/dist/index.d.ts +103 -0
- package/dist/packages/relay/dist/index.js +855 -0
- package/dist/packages/relay/dist/index.js.map +1 -0
- package/dist/packages/relay/dist/journal.d.ts +77 -0
- package/dist/packages/relay/dist/journal.js +339 -0
- package/dist/packages/relay/dist/journal.js.map +1 -0
- package/dist/packages/relay/dist/notifications.d.ts +63 -0
- package/dist/packages/relay/dist/notifications.js +55 -0
- package/dist/packages/relay/dist/notifications.js.map +1 -0
- package/dist/packages/relay/dist/retry.d.ts +12 -0
- package/dist/packages/relay/dist/retry.js +59 -0
- package/dist/packages/relay/dist/retry.js.map +1 -0
- package/dist/packages/relay/dist/ssrf.d.ts +32 -0
- package/dist/packages/relay/dist/ssrf.js +213 -0
- package/dist/packages/relay/dist/ssrf.js.map +1 -0
- package/dist/packages/relay/dist/worker.d.ts +92 -0
- package/dist/packages/relay/dist/worker.js +460 -0
- package/dist/packages/relay/dist/worker.js.map +1 -0
- package/dist/packages/runtime/dist/a2a.d.ts +8 -0
- package/dist/packages/runtime/dist/a2a.js +69 -0
- package/dist/packages/runtime/dist/a2a.js.map +1 -0
- package/dist/packages/runtime/dist/adapter.d.ts +51 -0
- package/dist/packages/runtime/dist/adapter.js +2 -0
- package/dist/packages/runtime/dist/adapter.js.map +1 -0
- package/dist/packages/runtime/dist/approval-providers.d.ts +99 -0
- package/dist/packages/runtime/dist/approval-providers.js +269 -0
- package/dist/packages/runtime/dist/approval-providers.js.map +1 -0
- package/dist/packages/runtime/dist/daily-limit.d.ts +2 -0
- package/dist/packages/runtime/dist/daily-limit.js +54 -0
- package/dist/packages/runtime/dist/daily-limit.js.map +1 -0
- package/dist/packages/runtime/dist/gateway.d.ts +35 -0
- package/dist/packages/runtime/dist/gateway.js +92 -0
- package/dist/packages/runtime/dist/gateway.js.map +1 -0
- package/dist/packages/runtime/dist/inbox.d.ts +10 -0
- package/dist/packages/runtime/dist/inbox.js +24 -0
- package/dist/packages/runtime/dist/inbox.js.map +1 -0
- package/dist/packages/runtime/dist/index.d.ts +13 -0
- package/dist/packages/runtime/dist/index.js +14 -0
- package/dist/packages/runtime/dist/index.js.map +1 -0
- package/dist/packages/runtime/dist/mcp-server.d.ts +90 -0
- package/dist/packages/runtime/dist/mcp-server.js +647 -0
- package/dist/packages/runtime/dist/mcp-server.js.map +1 -0
- package/dist/packages/runtime/dist/mcp-tool-catalog.d.ts +3 -0
- package/dist/packages/runtime/dist/mcp-tool-catalog.js +507 -0
- package/dist/packages/runtime/dist/mcp-tool-catalog.js.map +1 -0
- package/dist/packages/runtime/dist/mcp.d.ts +73 -0
- package/dist/packages/runtime/dist/mcp.js +67 -0
- package/dist/packages/runtime/dist/mcp.js.map +1 -0
- package/dist/packages/runtime/dist/policy.d.ts +19 -0
- package/dist/packages/runtime/dist/policy.js +41 -0
- package/dist/packages/runtime/dist/policy.js.map +1 -0
- package/dist/packages/runtime/dist/quota.d.ts +39 -0
- package/dist/packages/runtime/dist/quota.js +147 -0
- package/dist/packages/runtime/dist/quota.js.map +1 -0
- package/dist/packages/runtime/dist/session-audit.d.ts +107 -0
- package/dist/packages/runtime/dist/session-audit.js +520 -0
- package/dist/packages/runtime/dist/session-audit.js.map +1 -0
- package/dist/packages/runtime/dist/session-timeouts.d.ts +33 -0
- package/dist/packages/runtime/dist/session-timeouts.js +62 -0
- package/dist/packages/runtime/dist/session-timeouts.js.map +1 -0
- package/dist/packages/runtime/dist/session.d.ts +323 -0
- package/dist/packages/runtime/dist/session.js +2252 -0
- package/dist/packages/runtime/dist/session.js.map +1 -0
- package/dist/packages/runtime/dist/task-journal.d.ts +36 -0
- package/dist/packages/runtime/dist/task-journal.js +104 -0
- package/dist/packages/runtime/dist/task-journal.js.map +1 -0
- package/dist/packages/runtime/dist/verifier.d.ts +64 -0
- package/dist/packages/runtime/dist/verifier.js +109 -0
- package/dist/packages/runtime/dist/verifier.js.map +1 -0
- package/dist/packages/sandbox/dist/container.d.ts +33 -0
- package/dist/packages/sandbox/dist/container.js +189 -0
- package/dist/packages/sandbox/dist/container.js.map +1 -0
- package/dist/packages/sandbox/dist/index.d.ts +34 -0
- package/dist/packages/sandbox/dist/index.js +179 -0
- package/dist/packages/sandbox/dist/index.js.map +1 -0
- package/dist/packages/sandbox/dist/permission.d.ts +58 -0
- package/dist/packages/sandbox/dist/permission.js +90 -0
- package/dist/packages/sandbox/dist/permission.js.map +1 -0
- package/dist/spec/README.ja.md +76 -0
- package/dist/spec/README.md +135 -0
- package/dist/spec/ack-receipt.schema.json +77 -0
- package/dist/spec/agent-card.schema.json +83 -0
- package/dist/spec/approval-receipt.schema.json +94 -0
- package/dist/spec/audit-entry.schema.json +101 -0
- package/dist/spec/capability-token.schema.json +133 -0
- package/dist/spec/envelope.schema.json +197 -0
- package/dist/spec/examples/sample-ack-receipt.json +16 -0
- package/dist/spec/examples/sample-agent-card.json +35 -0
- package/dist/spec/examples/sample-approval-receipt.json +18 -0
- package/dist/spec/examples/sample-audit-entry.json +37 -0
- package/dist/spec/examples/sample-capability-token.json +39 -0
- package/dist/spec/examples/sample-envelope.json +21 -0
- package/dist/spec/examples/sample-task-manifest.json +141 -0
- package/dist/spec/examples/sample-usage-receipt.json +19 -0
- package/dist/spec/task-manifest.schema.json +422 -0
- package/dist/spec/usage-receipt.schema.json +91 -0
- package/package.json +61 -5
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
|
+
import { verifyEnvelope, verifySigned, resolveDid, createRelayAuthToken, normalizeRelayOrigin, validateRelayEndpointUrl, } from '../../core/dist/index.js';
|
|
5
|
+
import { DEFAULT_MAX_ENVELOPE_BYTES, InMemoryRelay, createRelayFetchHandler, mapRelayError, normalizeReceiveFilter, relayHttpHeaders, sanitizeRelayErrorCode, } from './fetch.js';
|
|
6
|
+
import { createPinnedRelayFetch, resolveAndValidateRelayEndpoint } from './ssrf.js';
|
|
7
|
+
export * from './fetch.js';
|
|
8
|
+
export * from './retry.js';
|
|
9
|
+
import { isTransientRelayError, relayRetryDelay, waitForRelayRetry, relayDeadline, relayDiagnostic } from './retry.js';
|
|
10
|
+
export { createPinnedRelayFetch, resolveAndValidateRelayEndpoint } from './ssrf.js';
|
|
11
|
+
export { createSignedAgentCard, verifySignedAgentCard, findDiscoveredAgentCard, toA2AAgentCard, createInviteToken, parseAndVerifyInviteToken, } from '../../core/dist/index.js';
|
|
12
|
+
export async function startLocalRelay(options = {}) {
|
|
13
|
+
const relay = new InMemoryRelay({
|
|
14
|
+
...options.relayOptions,
|
|
15
|
+
tamper: options.tamper,
|
|
16
|
+
allowUnboundRelayAuth: options.allowUnboundRelayAuth,
|
|
17
|
+
});
|
|
18
|
+
const handler = createRelayFetchHandler({
|
|
19
|
+
relay,
|
|
20
|
+
allowQueryAuth: options.allowQueryAuth,
|
|
21
|
+
allowUnboundRelayAuth: options.allowUnboundRelayAuth,
|
|
22
|
+
});
|
|
23
|
+
let boundOrigin = 'http://127.0.0.1';
|
|
24
|
+
const server = createServer(async (request, response) => {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
response.once('close', () => controller.abort());
|
|
27
|
+
try {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
let size = 0;
|
|
30
|
+
for await (const chunk of request) {
|
|
31
|
+
size += chunk.length;
|
|
32
|
+
if (size > DEFAULT_MAX_ENVELOPE_BYTES)
|
|
33
|
+
throw new Error('BODY_TOO_LARGE');
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
}
|
|
36
|
+
const result = await handler(new Request(new URL(request.url ?? '/', boundOrigin), {
|
|
37
|
+
method: request.method,
|
|
38
|
+
headers: Object.entries(request.headers).flatMap(([name, value]) => value === undefined
|
|
39
|
+
? []
|
|
40
|
+
: (Array.isArray(value) ? value : [value]).map((v) => [name, v])),
|
|
41
|
+
...(chunks.length ? { body: Buffer.concat(chunks) } : {}),
|
|
42
|
+
signal: controller.signal,
|
|
43
|
+
}));
|
|
44
|
+
const kind = result.headers.get('content-type')?.startsWith('text/event-stream')
|
|
45
|
+
? 'sse'
|
|
46
|
+
: result.body == null
|
|
47
|
+
? 'empty'
|
|
48
|
+
: 'json';
|
|
49
|
+
response.writeHead(result.status, {
|
|
50
|
+
...Object.fromEntries(result.headers),
|
|
51
|
+
...relayHttpHeaders(kind),
|
|
52
|
+
});
|
|
53
|
+
if (!result.body) {
|
|
54
|
+
response.end();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const reader = result.body.getReader();
|
|
58
|
+
try {
|
|
59
|
+
while (!response.destroyed && !response.writableEnded) {
|
|
60
|
+
const { value, done } = await reader.read();
|
|
61
|
+
if (done)
|
|
62
|
+
break;
|
|
63
|
+
if (!response.write(value)) {
|
|
64
|
+
// A disconnected client never emits drain. Close and error must unblock the write.
|
|
65
|
+
if (response.destroyed || response.writableEnded)
|
|
66
|
+
break;
|
|
67
|
+
await new Promise((resolve) => {
|
|
68
|
+
const doneWriting = () => {
|
|
69
|
+
response.off('drain', doneWriting);
|
|
70
|
+
response.off('close', doneWriting);
|
|
71
|
+
response.off('error', doneWriting);
|
|
72
|
+
resolve();
|
|
73
|
+
};
|
|
74
|
+
response.once('drain', doneWriting);
|
|
75
|
+
response.once('close', doneWriting);
|
|
76
|
+
response.once('error', doneWriting);
|
|
77
|
+
if (response.destroyed || response.writableEnded)
|
|
78
|
+
doneWriting();
|
|
79
|
+
});
|
|
80
|
+
if (response.destroyed || response.writableEnded)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await reader.cancel();
|
|
87
|
+
if (!response.destroyed && !response.writableEnded)
|
|
88
|
+
response.end();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
const mapped = mapRelayError(e);
|
|
93
|
+
if (!response.headersSent)
|
|
94
|
+
response.writeHead(mapped.status, relayHttpHeaders('json'));
|
|
95
|
+
if (!response.destroyed)
|
|
96
|
+
response.end(JSON.stringify({ error: mapped.error }));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.requestTimeout = 10000;
|
|
100
|
+
const wsServer = new WebSocketServer({ server, path: '/ws', maxPayload: 1048576 });
|
|
101
|
+
const wsAuth = () => ({
|
|
102
|
+
expectedOrigin: boundOrigin,
|
|
103
|
+
requireOrigin: options.allowUnboundRelayAuth !== true,
|
|
104
|
+
});
|
|
105
|
+
wsServer.on('connection', (socket) => {
|
|
106
|
+
let subscription = { disposed: true, stop: () => { } };
|
|
107
|
+
const disposeSubscription = () => {
|
|
108
|
+
subscription.disposed = true;
|
|
109
|
+
if (subscription.timer !== undefined) {
|
|
110
|
+
clearTimeout(subscription.timer);
|
|
111
|
+
subscription.timer = undefined;
|
|
112
|
+
}
|
|
113
|
+
subscription.stop();
|
|
114
|
+
subscription.stop = () => { };
|
|
115
|
+
};
|
|
116
|
+
socket.once('close', () => disposeSubscription());
|
|
117
|
+
socket.on('message', (raw) => {
|
|
118
|
+
try {
|
|
119
|
+
const request = JSON.parse(raw.toString());
|
|
120
|
+
if (request.method === 'send' && request.envelope) {
|
|
121
|
+
relay.send(request.envelope);
|
|
122
|
+
socket.send(JSON.stringify({ ok: true }));
|
|
123
|
+
}
|
|
124
|
+
else if (request.method === 'receive' && request.recipientDid) {
|
|
125
|
+
const receive = relay.openInbox(request.recipientDid, request.authToken ?? request.auth, 'receive', wsAuth(), { role: request.role, taskId: request.taskId, peek: request.peek });
|
|
126
|
+
socket.send(JSON.stringify({ envelope: receive() ?? null }));
|
|
127
|
+
}
|
|
128
|
+
else if (request.method === 'subscribe' && request.recipientDid) {
|
|
129
|
+
const did = request.recipientDid, receive = relay.openInbox(did, request.authToken ?? request.auth, 'subscribe', wsAuth(), { role: request.role, taskId: request.taskId });
|
|
130
|
+
disposeSubscription();
|
|
131
|
+
const current = { disposed: false, stop: () => { } };
|
|
132
|
+
subscription = current;
|
|
133
|
+
const flush = () => {
|
|
134
|
+
if (current.disposed)
|
|
135
|
+
return;
|
|
136
|
+
let envelope;
|
|
137
|
+
// A slow socket must not pull the recipient queue ahead of maxQueueSize.
|
|
138
|
+
while (!current.disposed &&
|
|
139
|
+
socket.readyState === WebSocket.OPEN &&
|
|
140
|
+
socket.bufferedAmount < 1048576 &&
|
|
141
|
+
(envelope = receive()))
|
|
142
|
+
socket.send(JSON.stringify({ envelope }));
|
|
143
|
+
if (!current.disposed &&
|
|
144
|
+
socket.readyState === WebSocket.OPEN &&
|
|
145
|
+
socket.bufferedAmount >= 1048576) {
|
|
146
|
+
if (current.timer !== undefined)
|
|
147
|
+
clearTimeout(current.timer);
|
|
148
|
+
current.timer = setTimeout(() => {
|
|
149
|
+
current.timer = undefined;
|
|
150
|
+
flush();
|
|
151
|
+
}, 20);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
current.stop = relay.subscribe(did, flush);
|
|
155
|
+
flush();
|
|
156
|
+
}
|
|
157
|
+
else if (request.method === 'unsubscribe') {
|
|
158
|
+
disposeSubscription();
|
|
159
|
+
}
|
|
160
|
+
else
|
|
161
|
+
throw new Error('INVALID_REQUEST');
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
socket.send(JSON.stringify({ ok: false, error: sanitizeRelayErrorCode(e) }));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
await new Promise((resolve, reject) => {
|
|
169
|
+
server.once('error', reject);
|
|
170
|
+
server.listen(options.port ?? 0, options.host ?? '127.0.0.1', () => {
|
|
171
|
+
const address = server.address();
|
|
172
|
+
if (!address || typeof address === 'string') {
|
|
173
|
+
reject(new Error('RELAY_BIND'));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const hostname = address.address.includes(':')
|
|
177
|
+
? '[' + address.address + ']'
|
|
178
|
+
: address.address;
|
|
179
|
+
boundOrigin = 'http://' + hostname + ':' + address.port;
|
|
180
|
+
resolve();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
const maintenance = setInterval(() => {
|
|
184
|
+
relay.evictExpired();
|
|
185
|
+
void relay.flushWakeNotifications();
|
|
186
|
+
}, 1000);
|
|
187
|
+
maintenance.unref();
|
|
188
|
+
return {
|
|
189
|
+
relay,
|
|
190
|
+
url: boundOrigin,
|
|
191
|
+
wsUrl: boundOrigin.replace(/^http:/, 'ws:') + '/ws',
|
|
192
|
+
close: async () => {
|
|
193
|
+
clearInterval(maintenance);
|
|
194
|
+
for (const client of wsServer.clients)
|
|
195
|
+
client.terminate();
|
|
196
|
+
await new Promise((resolve) => wsServer.close(() => resolve()));
|
|
197
|
+
server.closeAllConnections();
|
|
198
|
+
await new Promise((resolve, reject) => server.close((e) => (e ? reject(e) : resolve())));
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
class RelayCredentials {
|
|
203
|
+
authSource;
|
|
204
|
+
relayUrl;
|
|
205
|
+
recipientKeys = new Map();
|
|
206
|
+
constructor(authSource, relayUrl) {
|
|
207
|
+
this.authSource = authSource;
|
|
208
|
+
this.relayUrl = relayUrl;
|
|
209
|
+
}
|
|
210
|
+
setRecipientKey(key) {
|
|
211
|
+
this.recipientKeys.set(key.did, key);
|
|
212
|
+
return this;
|
|
213
|
+
}
|
|
214
|
+
async authToken(did, action) {
|
|
215
|
+
const origin = normalizeRelayOrigin(this.relayUrl);
|
|
216
|
+
const key = this.recipientKeys.get(did);
|
|
217
|
+
if (key)
|
|
218
|
+
return createRelayAuthToken(key, { action, origin });
|
|
219
|
+
if (typeof this.authSource === 'function')
|
|
220
|
+
return this.authSource(did, action, origin);
|
|
221
|
+
if (this.authSource?.did === did)
|
|
222
|
+
return createRelayAuthToken(this.authSource, { action, origin });
|
|
223
|
+
throw new Error('RELAY_AUTH_REQUIRED');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const nativeFetch = globalThis.fetch;
|
|
227
|
+
function isKeyPair(value) {
|
|
228
|
+
return (!!value &&
|
|
229
|
+
typeof value === 'object' &&
|
|
230
|
+
'did' in value &&
|
|
231
|
+
'privateKey' in value &&
|
|
232
|
+
typeof value.did === 'string' &&
|
|
233
|
+
typeof value.privateKey === 'string');
|
|
234
|
+
}
|
|
235
|
+
function splitRelayArgs(second, third, fourth) {
|
|
236
|
+
let fetcher = fetch;
|
|
237
|
+
let auth;
|
|
238
|
+
let security = {};
|
|
239
|
+
if (typeof second === 'function')
|
|
240
|
+
fetcher = second;
|
|
241
|
+
else if (isKeyPair(second))
|
|
242
|
+
auth = second;
|
|
243
|
+
else if (second)
|
|
244
|
+
security = { ...second };
|
|
245
|
+
if (typeof third === 'function' || isKeyPair(third))
|
|
246
|
+
auth = third;
|
|
247
|
+
else if (third)
|
|
248
|
+
security = { ...security, ...third };
|
|
249
|
+
if (fourth)
|
|
250
|
+
security = { ...security, ...fourth };
|
|
251
|
+
return { fetcher, auth, security };
|
|
252
|
+
}
|
|
253
|
+
export class HttpRelayClient extends RelayCredentials {
|
|
254
|
+
url;
|
|
255
|
+
statusPins = new Map();
|
|
256
|
+
buffered = new Map();
|
|
257
|
+
delivered = new Map();
|
|
258
|
+
baseFetcher;
|
|
259
|
+
security;
|
|
260
|
+
transportPromise;
|
|
261
|
+
fetcher;
|
|
262
|
+
constructor(url, fetcherOrKey = fetch, authOrOptions, options) {
|
|
263
|
+
const parsed = splitRelayArgs(fetcherOrKey, authOrOptions, options);
|
|
264
|
+
super(parsed.auth, url);
|
|
265
|
+
this.url = url;
|
|
266
|
+
this.baseFetcher = parsed.fetcher;
|
|
267
|
+
this.security = parsed.security;
|
|
268
|
+
this.fetcher = ((input, init) => this.dispatch(input, init));
|
|
269
|
+
}
|
|
270
|
+
async dispatch(input, init) {
|
|
271
|
+
const transport = await this.prepare();
|
|
272
|
+
return transport(input, init);
|
|
273
|
+
}
|
|
274
|
+
prepare() {
|
|
275
|
+
// Cache successful pins and security failures; transient DNS failures can be retried.
|
|
276
|
+
this.transportPromise ??= this.buildTransport().catch((error) => {
|
|
277
|
+
if (isTransientRelayError(error))
|
|
278
|
+
this.transportPromise = undefined;
|
|
279
|
+
throw error;
|
|
280
|
+
});
|
|
281
|
+
return this.transportPromise;
|
|
282
|
+
}
|
|
283
|
+
async buildTransport() {
|
|
284
|
+
const custom = this.baseFetcher !== nativeFetch;
|
|
285
|
+
if (custom && !this.security.lookup) {
|
|
286
|
+
validateRelayEndpointUrl(this.url, { allowPrivateRelay: this.security.allowPrivateRelay });
|
|
287
|
+
return this.baseFetcher;
|
|
288
|
+
}
|
|
289
|
+
const resolved = await resolveAndValidateRelayEndpoint(this.url, this.security);
|
|
290
|
+
if (custom)
|
|
291
|
+
return this.baseFetcher;
|
|
292
|
+
return createPinnedRelayFetch(resolved, this.security);
|
|
293
|
+
}
|
|
294
|
+
async send(envelope) {
|
|
295
|
+
const result = await this.fetcher(this.url + '/envelopes', {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
body: JSON.stringify(envelope),
|
|
298
|
+
headers: { 'content-type': 'application/json' },
|
|
299
|
+
});
|
|
300
|
+
if (!result.ok) {
|
|
301
|
+
await result.body?.cancel();
|
|
302
|
+
throw new Error('RELAY_REJECTED: ' + result.status);
|
|
303
|
+
}
|
|
304
|
+
const relayDid = result.headers.get('x-acp-relay-did');
|
|
305
|
+
if (relayDid) {
|
|
306
|
+
if (this.statusPins.size >= 16384)
|
|
307
|
+
this.statusPins.delete(this.statusPins.keys().next().value);
|
|
308
|
+
this.statusPins.set(envelope.payload.envelopeId, relayDid);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Register an operator-configured channel; null unregisters it. Startup still drains the normal inbox. */
|
|
312
|
+
async registerWake(recipientDid, channel) {
|
|
313
|
+
for (let attempt = 0;; attempt++) {
|
|
314
|
+
const timeout = this.security.requestTimeoutMs ?? 45_000;
|
|
315
|
+
try {
|
|
316
|
+
return await relayDeadline((async () => {
|
|
317
|
+
const r = await this.fetcher(this.url + '/wake?recipient=' + encodeURIComponent(recipientDid), {
|
|
318
|
+
method: 'POST', body: JSON.stringify({ channel }), signal: AbortSignal.timeout(timeout),
|
|
319
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + await this.authToken(recipientDid, 'receive') },
|
|
320
|
+
});
|
|
321
|
+
if (!r.ok) {
|
|
322
|
+
await r.body?.cancel();
|
|
323
|
+
throw new Error('RELAY_REGISTRATION_REJECTED: ' + r.status);
|
|
324
|
+
}
|
|
325
|
+
const value = await r.json();
|
|
326
|
+
resolveDid(value.relayDid);
|
|
327
|
+
return value.relayDid;
|
|
328
|
+
})(), timeout);
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (attempt >= 4 || !isTransientRelayError(error))
|
|
332
|
+
throw error;
|
|
333
|
+
console.warn('Relay wake registration retry ' + (attempt + 1));
|
|
334
|
+
await waitForRelayRetry(relayRetryDelay(attempt));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async deliveryStatus(envelope, signal) {
|
|
339
|
+
const p = envelope.payload;
|
|
340
|
+
const relayDid = this.statusPins.get(p.envelopeId);
|
|
341
|
+
// Old relays do not advertise signed delivery status support.
|
|
342
|
+
if (!relayDid)
|
|
343
|
+
return;
|
|
344
|
+
const timeout = this.security.requestTimeoutMs ?? 45_000;
|
|
345
|
+
const params = new URLSearchParams({ recipient: p.recipientDid, sender: p.senderDid, envelopeId: p.envelopeId });
|
|
346
|
+
return relayDeadline((async () => {
|
|
347
|
+
const r = await this.fetcher(this.url + '/delivery-status?' + params, {
|
|
348
|
+
signal: AbortSignal.any([AbortSignal.timeout(timeout), ...(signal ? [signal] : [])]),
|
|
349
|
+
headers: { authorization: 'Bearer ' + await this.authToken(p.senderDid, 'receive') },
|
|
350
|
+
});
|
|
351
|
+
if (!r.ok) {
|
|
352
|
+
await r.body?.cancel();
|
|
353
|
+
throw new Error('RELAY_REJECTED: ' + r.status);
|
|
354
|
+
}
|
|
355
|
+
const notice = await r.json();
|
|
356
|
+
const value = notice.payload;
|
|
357
|
+
if (!verifySigned(notice, relayDid) || value.type !== 'RelayDeliveryStatus' ||
|
|
358
|
+
value.envelopeId !== p.envelopeId || value.senderDid !== p.senderDid ||
|
|
359
|
+
value.recipientDid !== p.recipientDid || !['pending', 'expired', 'unknown'].includes(value.state) ||
|
|
360
|
+
(value.state !== 'unknown' && value.taskId !== p.taskId) ||
|
|
361
|
+
value.relay !== new URL(this.url).origin ||
|
|
362
|
+
(value.state !== 'unknown' && !Number.isSafeInteger(value.expiresAt)) ||
|
|
363
|
+
(value.state === 'pending' && Math.abs(Date.now() - Date.parse(notice.signature.signedAt)) > 300_000))
|
|
364
|
+
throw new Error('RELAY_TAMPERING_DETECTED');
|
|
365
|
+
if (value.state !== 'pending')
|
|
366
|
+
this.statusPins.delete(p.envelopeId);
|
|
367
|
+
return notice;
|
|
368
|
+
})(), timeout, signal);
|
|
369
|
+
}
|
|
370
|
+
async register(card) {
|
|
371
|
+
for (let attempt = 0;; attempt++) {
|
|
372
|
+
try {
|
|
373
|
+
const timeout = this.security.requestTimeoutMs ?? 45_000;
|
|
374
|
+
await relayDeadline((async () => {
|
|
375
|
+
const r = await this.fetcher(this.url + '/agents', {
|
|
376
|
+
method: 'POST', body: JSON.stringify(card),
|
|
377
|
+
headers: { 'content-type': 'application/json' },
|
|
378
|
+
signal: AbortSignal.timeout(timeout),
|
|
379
|
+
});
|
|
380
|
+
if (!r.ok) {
|
|
381
|
+
await r.body?.cancel();
|
|
382
|
+
throw new Error('RELAY_REGISTRATION_REJECTED: ' + r.status);
|
|
383
|
+
}
|
|
384
|
+
await r.arrayBuffer();
|
|
385
|
+
})(), timeout);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
if (attempt >= 4 || !isTransientRelayError(error))
|
|
390
|
+
throw error;
|
|
391
|
+
console.warn('Relay registration retry ' + (attempt + 1));
|
|
392
|
+
await waitForRelayRetry(relayRetryDelay(attempt));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async discover() {
|
|
397
|
+
const r = await this.fetcher(this.url + '/agents');
|
|
398
|
+
if (!r.ok)
|
|
399
|
+
throw new Error('DISCOVERY_FAILED');
|
|
400
|
+
const cards = (await r.json());
|
|
401
|
+
if (!Array.isArray(cards))
|
|
402
|
+
throw new Error('INVALID_AGENT_CARD');
|
|
403
|
+
const { verifySignedAgentCard } = await import('../../core/dist/index.js');
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
return cards.filter((c) => verifySignedAgentCard(c, now));
|
|
406
|
+
}
|
|
407
|
+
async receive(recipientDid, waitMs, signal, filter) {
|
|
408
|
+
const normalized = normalizeReceiveFilter(filter);
|
|
409
|
+
const bufferKey = recipientDid +
|
|
410
|
+
'\0' +
|
|
411
|
+
(normalized.lease ? 'lease:' : '') + (normalized.role ?? '') +
|
|
412
|
+
'\0' +
|
|
413
|
+
(normalized.taskIds ? [...normalized.taskIds].sort().join(',') : '');
|
|
414
|
+
if (!normalized.peek) {
|
|
415
|
+
const queue = this.buffered.get(bufferKey) ?? [];
|
|
416
|
+
if (queue.length)
|
|
417
|
+
return queue.shift();
|
|
418
|
+
}
|
|
419
|
+
const params = new URLSearchParams({ recipient: recipientDid });
|
|
420
|
+
if (waitMs !== undefined)
|
|
421
|
+
params.set('waitMs', String(waitMs));
|
|
422
|
+
if (normalized.role)
|
|
423
|
+
params.set('role', normalized.role);
|
|
424
|
+
if (normalized.taskIds) {
|
|
425
|
+
for (const id of normalized.taskIds)
|
|
426
|
+
params.append('taskId', id);
|
|
427
|
+
}
|
|
428
|
+
if (normalized.peek)
|
|
429
|
+
params.set('peek', 'true');
|
|
430
|
+
else if (normalized.lease)
|
|
431
|
+
params.set('lease', 'true');
|
|
432
|
+
const r = await this.fetcher(this.url + '/envelopes?' + params.toString(), {
|
|
433
|
+
signal,
|
|
434
|
+
headers: { authorization: 'Bearer ' + (await this.authToken(recipientDid, 'receive')) },
|
|
435
|
+
});
|
|
436
|
+
if (r.status === 204)
|
|
437
|
+
return;
|
|
438
|
+
if (!r.ok) {
|
|
439
|
+
await r.body?.cancel();
|
|
440
|
+
throw new Error('RELAY_REJECTED: ' + r.status);
|
|
441
|
+
}
|
|
442
|
+
const body = (await r.json());
|
|
443
|
+
let values = 'envelopes' in body ? body.envelopes : [body];
|
|
444
|
+
if (!Array.isArray(values) || values.some((e) => !verifyEnvelope(e, recipientDid)))
|
|
445
|
+
throw new Error('RELAY_TAMPERING_DETECTED');
|
|
446
|
+
if (normalized.peek)
|
|
447
|
+
return values[0];
|
|
448
|
+
if (normalized.lease) {
|
|
449
|
+
const fresh = [];
|
|
450
|
+
for (const envelope of values)
|
|
451
|
+
if (await this.rememberDelivery(recipientDid, envelope))
|
|
452
|
+
fresh.push(envelope);
|
|
453
|
+
values = fresh;
|
|
454
|
+
}
|
|
455
|
+
this.buffered.set(bufferKey, values);
|
|
456
|
+
return values.shift();
|
|
457
|
+
}
|
|
458
|
+
async rememberDelivery(recipientDid, envelope) {
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
for (const [id, entry] of this.delivered)
|
|
461
|
+
if (entry.expires <= now)
|
|
462
|
+
this.delivered.delete(id);
|
|
463
|
+
const id = recipientDid + ':' + envelope.payload.envelopeId;
|
|
464
|
+
const prior = this.delivered.get(id);
|
|
465
|
+
if (prior) {
|
|
466
|
+
if (prior.acknowledged) {
|
|
467
|
+
try {
|
|
468
|
+
await this.acknowledgeDelivery(recipientDid, envelope.payload.envelopeId);
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
if (!isTransientRelayError(error))
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
if (this.delivered.size >= 16384)
|
|
478
|
+
throw new Error('RELAY_DEDUPE_CAPACITY');
|
|
479
|
+
this.delivered.set(id, { expires: now + 10 * 60_000, acknowledged: false });
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
async acknowledgeDelivery(recipientDid, envelopeId) {
|
|
483
|
+
const entry = this.delivered.get(recipientDid + ':' + envelopeId);
|
|
484
|
+
if (!entry)
|
|
485
|
+
return;
|
|
486
|
+
// Processing has completed even if the DELETE response is lost. Redelivery retries the ack.
|
|
487
|
+
entry.acknowledged = true;
|
|
488
|
+
const r = await this.fetcher(this.url + '/envelopes/' + encodeURIComponent(envelopeId) +
|
|
489
|
+
'?recipient=' + encodeURIComponent(recipientDid), {
|
|
490
|
+
method: 'DELETE',
|
|
491
|
+
headers: { authorization: 'Bearer ' + await this.authToken(recipientDid, 'receive') },
|
|
492
|
+
});
|
|
493
|
+
await r.arrayBuffer();
|
|
494
|
+
if (!r.ok && r.status !== 404)
|
|
495
|
+
throw new Error('RELAY_REJECTED: ' + r.status);
|
|
496
|
+
}
|
|
497
|
+
async peek(recipientDid, filter) {
|
|
498
|
+
return this.receive(recipientDid, undefined, undefined, { ...filter, peek: true });
|
|
499
|
+
}
|
|
500
|
+
async pollInbox(recipientDid, filter) {
|
|
501
|
+
return this.receive(recipientDid, filter?.waitMs, filter?.signal, filter);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/** Persistent SSE transport. Leased frames remain on the relay until explicitly acknowledged. */
|
|
505
|
+
export class SseRelayClient extends HttpRelayClient {
|
|
506
|
+
connectionStatus = 'stopped';
|
|
507
|
+
maxBufferBytes = 2 * 1024 * 1024;
|
|
508
|
+
streams = new Map();
|
|
509
|
+
async receive(recipientDid, waitMs = 30000, signal, filter) {
|
|
510
|
+
signal?.throwIfAborted();
|
|
511
|
+
if (filter?.peek)
|
|
512
|
+
return super.receive(recipientDid, waitMs, signal, filter);
|
|
513
|
+
const normalized = normalizeReceiveFilter(filter);
|
|
514
|
+
const key = recipientDid + '\0' + (normalized.role ?? '') + '\0' +
|
|
515
|
+
(normalized.taskIds ? [...normalized.taskIds].sort().join(',') : '');
|
|
516
|
+
let state = this.streams.get(key);
|
|
517
|
+
if (!state) {
|
|
518
|
+
state = { controller: new AbortController(), queue: [], notify: new Set() };
|
|
519
|
+
this.streams.set(key, state);
|
|
520
|
+
}
|
|
521
|
+
const current = state;
|
|
522
|
+
if (!current.pump && !current.error) {
|
|
523
|
+
current.controller = new AbortController();
|
|
524
|
+
current.pump = (async () => {
|
|
525
|
+
let attempt = 0;
|
|
526
|
+
while (!current.controller.signal.aborted) {
|
|
527
|
+
const started = Date.now();
|
|
528
|
+
try {
|
|
529
|
+
for await (const e of this.stream(recipientDid, current.controller.signal, filter)) {
|
|
530
|
+
if (current.queue.length >= 1000)
|
|
531
|
+
throw new Error('SSE_CAPACITY');
|
|
532
|
+
current.queue.push(e);
|
|
533
|
+
attempt = 0;
|
|
534
|
+
for (const notify of [...current.notify])
|
|
535
|
+
notify();
|
|
536
|
+
}
|
|
537
|
+
throw new Error('SSE_CLOSED');
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
if (current.controller.signal.aborted)
|
|
541
|
+
break;
|
|
542
|
+
if (!isTransientRelayError(error)) {
|
|
543
|
+
current.error = relayDiagnostic(error);
|
|
544
|
+
this.connectionStatus = 'unavailable';
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
this.connectionStatus = 'reconnecting';
|
|
548
|
+
if (Date.now() - started > 35_000)
|
|
549
|
+
attempt = 0;
|
|
550
|
+
try {
|
|
551
|
+
await waitForRelayRetry(relayRetryDelay(attempt++), current.controller.signal);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const notify of [...current.notify])
|
|
559
|
+
notify();
|
|
560
|
+
})().finally(() => { current.pump = undefined; });
|
|
561
|
+
}
|
|
562
|
+
const abort = () => current.controller.abort(signal?.reason);
|
|
563
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
564
|
+
try {
|
|
565
|
+
if (current.error)
|
|
566
|
+
throw current.error;
|
|
567
|
+
if (current.queue.length)
|
|
568
|
+
return current.queue.shift();
|
|
569
|
+
await new Promise((resolve, reject) => {
|
|
570
|
+
const cleanup = () => {
|
|
571
|
+
clearTimeout(timer);
|
|
572
|
+
current.notify.delete(wake);
|
|
573
|
+
signal?.removeEventListener('abort', stop);
|
|
574
|
+
};
|
|
575
|
+
const wake = () => { cleanup(); resolve(); };
|
|
576
|
+
const stop = () => { cleanup(); reject(signal?.reason ?? new Error('SSE_ABORTED')); };
|
|
577
|
+
const timer = setTimeout(wake, Math.max(0, Math.min(waitMs, 30000)));
|
|
578
|
+
current.notify.add(wake);
|
|
579
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
580
|
+
if (signal?.aborted)
|
|
581
|
+
stop();
|
|
582
|
+
});
|
|
583
|
+
if (current.error)
|
|
584
|
+
throw current.error;
|
|
585
|
+
return current.queue.shift();
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
signal?.removeEventListener('abort', abort);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async close() {
|
|
592
|
+
this.connectionStatus = 'stopped';
|
|
593
|
+
const states = [...this.streams.values()];
|
|
594
|
+
this.streams.clear();
|
|
595
|
+
for (const state of states) {
|
|
596
|
+
state.error = new Error('SSE_ABORTED');
|
|
597
|
+
state.controller.abort(state.error);
|
|
598
|
+
for (const notify of [...state.notify])
|
|
599
|
+
notify();
|
|
600
|
+
}
|
|
601
|
+
await Promise.all(states.map((s) => s.pump));
|
|
602
|
+
}
|
|
603
|
+
async *stream(recipientDid, signal, filter) {
|
|
604
|
+
const params = new URLSearchParams({ recipient: recipientDid, lease: 'true' });
|
|
605
|
+
const normalized = normalizeReceiveFilter(filter);
|
|
606
|
+
if (normalized.role)
|
|
607
|
+
params.set('role', normalized.role);
|
|
608
|
+
for (const id of normalized.taskIds ?? [])
|
|
609
|
+
params.append('taskId', id);
|
|
610
|
+
const controller = new AbortController();
|
|
611
|
+
const stopped = () => controller.abort(signal?.reason);
|
|
612
|
+
signal?.addEventListener('abort', stopped, { once: true });
|
|
613
|
+
if (signal?.aborted)
|
|
614
|
+
stopped();
|
|
615
|
+
const connectTimeout = this.security.requestTimeoutMs ?? 45_000;
|
|
616
|
+
let timer = setTimeout(() => controller.abort(new Error('RELAY_IDLE_TIMEOUT')), connectTimeout);
|
|
617
|
+
let reader;
|
|
618
|
+
try {
|
|
619
|
+
// A new signed credential is created for every connection, including reconnects.
|
|
620
|
+
const r = await relayDeadline(this.fetcher(this.url + '/stream?' + params, {
|
|
621
|
+
signal: controller.signal,
|
|
622
|
+
headers: { authorization: 'Bearer ' + (await this.authToken(recipientDid, 'stream')) },
|
|
623
|
+
}), connectTimeout, controller.signal);
|
|
624
|
+
clearTimeout(timer);
|
|
625
|
+
if (!r.ok) {
|
|
626
|
+
await r.body?.cancel();
|
|
627
|
+
throw new Error('RELAY_REJECTED: ' + r.status);
|
|
628
|
+
}
|
|
629
|
+
if (!r.body)
|
|
630
|
+
throw new Error('SSE_CLOSED');
|
|
631
|
+
this.connectionStatus = 'connected';
|
|
632
|
+
reader = r.body.getReader();
|
|
633
|
+
const decoder = new TextDecoder();
|
|
634
|
+
let buffer = '';
|
|
635
|
+
while (true) {
|
|
636
|
+
timer = setTimeout(() => controller.abort(new Error('RELAY_IDLE_TIMEOUT')), this.security.socketTimeoutMs ?? 35_000);
|
|
637
|
+
const { value, done } = await relayDeadline(reader.read(), this.security.socketTimeoutMs ?? 35_000, controller.signal);
|
|
638
|
+
clearTimeout(timer);
|
|
639
|
+
if (done)
|
|
640
|
+
break;
|
|
641
|
+
buffer += decoder.decode(value, { stream: true });
|
|
642
|
+
buffer = buffer.replaceAll('\r\n', '\n');
|
|
643
|
+
if (Buffer.byteLength(buffer, 'utf8') > this.maxBufferBytes)
|
|
644
|
+
throw new Error('SSE_BUFFER_OVERFLOW');
|
|
645
|
+
let index;
|
|
646
|
+
while ((index = buffer.indexOf('\n\n')) >= 0) {
|
|
647
|
+
const frame = buffer.slice(0, index);
|
|
648
|
+
buffer = buffer.slice(index + 2);
|
|
649
|
+
const data = frame.split('\n').filter(line => line.startsWith('data:'))
|
|
650
|
+
.map(line => line.slice(5).replace(/^ /, '')).join('\n');
|
|
651
|
+
if (data) {
|
|
652
|
+
const e = JSON.parse(data);
|
|
653
|
+
if (!verifyEnvelope(e, recipientDid))
|
|
654
|
+
throw new Error('RELAY_TAMPERING_DETECTED');
|
|
655
|
+
if (!await this.rememberDelivery(recipientDid, e))
|
|
656
|
+
continue;
|
|
657
|
+
yield e;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
if (controller.signal.aborted)
|
|
664
|
+
throw controller.signal.reason;
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
finally {
|
|
668
|
+
clearTimeout(timer);
|
|
669
|
+
controller.abort();
|
|
670
|
+
signal?.removeEventListener('abort', stopped);
|
|
671
|
+
await reader?.cancel().catch(() => { });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function socketIsOpen(socket) {
|
|
676
|
+
return socket?.readyState === WebSocket.OPEN;
|
|
677
|
+
}
|
|
678
|
+
export class WebSocketRelayClient extends RelayCredentials {
|
|
679
|
+
url;
|
|
680
|
+
socket;
|
|
681
|
+
pending = false;
|
|
682
|
+
stopped = false;
|
|
683
|
+
opening;
|
|
684
|
+
security;
|
|
685
|
+
constructor(url, authOrOptions, options) {
|
|
686
|
+
const parsed = splitRelayArgs(undefined, authOrOptions, options);
|
|
687
|
+
super(parsed.auth, url);
|
|
688
|
+
this.url = url;
|
|
689
|
+
this.security = parsed.security;
|
|
690
|
+
}
|
|
691
|
+
requireSocket() {
|
|
692
|
+
if (!this.socket)
|
|
693
|
+
throw new Error('RELAY_CLOSED');
|
|
694
|
+
return this.socket;
|
|
695
|
+
}
|
|
696
|
+
async open() {
|
|
697
|
+
const resolved = await resolveAndValidateRelayEndpoint(this.url, this.security);
|
|
698
|
+
if (this.stopped)
|
|
699
|
+
return;
|
|
700
|
+
const hostname = resolved.url.hostname.replace(/^\[|\]$/g, '');
|
|
701
|
+
const secure = resolved.url.protocol === 'wss:';
|
|
702
|
+
const wsOptions = {
|
|
703
|
+
lookup: resolved.lookup,
|
|
704
|
+
...(secure ? { servername: hostname } : {}),
|
|
705
|
+
};
|
|
706
|
+
const socket = new WebSocket(this.url, wsOptions);
|
|
707
|
+
this.socket = socket;
|
|
708
|
+
if (this.stopped) {
|
|
709
|
+
socket.close();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (socket.readyState === WebSocket.OPEN)
|
|
713
|
+
return;
|
|
714
|
+
await new Promise((resolve, reject) => {
|
|
715
|
+
const fail = (error) => reject(error);
|
|
716
|
+
socket.once('open', () => {
|
|
717
|
+
socket.off('error', fail);
|
|
718
|
+
resolve();
|
|
719
|
+
});
|
|
720
|
+
socket.once('error', fail);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
async ready() {
|
|
724
|
+
if (this.stopped)
|
|
725
|
+
throw new Error('RELAY_CLOSED');
|
|
726
|
+
// Read readyState through a helper so the early check does not exclude OPEN later.
|
|
727
|
+
if (!socketIsOpen(this.socket)) {
|
|
728
|
+
this.opening ??= this.open();
|
|
729
|
+
await this.opening;
|
|
730
|
+
}
|
|
731
|
+
if (this.stopped || !socketIsOpen(this.socket))
|
|
732
|
+
throw new Error('RELAY_CLOSED');
|
|
733
|
+
}
|
|
734
|
+
async request(body) {
|
|
735
|
+
await this.ready();
|
|
736
|
+
if (this.pending)
|
|
737
|
+
throw new Error('CONCURRENT_RELAY_REQUEST');
|
|
738
|
+
this.pending = true;
|
|
739
|
+
try {
|
|
740
|
+
const socket = this.requireSocket();
|
|
741
|
+
return await new Promise((resolve, reject) => {
|
|
742
|
+
const timer = setTimeout(() => {
|
|
743
|
+
socket.removeListener('message', receive);
|
|
744
|
+
reject(new Error('RELAY_TIMEOUT'));
|
|
745
|
+
}, 5000);
|
|
746
|
+
const receive = (raw) => {
|
|
747
|
+
clearTimeout(timer);
|
|
748
|
+
try {
|
|
749
|
+
const r = JSON.parse(raw.toString());
|
|
750
|
+
r.error ? reject(new Error(r.error)) : resolve(r);
|
|
751
|
+
}
|
|
752
|
+
catch (e) {
|
|
753
|
+
reject(e);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
socket.once('message', receive);
|
|
757
|
+
socket.send(JSON.stringify(body));
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
finally {
|
|
761
|
+
this.pending = false;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async send(envelope) {
|
|
765
|
+
await this.request({ method: 'send', envelope });
|
|
766
|
+
}
|
|
767
|
+
async receive(recipientDid, _waitMs, _signal, filter) {
|
|
768
|
+
const r = await this.request({
|
|
769
|
+
method: 'receive',
|
|
770
|
+
recipientDid,
|
|
771
|
+
authToken: await this.authToken(recipientDid, 'receive'),
|
|
772
|
+
...(filter?.role ? { role: filter.role } : {}),
|
|
773
|
+
...(filter?.taskId ? { taskId: filter.taskId } : {}),
|
|
774
|
+
...(filter?.peek ? { peek: true } : {}),
|
|
775
|
+
});
|
|
776
|
+
if (r.envelope && !verifyEnvelope(r.envelope, recipientDid))
|
|
777
|
+
throw new Error('RELAY_TAMPERING_DETECTED');
|
|
778
|
+
return r.envelope ?? undefined;
|
|
779
|
+
}
|
|
780
|
+
async *subscribe(recipientDid, signal) {
|
|
781
|
+
signal?.throwIfAborted();
|
|
782
|
+
await this.ready();
|
|
783
|
+
const authToken = await this.authToken(recipientDid, 'subscribe');
|
|
784
|
+
if (this.pending)
|
|
785
|
+
throw new Error('CONCURRENT_RELAY_REQUEST');
|
|
786
|
+
this.pending = true;
|
|
787
|
+
const queue = [];
|
|
788
|
+
let failure, wake = () => { };
|
|
789
|
+
const receive = (raw) => {
|
|
790
|
+
try {
|
|
791
|
+
const r = JSON.parse(raw.toString());
|
|
792
|
+
if (r.error)
|
|
793
|
+
throw new Error(r.error);
|
|
794
|
+
if (!r.envelope || !verifyEnvelope(r.envelope, recipientDid))
|
|
795
|
+
throw new Error('RELAY_TAMPERING_DETECTED');
|
|
796
|
+
if (queue.length >= 1000)
|
|
797
|
+
throw new Error('RELAY_CAPACITY');
|
|
798
|
+
queue.push(r.envelope);
|
|
799
|
+
}
|
|
800
|
+
catch (e) {
|
|
801
|
+
failure = e;
|
|
802
|
+
}
|
|
803
|
+
wake();
|
|
804
|
+
};
|
|
805
|
+
const close = () => {
|
|
806
|
+
failure = new Error('RELAY_CLOSED');
|
|
807
|
+
wake();
|
|
808
|
+
}, error = (e) => {
|
|
809
|
+
failure = e;
|
|
810
|
+
wake();
|
|
811
|
+
};
|
|
812
|
+
const abort = () => wake();
|
|
813
|
+
const socket = this.requireSocket();
|
|
814
|
+
socket.on('message', receive);
|
|
815
|
+
socket.once('close', close);
|
|
816
|
+
socket.once('error', error);
|
|
817
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
818
|
+
try {
|
|
819
|
+
socket.send(JSON.stringify({ method: 'subscribe', recipientDid, authToken }));
|
|
820
|
+
while (!signal?.aborted) {
|
|
821
|
+
if (failure)
|
|
822
|
+
throw failure;
|
|
823
|
+
if (queue.length) {
|
|
824
|
+
yield queue.shift();
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
await new Promise((resolve) => {
|
|
828
|
+
wake = resolve;
|
|
829
|
+
if (queue.length || failure || signal?.aborted)
|
|
830
|
+
resolve();
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
finally {
|
|
835
|
+
socket.off('message', receive);
|
|
836
|
+
socket.off('close', close);
|
|
837
|
+
socket.off('error', error);
|
|
838
|
+
signal?.removeEventListener('abort', abort);
|
|
839
|
+
if (socket.readyState === WebSocket.OPEN)
|
|
840
|
+
socket.send(JSON.stringify({ method: 'unsubscribe' }));
|
|
841
|
+
this.pending = false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
async close() {
|
|
845
|
+
this.stopped = true;
|
|
846
|
+
const socket = this.socket;
|
|
847
|
+
if (!socket || socket.readyState === WebSocket.CLOSED)
|
|
848
|
+
return;
|
|
849
|
+
await new Promise((resolve) => {
|
|
850
|
+
socket.once('close', resolve);
|
|
851
|
+
socket.close();
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
//# sourceMappingURL=index.js.map
|