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,1401 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { verifySigned, generateKeyPair, signPayload, resolveDid, verifySignedAgentCard, findDiscoveredAgentCard, toA2AAgentCard, verifyRelayAuthToken, createRelayAuthToken, } from '../../core/dist/edge.js';
|
|
3
|
+
export * from './notifications.js';
|
|
4
|
+
// Same outer wire shape as envelope.schema.json, without filesystem schema loads.
|
|
5
|
+
export function verifyRelayEnvelope(e, recipient) {
|
|
6
|
+
try {
|
|
7
|
+
const p = e.payload;
|
|
8
|
+
resolveDid(p.senderDid);
|
|
9
|
+
resolveDid(p.recipientDid);
|
|
10
|
+
return (Object.keys(e).sort().join() === 'payload,signature' &&
|
|
11
|
+
Object.keys(p).sort().join() ===
|
|
12
|
+
'envelopeId,eventType,payload,recipientDid,senderDid,seq,taskId,type' &&
|
|
13
|
+
Object.keys(e.signature).sort().join() ===
|
|
14
|
+
'canonicalization,payloadDigest,signature,signedAt,signerDid' &&
|
|
15
|
+
p.type === 'SignedEnvelope' &&
|
|
16
|
+
[p.envelopeId, p.taskId, p.eventType].every((s) => typeof s === 'string' && s.length > 0) &&
|
|
17
|
+
Number.isSafeInteger(p.seq) &&
|
|
18
|
+
p.seq >= 0 &&
|
|
19
|
+
(!recipient || p.recipientDid === recipient) &&
|
|
20
|
+
verifySigned(e, p.senderDid));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Single signed envelope JSON body. Larger bodies are rejected before they are queued. */
|
|
27
|
+
export const DEFAULT_MAX_ENVELOPE_BYTES = 256 * 1024;
|
|
28
|
+
/** Unread envelopes kept for one recipient. */
|
|
29
|
+
export const DEFAULT_MAX_QUEUE_SIZE = 256;
|
|
30
|
+
/** Sum of queued envelope JSON sizes for one recipient. */
|
|
31
|
+
export const DEFAULT_MAX_QUEUE_BYTES_PER_RECIPIENT = 4 * 1024 * 1024;
|
|
32
|
+
export const DEFAULT_ENVELOPE_TTL_MS = 10 * 60 * 1000;
|
|
33
|
+
export const DEFAULT_SEEN_TTL_MS = 60 * 60 * 1000;
|
|
34
|
+
export const DEFAULT_AUTH_SEEN_TTL_MS = 10 * 60 * 1000;
|
|
35
|
+
export const DEFAULT_AGENT_CARD_TTL_MS = 60 * 60 * 1000;
|
|
36
|
+
/** Caps the durable replay ring scanned when an inbox object wakes. */
|
|
37
|
+
export const DEFAULT_MAX_SEEN_ENTRIES = 4096;
|
|
38
|
+
export const DEFAULT_MAX_QUEUES = 10000;
|
|
39
|
+
export const DEFAULT_MAX_AGENTS = 10000;
|
|
40
|
+
const WORKER_PRIMARY_EVENTS = new Set([
|
|
41
|
+
'PROPOSE',
|
|
42
|
+
'REVOKE',
|
|
43
|
+
'VERIFY_ACCEPT',
|
|
44
|
+
'VERIFY_REJECT',
|
|
45
|
+
'SETTLE',
|
|
46
|
+
'SETTLED',
|
|
47
|
+
'CANCEL',
|
|
48
|
+
'CHECKPOINT',
|
|
49
|
+
]);
|
|
50
|
+
const REQUESTER_PRIMARY_EVENTS = new Set([
|
|
51
|
+
'ACCEPT',
|
|
52
|
+
'ATTEST',
|
|
53
|
+
'START',
|
|
54
|
+
'PROGRESS',
|
|
55
|
+
'DELIVER',
|
|
56
|
+
'HEARTBEAT',
|
|
57
|
+
'CHECKPOINT_ACK',
|
|
58
|
+
]);
|
|
59
|
+
const BILATERAL_AUDIT_EVENTS = new Set([
|
|
60
|
+
'PROPOSE',
|
|
61
|
+
'ACCEPT',
|
|
62
|
+
'START',
|
|
63
|
+
'PROGRESS',
|
|
64
|
+
'DELIVER',
|
|
65
|
+
'VERIFY_ACCEPT',
|
|
66
|
+
'VERIFY_REJECT',
|
|
67
|
+
'SETTLED',
|
|
68
|
+
'CANCEL',
|
|
69
|
+
]);
|
|
70
|
+
export function classifyEnvelopeRole(envelope, state) {
|
|
71
|
+
const { eventType: type, seq, senderDid, recipientDid } = envelope.payload;
|
|
72
|
+
const cross = senderDid !== recipientDid;
|
|
73
|
+
if (WORKER_PRIMARY_EVENTS.has(type)) {
|
|
74
|
+
if (state) {
|
|
75
|
+
if (cross) {
|
|
76
|
+
state.crossRole.set(recipientDid, 'worker');
|
|
77
|
+
state.crossRole.set(senderDid, 'requester');
|
|
78
|
+
}
|
|
79
|
+
state.lastPrimaryRole = 'worker';
|
|
80
|
+
if (BILATERAL_AUDIT_EVENTS.has(type))
|
|
81
|
+
state.pendingAckRoles.push('requester');
|
|
82
|
+
state.workerSeq = Math.max(state.workerSeq, seq + 1);
|
|
83
|
+
}
|
|
84
|
+
return 'worker';
|
|
85
|
+
}
|
|
86
|
+
if (REQUESTER_PRIMARY_EVENTS.has(type)) {
|
|
87
|
+
if (state) {
|
|
88
|
+
if (cross) {
|
|
89
|
+
state.crossRole.set(recipientDid, 'requester');
|
|
90
|
+
state.crossRole.set(senderDid, 'worker');
|
|
91
|
+
}
|
|
92
|
+
state.lastPrimaryRole = 'requester';
|
|
93
|
+
if (BILATERAL_AUDIT_EVENTS.has(type))
|
|
94
|
+
state.pendingAckRoles.push('worker');
|
|
95
|
+
state.requesterSeq = Math.max(state.requesterSeq, seq + 1);
|
|
96
|
+
}
|
|
97
|
+
return 'requester';
|
|
98
|
+
}
|
|
99
|
+
if (type === 'AUDIT_ENTRY') {
|
|
100
|
+
let role;
|
|
101
|
+
if (cross && state?.crossRole.has(recipientDid))
|
|
102
|
+
role = state.crossRole.get(recipientDid);
|
|
103
|
+
else if (state?.lastPrimaryRole)
|
|
104
|
+
role = state.lastPrimaryRole;
|
|
105
|
+
else
|
|
106
|
+
role = seq === 1 ? 'worker' : 'requester';
|
|
107
|
+
if (state) {
|
|
108
|
+
if (role === 'worker')
|
|
109
|
+
state.workerSeq = Math.max(state.workerSeq, seq + 1);
|
|
110
|
+
else
|
|
111
|
+
state.requesterSeq = Math.max(state.requesterSeq, seq + 1);
|
|
112
|
+
}
|
|
113
|
+
return role;
|
|
114
|
+
}
|
|
115
|
+
if (type === 'AUDIT_ACK') {
|
|
116
|
+
let role;
|
|
117
|
+
if (cross && state?.crossRole.has(recipientDid)) {
|
|
118
|
+
role = state.crossRole.get(recipientDid);
|
|
119
|
+
if (state.pendingAckRoles[0] === role)
|
|
120
|
+
state.pendingAckRoles.shift();
|
|
121
|
+
}
|
|
122
|
+
else if (state && state.pendingAckRoles.length > 0) {
|
|
123
|
+
role = state.pendingAckRoles.shift();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
role = seq === 0 ? 'requester' : 'worker';
|
|
127
|
+
if (state && cross) {
|
|
128
|
+
state.crossRole.set(recipientDid, role);
|
|
129
|
+
state.crossRole.set(senderDid, role === 'requester' ? 'worker' : 'requester');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (state) {
|
|
133
|
+
if (role === 'worker')
|
|
134
|
+
state.workerSeq = Math.max(state.workerSeq, seq + 1);
|
|
135
|
+
else
|
|
136
|
+
state.requesterSeq = Math.max(state.requesterSeq, seq + 1);
|
|
137
|
+
}
|
|
138
|
+
return role;
|
|
139
|
+
}
|
|
140
|
+
if (type === 'ERROR') {
|
|
141
|
+
if (cross && state?.crossRole.has(recipientDid))
|
|
142
|
+
return state.crossRole.get(recipientDid);
|
|
143
|
+
if (seq === 0) {
|
|
144
|
+
if (state && cross) {
|
|
145
|
+
state.crossRole.set(recipientDid, 'requester');
|
|
146
|
+
state.crossRole.set(senderDid, 'worker');
|
|
147
|
+
}
|
|
148
|
+
return 'requester';
|
|
149
|
+
}
|
|
150
|
+
if (state) {
|
|
151
|
+
if (seq === state.requesterSeq && seq !== state.workerSeq)
|
|
152
|
+
return 'requester';
|
|
153
|
+
if (seq === state.workerSeq && seq !== state.requesterSeq)
|
|
154
|
+
return 'worker';
|
|
155
|
+
if (state.pendingAckRoles.length > 0)
|
|
156
|
+
return state.pendingAckRoles.shift();
|
|
157
|
+
if (state.lastPrimaryRole)
|
|
158
|
+
return state.lastPrimaryRole === 'worker' ? 'requester' : 'worker';
|
|
159
|
+
}
|
|
160
|
+
return 'worker';
|
|
161
|
+
}
|
|
162
|
+
if (cross && state?.crossRole.has(recipientDid))
|
|
163
|
+
return state.crossRole.get(recipientDid);
|
|
164
|
+
return 'worker';
|
|
165
|
+
}
|
|
166
|
+
export function normalizeReceiveFilter(filter) {
|
|
167
|
+
if (!filter)
|
|
168
|
+
return { peek: false };
|
|
169
|
+
if (filter.role !== undefined && filter.role !== 'worker' && filter.role !== 'requester')
|
|
170
|
+
throw new Error('INVALID_REQUEST');
|
|
171
|
+
let taskIds;
|
|
172
|
+
if (filter.taskId !== undefined) {
|
|
173
|
+
const raw = Array.isArray(filter.taskId) ? filter.taskId : [filter.taskId];
|
|
174
|
+
const items = [];
|
|
175
|
+
for (const entry of raw) {
|
|
176
|
+
if (typeof entry !== 'string')
|
|
177
|
+
throw new Error('INVALID_REQUEST');
|
|
178
|
+
for (const part of entry.split(',')) {
|
|
179
|
+
const trimmed = part.trim();
|
|
180
|
+
if (trimmed)
|
|
181
|
+
items.push(trimmed);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (items.length > 0)
|
|
185
|
+
taskIds = new Set(items);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
...(taskIds ? { taskIds } : {}),
|
|
189
|
+
...(filter.role ? { role: filter.role } : {}),
|
|
190
|
+
peek: filter.peek === true,
|
|
191
|
+
lease: filter.lease === true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function parseReceiveFilterFromUrl(url) {
|
|
195
|
+
const roleParam = url.searchParams.get('role');
|
|
196
|
+
if (roleParam !== null && roleParam !== 'worker' && roleParam !== 'requester')
|
|
197
|
+
throw new Error('INVALID_REQUEST');
|
|
198
|
+
const peekParam = url.searchParams.get('peek');
|
|
199
|
+
if (peekParam !== null &&
|
|
200
|
+
peekParam !== 'true' &&
|
|
201
|
+
peekParam !== '1' &&
|
|
202
|
+
peekParam !== 'false' &&
|
|
203
|
+
peekParam !== '0')
|
|
204
|
+
throw new Error('INVALID_REQUEST');
|
|
205
|
+
const taskIdParams = url.searchParams
|
|
206
|
+
.getAll('taskId')
|
|
207
|
+
.flatMap((v) => v.split(','))
|
|
208
|
+
.map((v) => v.trim())
|
|
209
|
+
.filter(Boolean);
|
|
210
|
+
const leaseParam = url.searchParams.get('lease');
|
|
211
|
+
if (leaseParam !== null && leaseParam !== 'true' && leaseParam !== 'false')
|
|
212
|
+
throw new Error('INVALID_REQUEST');
|
|
213
|
+
const lease = leaseParam === 'true';
|
|
214
|
+
const peek = peekParam === 'true' || peekParam === '1';
|
|
215
|
+
if (!roleParam && taskIdParams.length === 0 && !peek && !lease)
|
|
216
|
+
return undefined;
|
|
217
|
+
return {
|
|
218
|
+
...(roleParam ? { role: roleParam } : {}),
|
|
219
|
+
...(taskIdParams.length > 0 ? { taskId: taskIdParams } : {}),
|
|
220
|
+
...(peek ? { peek: true } : {}),
|
|
221
|
+
...(lease ? { lease: true } : {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
export function inboxRecipientFromRequest(path, url) {
|
|
225
|
+
if (path === '/envelopes' || path === '/stream')
|
|
226
|
+
return url.searchParams.get('recipient') ?? '';
|
|
227
|
+
if (path.startsWith('/inbox/')) {
|
|
228
|
+
const rest = path.slice('/inbox/'.length);
|
|
229
|
+
if (!rest || rest.includes('/'))
|
|
230
|
+
return undefined;
|
|
231
|
+
return decodeURIComponent(rest);
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
function matchesStoredEnvelope(item, filter) {
|
|
236
|
+
const { taskId, eventType, senderDid, recipientDid } = item.envelope.payload;
|
|
237
|
+
if (filter.taskIds && !filter.taskIds.has(taskId))
|
|
238
|
+
return false;
|
|
239
|
+
if (!filter.role)
|
|
240
|
+
return true;
|
|
241
|
+
if (item.role === filter.role)
|
|
242
|
+
return true;
|
|
243
|
+
// When a requester explicitly polls for its known taskId on a cross-DID task,
|
|
244
|
+
// any non-worker-primary frame for that taskId belongs to the requester even after a cold restart.
|
|
245
|
+
if (filter.role === 'requester' &&
|
|
246
|
+
filter.taskIds?.has(taskId) &&
|
|
247
|
+
senderDid !== recipientDid &&
|
|
248
|
+
!WORKER_PRIMARY_EVENTS.has(eventType))
|
|
249
|
+
return true;
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
function positiveLimit(value, fallback) {
|
|
253
|
+
const limit = value ?? fallback;
|
|
254
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
255
|
+
throw new Error('INVALID_RELAY_LIMIT');
|
|
256
|
+
return limit;
|
|
257
|
+
}
|
|
258
|
+
export function relayLimitsFromEnv(env) {
|
|
259
|
+
const read = (key) => {
|
|
260
|
+
const value = env?.[key];
|
|
261
|
+
if (typeof value !== 'string' || value === '')
|
|
262
|
+
return undefined;
|
|
263
|
+
const parsed = Number(value);
|
|
264
|
+
return Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : undefined;
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
notificationTtlMs: read('ACP_NOTIFICATION_TTL_MS'),
|
|
268
|
+
maxEnvelopeBytes: read('ACP_MAX_ENVELOPE_BYTES'),
|
|
269
|
+
maxQueueSize: read('ACP_MAX_QUEUE_SIZE'),
|
|
270
|
+
maxQueueBytesPerRecipient: read('ACP_MAX_QUEUE_BYTES'),
|
|
271
|
+
maxEnvelopesPerSender: read('ACP_MAX_ENVELOPES_PER_SENDER'),
|
|
272
|
+
maxSeenEntries: read('ACP_MAX_SEEN_ENTRIES'),
|
|
273
|
+
envelopeTtlMs: read('ACP_ENVELOPE_TTL_MS'),
|
|
274
|
+
seenTtlMs: read('ACP_SEEN_TTL_MS'),
|
|
275
|
+
authSeenTtlMs: read('ACP_AUTH_SEEN_TTL_MS'),
|
|
276
|
+
agentCardTtlMs: read('ACP_AGENT_CARD_TTL_MS'),
|
|
277
|
+
maxAgents: read('ACP_MAX_AGENTS'),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export function canonicalRelayPath(pathname) {
|
|
281
|
+
if (pathname === '/v1')
|
|
282
|
+
return '/';
|
|
283
|
+
if (pathname.startsWith('/v1/'))
|
|
284
|
+
return pathname.slice(3);
|
|
285
|
+
return pathname;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Registry identity for one owner's registration of one agent.
|
|
289
|
+
* did:key values do not contain "/", so the pair round-trips unambiguously.
|
|
290
|
+
*/
|
|
291
|
+
export function agentRegistryKey(ownerDid, agentDid) {
|
|
292
|
+
return ownerDid + '/' + agentDid;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Use the same verified, deterministic DID selection as requesters.
|
|
296
|
+
*/
|
|
297
|
+
export function selectAgentCard(cards, agentDid, now = Date.now()) {
|
|
298
|
+
return findDiscoveredAgentCard([...cards], agentDid, now);
|
|
299
|
+
}
|
|
300
|
+
export class InMemoryRelay {
|
|
301
|
+
operations = new Map();
|
|
302
|
+
notificationKey;
|
|
303
|
+
wake;
|
|
304
|
+
notificationTtlMs;
|
|
305
|
+
flushing;
|
|
306
|
+
get notificationDid() { return this.notificationKey.did; }
|
|
307
|
+
/** Durable hosts install the persisted relay identity before restoring queues. */
|
|
308
|
+
setNotificationKey(key) { this.notificationKey = key; }
|
|
309
|
+
queues = new Map();
|
|
310
|
+
taskStates = new Map();
|
|
311
|
+
seen = new Map();
|
|
312
|
+
authSeen = new Map();
|
|
313
|
+
agentExpiry = new Map();
|
|
314
|
+
version = 0;
|
|
315
|
+
get mutationVersion() {
|
|
316
|
+
return this.version;
|
|
317
|
+
}
|
|
318
|
+
listeners = new Map();
|
|
319
|
+
/** Keyed by agentRegistryKey(ownerDid, agentDid). One owner cannot block another. */
|
|
320
|
+
agents = new Map();
|
|
321
|
+
tamper;
|
|
322
|
+
requireAuth;
|
|
323
|
+
allowUnboundRelayAuth;
|
|
324
|
+
maxSeenEntries;
|
|
325
|
+
maxAuthSeenEntries;
|
|
326
|
+
maxQueues;
|
|
327
|
+
maxQueueSize;
|
|
328
|
+
maxEnvelopeBytes;
|
|
329
|
+
maxQueueBytesPerRecipient;
|
|
330
|
+
maxEnvelopesPerSender;
|
|
331
|
+
maxAgents;
|
|
332
|
+
envelopeTtlMs;
|
|
333
|
+
seenTtlMs;
|
|
334
|
+
authSeenTtlMs;
|
|
335
|
+
agentCardTtlMs;
|
|
336
|
+
now;
|
|
337
|
+
constructor(options) {
|
|
338
|
+
const config = typeof options === 'function' ? { tamper: options } : (options ?? {});
|
|
339
|
+
this.notificationKey = config.notificationKey ?? generateKeyPair();
|
|
340
|
+
this.wake = config.wake;
|
|
341
|
+
if (this.wake) {
|
|
342
|
+
positiveLimit(this.wake.maxAttempts, 5);
|
|
343
|
+
positiveLimit(this.wake.retryBaseMs, 1000);
|
|
344
|
+
if (positiveLimit(this.wake.timeoutMs, 5000) > 2_147_483_647)
|
|
345
|
+
throw new Error('INVALID_RELAY_LIMIT');
|
|
346
|
+
const url = new URL(this.wake.relay);
|
|
347
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
348
|
+
throw new Error('INVALID_REQUEST');
|
|
349
|
+
}
|
|
350
|
+
this.notificationTtlMs = positiveLimit(config.notificationTtlMs, DEFAULT_SEEN_TTL_MS);
|
|
351
|
+
this.tamper = config.tamper;
|
|
352
|
+
this.requireAuth = config.requireAuth ?? false;
|
|
353
|
+
this.allowUnboundRelayAuth = config.allowUnboundRelayAuth ?? false;
|
|
354
|
+
this.maxSeenEntries = positiveLimit(config.maxSeenEntries, DEFAULT_MAX_SEEN_ENTRIES);
|
|
355
|
+
this.maxAuthSeenEntries = positiveLimit(config.maxAuthSeenEntries, this.maxSeenEntries);
|
|
356
|
+
this.maxQueues = positiveLimit(config.maxQueues, DEFAULT_MAX_QUEUES);
|
|
357
|
+
this.maxQueueSize = positiveLimit(config.maxQueueSize, DEFAULT_MAX_QUEUE_SIZE);
|
|
358
|
+
this.maxEnvelopeBytes = positiveLimit(config.maxEnvelopeBytes, DEFAULT_MAX_ENVELOPE_BYTES);
|
|
359
|
+
this.maxQueueBytesPerRecipient = positiveLimit(config.maxQueueBytesPerRecipient, DEFAULT_MAX_QUEUE_BYTES_PER_RECIPIENT);
|
|
360
|
+
this.maxEnvelopesPerSender = positiveLimit(config.maxEnvelopesPerSender, this.maxQueueSize);
|
|
361
|
+
this.maxAgents = positiveLimit(config.maxAgents, DEFAULT_MAX_AGENTS);
|
|
362
|
+
this.envelopeTtlMs = positiveLimit(config.envelopeTtlMs, DEFAULT_ENVELOPE_TTL_MS);
|
|
363
|
+
this.seenTtlMs = positiveLimit(config.seenTtlMs, DEFAULT_SEEN_TTL_MS);
|
|
364
|
+
this.authSeenTtlMs = positiveLimit(config.authSeenTtlMs, DEFAULT_AUTH_SEEN_TTL_MS);
|
|
365
|
+
this.agentCardTtlMs = positiveLimit(config.agentCardTtlMs, DEFAULT_AGENT_CARD_TTL_MS);
|
|
366
|
+
this.now = config.now ?? Date.now;
|
|
367
|
+
}
|
|
368
|
+
remember(map, key, limit, ttlMs) {
|
|
369
|
+
const now = this.now();
|
|
370
|
+
for (const [entry, expiresAt] of map)
|
|
371
|
+
if (expiresAt <= now)
|
|
372
|
+
map.delete(entry);
|
|
373
|
+
if (map.size >= limit) {
|
|
374
|
+
const oldest = map.keys().next().value;
|
|
375
|
+
if (oldest !== undefined)
|
|
376
|
+
map.delete(oldest);
|
|
377
|
+
}
|
|
378
|
+
map.set(key, now + ttlMs);
|
|
379
|
+
this.version++;
|
|
380
|
+
}
|
|
381
|
+
authenticate(recipientDid, token, action, check) {
|
|
382
|
+
this.evictExpired();
|
|
383
|
+
const result = verifyRelayAuthToken(token ?? '', recipientDid, {
|
|
384
|
+
expectedOrigin: check?.expectedOrigin,
|
|
385
|
+
requireOrigin: check?.requireOrigin,
|
|
386
|
+
});
|
|
387
|
+
if (!result.valid) {
|
|
388
|
+
if (result.reason === 'RELAY_AUTH_ORIGIN_MISMATCH' ||
|
|
389
|
+
result.reason === 'RELAY_AUTH_ORIGIN_REQUIRED')
|
|
390
|
+
throw new Error(result.reason);
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
if (action !== undefined && result.action !== action)
|
|
394
|
+
return false;
|
|
395
|
+
const key = recipientDid + ':' + result.nonce;
|
|
396
|
+
if (this.authSeen.has(key))
|
|
397
|
+
return false;
|
|
398
|
+
this.remember(this.authSeen, key, this.maxAuthSeenEntries, this.authSeenTtlMs);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
stateForTask(taskId) {
|
|
402
|
+
let state = this.taskStates.get(taskId);
|
|
403
|
+
if (!state) {
|
|
404
|
+
if (this.taskStates.size >= this.maxSeenEntries) {
|
|
405
|
+
const oldest = this.taskStates.keys().next().value;
|
|
406
|
+
if (oldest !== undefined)
|
|
407
|
+
this.taskStates.delete(oldest);
|
|
408
|
+
}
|
|
409
|
+
state = { crossRole: new Map(), pendingAckRoles: [], workerSeq: 0, requesterSeq: 0 };
|
|
410
|
+
this.taskStates.set(taskId, state);
|
|
411
|
+
}
|
|
412
|
+
return state;
|
|
413
|
+
}
|
|
414
|
+
/** Authenticate once for a long poll or persistent stream, then read only this inbox. */
|
|
415
|
+
openInbox(recipientDid, token, action = 'receive', check, filter) {
|
|
416
|
+
const normalized = normalizeReceiveFilter(filter);
|
|
417
|
+
if (!this.authenticate(recipientDid, token, action, check))
|
|
418
|
+
throw new Error('RELAY_UNAUTHORIZED');
|
|
419
|
+
if (normalized.peek) {
|
|
420
|
+
const seenInPeek = new Set();
|
|
421
|
+
return () => this.dequeue(recipientDid, filter, seenInPeek);
|
|
422
|
+
}
|
|
423
|
+
return () => this.dequeue(recipientDid, filter);
|
|
424
|
+
}
|
|
425
|
+
ensureQueue(did) {
|
|
426
|
+
let queue = this.queues.get(did);
|
|
427
|
+
if (!queue) {
|
|
428
|
+
if (this.queues.size >= this.maxQueues)
|
|
429
|
+
throw new Error('RELAY_CAPACITY');
|
|
430
|
+
queue = [];
|
|
431
|
+
this.queues.set(did, queue);
|
|
432
|
+
}
|
|
433
|
+
return queue;
|
|
434
|
+
}
|
|
435
|
+
putOperation(id, operation) {
|
|
436
|
+
// Each kind has its own cap; notification traffic cannot consume queue capacity.
|
|
437
|
+
if (!this.operations.has(id)) {
|
|
438
|
+
const same = [...this.operations].filter(([, row]) => row.kind === operation.kind);
|
|
439
|
+
if (same.length >= this.maxSeenEntries)
|
|
440
|
+
this.operations.delete(same[0][0]);
|
|
441
|
+
}
|
|
442
|
+
this.operations.set(id, operation);
|
|
443
|
+
this.version++;
|
|
444
|
+
}
|
|
445
|
+
registerWake(recipientDid, channel, token, check) {
|
|
446
|
+
if (!this.authenticate(recipientDid, token, 'receive', check))
|
|
447
|
+
throw new Error('RELAY_UNAUTHORIZED');
|
|
448
|
+
if (channel === null) {
|
|
449
|
+
this.operations.delete('registration:' + recipientDid);
|
|
450
|
+
for (const [id, op] of this.operations)
|
|
451
|
+
if (op.kind === 'wake' && op.recipientDid === recipientDid)
|
|
452
|
+
this.operations.delete(id);
|
|
453
|
+
this.version++;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (!this.wake || !Object.hasOwn(this.wake.channels, channel))
|
|
457
|
+
throw new Error('INVALID_REQUEST');
|
|
458
|
+
this.putOperation('registration:' + recipientDid, {
|
|
459
|
+
kind: 'registration', recipientDid, channel, expiresAt: this.now() + this.notificationTtlMs,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
queueWake(envelope) {
|
|
463
|
+
const { recipientDid, taskId } = envelope.payload;
|
|
464
|
+
if (!this.wake || this.listeners.get(recipientDid)?.size)
|
|
465
|
+
return;
|
|
466
|
+
const reg = this.operations.get('registration:' + recipientDid);
|
|
467
|
+
const id = 'wake:' + recipientDid + ':' + taskId;
|
|
468
|
+
if (reg?.kind !== 'registration' || reg.expiresAt <= this.now() || this.operations.has(id))
|
|
469
|
+
return;
|
|
470
|
+
this.putOperation(id, { kind: 'wake', recipientDid, taskId, channel: reg.channel,
|
|
471
|
+
attempts: 0, nextAttempt: this.now(), done: false, expiresAt: this.now() + this.notificationTtlMs });
|
|
472
|
+
}
|
|
473
|
+
/** Claim inside a durable mutation turn and persist before invoking the channel. */
|
|
474
|
+
claimWakeNotification() {
|
|
475
|
+
if (!this.wake)
|
|
476
|
+
return;
|
|
477
|
+
for (const [id, op] of this.operations) {
|
|
478
|
+
if (op.kind !== 'wake' || op.done || op.nextAttempt > this.now() || op.expiresAt <= this.now())
|
|
479
|
+
continue;
|
|
480
|
+
const registration = this.operations.get('registration:' + op.recipientDid);
|
|
481
|
+
if (op.attempts >= (this.wake.maxAttempts ?? 5) || registration?.kind !== 'registration' ||
|
|
482
|
+
registration.expiresAt <= this.now() || registration.channel !== op.channel ||
|
|
483
|
+
this.listeners.get(op.recipientDid)?.size ||
|
|
484
|
+
!this.queues.get(op.recipientDid)?.some(item => item.expiresAt > this.now() && item.envelope.payload.taskId === op.taskId)) {
|
|
485
|
+
op.done = true;
|
|
486
|
+
this.version++;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const channel = this.wake.channels[op.channel];
|
|
490
|
+
if (!channel || !Object.hasOwn(this.wake.channels, op.channel))
|
|
491
|
+
continue;
|
|
492
|
+
op.attempts++;
|
|
493
|
+
op.nextAttempt = this.now() + Math.min(60_000, (this.wake.retryBaseMs ?? 1000) * 2 ** Math.min(op.attempts - 1, 16));
|
|
494
|
+
this.version++;
|
|
495
|
+
return { id, attempt: op.attempts, deliver: channel, timeoutMs: this.wake.timeoutMs ?? 5000,
|
|
496
|
+
notice: signPayload({ recipientDid: op.recipientDid, taskId: op.taskId, relay: this.wake.relay }, this.notificationKey) };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/** Run outside the durable mutation turn so notification latency cannot stall inbox delivery. */
|
|
500
|
+
async deliverWakeNotification(attempt) {
|
|
501
|
+
const controller = new AbortController();
|
|
502
|
+
let timer;
|
|
503
|
+
try {
|
|
504
|
+
await Promise.race([
|
|
505
|
+
attempt.deliver(attempt.notice, controller.signal),
|
|
506
|
+
new Promise((_, reject) => {
|
|
507
|
+
timer = setTimeout(() => {
|
|
508
|
+
controller.abort();
|
|
509
|
+
reject(new Error('WAKE_TIMEOUT'));
|
|
510
|
+
}, attempt.timeoutMs);
|
|
511
|
+
}),
|
|
512
|
+
]);
|
|
513
|
+
return !controller.signal.aborted;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
clearTimeout(timer);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
completeWakeNotification(attempt, delivered) {
|
|
523
|
+
const op = this.operations.get(attempt.id);
|
|
524
|
+
if (op?.kind !== 'wake' || op.attempts !== attempt.attempt)
|
|
525
|
+
return;
|
|
526
|
+
op.done = delivered || op.attempts >= (this.wake?.maxAttempts ?? 5);
|
|
527
|
+
if (!op.done)
|
|
528
|
+
op.nextAttempt = this.now() + Math.min(60_000, (this.wake?.retryBaseMs ?? 1000) * 2 ** Math.min(op.attempts - 1, 16));
|
|
529
|
+
this.version++;
|
|
530
|
+
}
|
|
531
|
+
/** In-memory hosts call periodically; durable hosts commit claims and completion separately. */
|
|
532
|
+
flushWakeNotifications() {
|
|
533
|
+
if (this.flushing)
|
|
534
|
+
return this.flushing;
|
|
535
|
+
this.flushing = (async () => {
|
|
536
|
+
let attempt;
|
|
537
|
+
while ((attempt = this.claimWakeNotification())) {
|
|
538
|
+
const delivered = await this.deliverWakeNotification(attempt);
|
|
539
|
+
this.completeWakeNotification(attempt, delivered);
|
|
540
|
+
}
|
|
541
|
+
})().finally(() => { this.flushing = undefined; });
|
|
542
|
+
return this.flushing;
|
|
543
|
+
}
|
|
544
|
+
getDeliveryStatus(recipientDid, envelopeId, senderDid, relay, token, check) {
|
|
545
|
+
if (!this.authenticate(senderDid, token, 'receive', check))
|
|
546
|
+
throw new Error('RELAY_UNAUTHORIZED');
|
|
547
|
+
const expired = this.operations.get('expiry:' + envelopeId);
|
|
548
|
+
if (expired?.kind === 'expiry' && expired.notice.payload.senderDid === senderDid &&
|
|
549
|
+
expired.notice.payload.recipientDid === recipientDid)
|
|
550
|
+
return signPayload({ ...expired.notice.payload, relay }, this.notificationKey);
|
|
551
|
+
const item = this.queues.get(recipientDid)?.find(row => row.envelope.payload.envelopeId === envelopeId && row.envelope.payload.senderDid === senderDid);
|
|
552
|
+
return signPayload({ type: 'RelayDeliveryStatus', state: item ? 'pending' : 'unknown', envelopeId,
|
|
553
|
+
senderDid, recipientDid, relay,
|
|
554
|
+
...(item ? { taskId: item.envelope.payload.taskId, expiresAt: item.expiresAt } : {}),
|
|
555
|
+
}, this.notificationKey);
|
|
556
|
+
}
|
|
557
|
+
/** Drop queued envelopes, replay ids, and agent cards whose TTL has elapsed. */
|
|
558
|
+
evictExpired() {
|
|
559
|
+
const now = this.now();
|
|
560
|
+
let changed = false;
|
|
561
|
+
for (const [id, op] of this.operations)
|
|
562
|
+
if (op.expiresAt <= now) {
|
|
563
|
+
this.operations.delete(id);
|
|
564
|
+
changed = true;
|
|
565
|
+
}
|
|
566
|
+
for (const [did, queue] of this.queues) {
|
|
567
|
+
const live = queue.filter((item) => {
|
|
568
|
+
if (item.expiresAt > now)
|
|
569
|
+
return true;
|
|
570
|
+
const { envelopeId, senderDid, recipientDid, taskId } = item.envelope.payload;
|
|
571
|
+
this.putOperation('expiry:' + envelopeId, { kind: 'expiry', expiresAt: now + this.notificationTtlMs,
|
|
572
|
+
notice: signPayload({ type: 'RelayDeliveryStatus', state: 'expired', envelopeId, senderDid,
|
|
573
|
+
recipientDid, taskId, relay: this.wake?.relay ?? '', expiresAt: item.expiresAt }, this.notificationKey) });
|
|
574
|
+
return false;
|
|
575
|
+
});
|
|
576
|
+
if (live.length === queue.length)
|
|
577
|
+
continue;
|
|
578
|
+
changed = true;
|
|
579
|
+
if (live.length || this.listeners.get(did)?.size)
|
|
580
|
+
this.queues.set(did, live);
|
|
581
|
+
else
|
|
582
|
+
this.queues.delete(did);
|
|
583
|
+
}
|
|
584
|
+
for (const map of [this.seen, this.authSeen, this.agentExpiry]) {
|
|
585
|
+
for (const [key, expiresAt] of map) {
|
|
586
|
+
if (expiresAt > now)
|
|
587
|
+
continue;
|
|
588
|
+
map.delete(key);
|
|
589
|
+
changed = true;
|
|
590
|
+
if (map === this.agentExpiry)
|
|
591
|
+
this.agents.delete(key);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
for (const [key, card] of this.agents) {
|
|
595
|
+
const expiresAt = this.agentExpiry.get(key);
|
|
596
|
+
if ((expiresAt !== undefined && expiresAt <= now) || !verifySignedAgentCard(card, now)) {
|
|
597
|
+
this.agents.delete(key);
|
|
598
|
+
this.agentExpiry.delete(key);
|
|
599
|
+
changed = true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (changed)
|
|
603
|
+
this.version++;
|
|
604
|
+
}
|
|
605
|
+
queuedBytes(queue) {
|
|
606
|
+
let total = 0;
|
|
607
|
+
for (const item of queue)
|
|
608
|
+
total += item.bytes;
|
|
609
|
+
return total;
|
|
610
|
+
}
|
|
611
|
+
senderCount(queue, senderDid) {
|
|
612
|
+
let total = 0;
|
|
613
|
+
for (const item of queue)
|
|
614
|
+
if (item.envelope.payload.senderDid === senderDid)
|
|
615
|
+
total++;
|
|
616
|
+
return total;
|
|
617
|
+
}
|
|
618
|
+
send(envelope) {
|
|
619
|
+
if (!verifyRelayEnvelope(envelope))
|
|
620
|
+
throw new Error('INVALID_ENVELOPE');
|
|
621
|
+
this.evictExpired();
|
|
622
|
+
if (this.seen.has(envelope.payload.envelopeId) || this.operations.has('expiry:' + envelope.payload.envelopeId))
|
|
623
|
+
throw new Error('REPLAYED_ENVELOPE');
|
|
624
|
+
const did = envelope.payload.recipientDid;
|
|
625
|
+
const queue = this.queues.get(did) ?? [];
|
|
626
|
+
const bytes = Buffer.byteLength(JSON.stringify(envelope));
|
|
627
|
+
if (!this.queues.has(did) && this.queues.size >= this.maxQueues)
|
|
628
|
+
throw new Error('RELAY_CAPACITY');
|
|
629
|
+
if (queue.length >= this.maxQueueSize || bytes > this.maxEnvelopeBytes)
|
|
630
|
+
throw new Error('RELAY_CAPACITY');
|
|
631
|
+
if (this.queuedBytes(queue) + bytes > this.maxQueueBytesPerRecipient)
|
|
632
|
+
throw new Error('QUEUE_BYTES_EXCEEDED');
|
|
633
|
+
if (this.senderCount(queue, envelope.payload.senderDid) >= this.maxEnvelopesPerSender)
|
|
634
|
+
throw new Error('SENDER_QUOTA_EXCEEDED');
|
|
635
|
+
this.remember(this.seen, envelope.payload.envelopeId, this.maxSeenEntries, this.seenTtlMs);
|
|
636
|
+
const role = classifyEnvelopeRole(envelope, this.stateForTask(envelope.payload.taskId));
|
|
637
|
+
const wire = structuredClone(envelope);
|
|
638
|
+
const stored = this.tamper ? this.tamper(wire) : wire;
|
|
639
|
+
queue.push({
|
|
640
|
+
envelope: stored,
|
|
641
|
+
bytes: Buffer.byteLength(JSON.stringify(stored)),
|
|
642
|
+
expiresAt: this.now() + this.envelopeTtlMs,
|
|
643
|
+
role,
|
|
644
|
+
});
|
|
645
|
+
this.queues.set(did, queue);
|
|
646
|
+
this.queueWake(envelope);
|
|
647
|
+
for (const fn of [...(this.listeners.get(did) ?? [])])
|
|
648
|
+
fn();
|
|
649
|
+
}
|
|
650
|
+
receive(recipientDid, authToken, _signal, checkOrFilter, maybeFilter) {
|
|
651
|
+
const isFilter = checkOrFilter !== undefined &&
|
|
652
|
+
('role' in checkOrFilter || 'taskId' in checkOrFilter || 'peek' in checkOrFilter || 'lease' in checkOrFilter);
|
|
653
|
+
const check = isFilter ? undefined : checkOrFilter;
|
|
654
|
+
const filter = isFilter ? checkOrFilter : maybeFilter;
|
|
655
|
+
const normalized = normalizeReceiveFilter(filter);
|
|
656
|
+
if ((typeof authToken === 'string' || this.requireAuth) &&
|
|
657
|
+
!this.authenticate(recipientDid, typeof authToken === 'string' ? authToken : undefined, 'receive', check))
|
|
658
|
+
throw new Error('RELAY_UNAUTHORIZED');
|
|
659
|
+
return this.dequeue(recipientDid, normalized);
|
|
660
|
+
}
|
|
661
|
+
peek(recipientDid, authTokenOrFilter, check, filter) {
|
|
662
|
+
const authToken = typeof authTokenOrFilter === 'string' ? authTokenOrFilter : undefined;
|
|
663
|
+
const effectiveFilter = typeof authTokenOrFilter === 'object' && authTokenOrFilter !== null
|
|
664
|
+
? authTokenOrFilter
|
|
665
|
+
: filter;
|
|
666
|
+
return this.receive(recipientDid, authToken, undefined, check, {
|
|
667
|
+
...effectiveFilter,
|
|
668
|
+
peek: true,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
pollInbox(recipientDid, authTokenOrFilter, filter, check) {
|
|
672
|
+
const authToken = typeof authTokenOrFilter === 'string' ? authTokenOrFilter : undefined;
|
|
673
|
+
const effectiveFilter = typeof authTokenOrFilter === 'object' && authTokenOrFilter !== null
|
|
674
|
+
? authTokenOrFilter
|
|
675
|
+
: filter;
|
|
676
|
+
return this.receive(recipientDid, authToken, undefined, check, effectiveFilter);
|
|
677
|
+
}
|
|
678
|
+
dequeue(recipientDid, filter, seenInPeek) {
|
|
679
|
+
this.evictExpired();
|
|
680
|
+
const normalized = filter && 'peek' in filter && typeof filter.peek === 'boolean' && !('taskId' in filter)
|
|
681
|
+
? filter
|
|
682
|
+
: normalizeReceiveFilter(filter);
|
|
683
|
+
const q = this.queues.get(recipientDid);
|
|
684
|
+
if (!q || !q.length)
|
|
685
|
+
return undefined;
|
|
686
|
+
const index = q.findIndex((item) => (normalized.peek || (item.leasedUntil ?? 0) <= this.now()) &&
|
|
687
|
+
matchesStoredEnvelope(item, normalized) &&
|
|
688
|
+
(!seenInPeek || !seenInPeek.has(item.envelope.payload.envelopeId)));
|
|
689
|
+
if (index < 0)
|
|
690
|
+
return undefined;
|
|
691
|
+
const item = normalized.peek || normalized.lease ? q[index] : q.splice(index, 1)[0];
|
|
692
|
+
if (item && normalized.lease && !normalized.peek)
|
|
693
|
+
item.leasedUntil = Math.min(item.expiresAt, this.now() + 30_000);
|
|
694
|
+
if (!normalized.peek) {
|
|
695
|
+
if (!q.length && !this.listeners.get(recipientDid)?.size)
|
|
696
|
+
this.queues.delete(recipientDid);
|
|
697
|
+
if (item)
|
|
698
|
+
this.version++;
|
|
699
|
+
}
|
|
700
|
+
else if (item && seenInPeek) {
|
|
701
|
+
seenInPeek.add(item.envelope.payload.envelopeId);
|
|
702
|
+
}
|
|
703
|
+
if (item && !verifyRelayEnvelope(item.envelope, recipientDid))
|
|
704
|
+
throw new Error('RELAY_TAMPERING_DETECTED');
|
|
705
|
+
return item ? (normalized.peek ? structuredClone(item.envelope) : item.envelope) : undefined;
|
|
706
|
+
}
|
|
707
|
+
/** Recipient-authenticated removal of one queued envelope. False when it is not queued. */
|
|
708
|
+
acknowledge(recipientDid, envelopeId, token, check) {
|
|
709
|
+
this.evictExpired();
|
|
710
|
+
if (!this.authenticate(recipientDid, token, 'receive', check))
|
|
711
|
+
throw new Error('RELAY_UNAUTHORIZED');
|
|
712
|
+
const queue = this.queues.get(recipientDid);
|
|
713
|
+
if (!queue)
|
|
714
|
+
return false;
|
|
715
|
+
const index = queue.findIndex((item) => item.envelope.payload.envelopeId === envelopeId);
|
|
716
|
+
if (index < 0)
|
|
717
|
+
return false;
|
|
718
|
+
queue.splice(index, 1);
|
|
719
|
+
if (!queue.length && !this.listeners.get(recipientDid)?.size)
|
|
720
|
+
this.queues.delete(recipientDid);
|
|
721
|
+
this.version++;
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
/** Seconds until the soonest queued item expires, for Retry-After. */
|
|
725
|
+
retryAfterSeconds() {
|
|
726
|
+
const next = this.nextExpiry();
|
|
727
|
+
if (next === undefined)
|
|
728
|
+
return 60;
|
|
729
|
+
return Math.max(1, Math.ceil((next - this.now()) / 1000));
|
|
730
|
+
}
|
|
731
|
+
nextExpiry() {
|
|
732
|
+
const now = this.now();
|
|
733
|
+
let soonest = Infinity;
|
|
734
|
+
const consider = (value) => {
|
|
735
|
+
if (value > now && value < soonest)
|
|
736
|
+
soonest = value;
|
|
737
|
+
};
|
|
738
|
+
for (const op of this.operations.values()) {
|
|
739
|
+
consider(op.expiresAt);
|
|
740
|
+
if (op.kind === 'wake' && !op.done)
|
|
741
|
+
consider(Math.max(now + 1, op.nextAttempt));
|
|
742
|
+
}
|
|
743
|
+
for (const queue of this.queues.values())
|
|
744
|
+
for (const item of queue)
|
|
745
|
+
consider(item.expiresAt);
|
|
746
|
+
for (const expiresAt of this.seen.values())
|
|
747
|
+
consider(expiresAt);
|
|
748
|
+
for (const expiresAt of this.authSeen.values())
|
|
749
|
+
consider(expiresAt);
|
|
750
|
+
for (const expiresAt of this.agentExpiry.values())
|
|
751
|
+
consider(expiresAt);
|
|
752
|
+
return Number.isFinite(soonest) ? soonest : undefined;
|
|
753
|
+
}
|
|
754
|
+
withRecipient(key) {
|
|
755
|
+
return {
|
|
756
|
+
send: (e) => this.send(e),
|
|
757
|
+
deliveryStatus: (e) => this.getDeliveryStatus(e.payload.recipientDid, e.payload.envelopeId, key.did, this.wake?.relay ?? '', createRelayAuthToken(key)),
|
|
758
|
+
receive: (did, _waitMs, signal, filter) => this.receive(did, createRelayAuthToken(key), signal, undefined, filter),
|
|
759
|
+
peek: (did, filter) => this.peek(did, createRelayAuthToken(key), undefined, filter),
|
|
760
|
+
pollInbox: (did, filter) => this.receive(did, createRelayAuthToken(key), filter?.signal, undefined, filter),
|
|
761
|
+
register: (card) => this.register(card),
|
|
762
|
+
discover: () => this.discover(),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
subscribe(did, fn) {
|
|
766
|
+
this.ensureQueue(did);
|
|
767
|
+
const set = this.listeners.get(did) ?? new Set();
|
|
768
|
+
set.add(fn);
|
|
769
|
+
this.listeners.set(did, set);
|
|
770
|
+
return () => {
|
|
771
|
+
set.delete(fn);
|
|
772
|
+
if (!set.size) {
|
|
773
|
+
this.listeners.delete(did);
|
|
774
|
+
if (!this.queues.get(did)?.length)
|
|
775
|
+
this.queues.delete(did);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
hasMatching(did, filter) {
|
|
780
|
+
const q = this.queues.get(did);
|
|
781
|
+
if (!q?.length)
|
|
782
|
+
return false;
|
|
783
|
+
const normalized = normalizeReceiveFilter(filter);
|
|
784
|
+
return q.some((item) => (normalized.peek || (item.leasedUntil ?? 0) <= this.now()) && matchesStoredEnvelope(item, normalized));
|
|
785
|
+
}
|
|
786
|
+
async wait(did, ms, signal, filter) {
|
|
787
|
+
this.evictExpired();
|
|
788
|
+
if (signal?.aborted || ms <= 0 || this.hasMatching(did, filter))
|
|
789
|
+
return;
|
|
790
|
+
const normalized = normalizeReceiveFilter(filter);
|
|
791
|
+
const leaseWait = (this.queues.get(did) ?? [])
|
|
792
|
+
.filter(item => matchesStoredEnvelope(item, normalized) && (item.leasedUntil ?? 0) > this.now())
|
|
793
|
+
.reduce((delay, item) => Math.min(delay, item.leasedUntil - this.now()), ms);
|
|
794
|
+
await new Promise((resolve) => {
|
|
795
|
+
let settled = false;
|
|
796
|
+
const done = () => {
|
|
797
|
+
if (settled)
|
|
798
|
+
return;
|
|
799
|
+
settled = true;
|
|
800
|
+
clearTimeout(timer);
|
|
801
|
+
off();
|
|
802
|
+
signal?.removeEventListener('abort', done);
|
|
803
|
+
resolve();
|
|
804
|
+
};
|
|
805
|
+
const checkWake = () => {
|
|
806
|
+
if (this.hasMatching(did, filter))
|
|
807
|
+
done();
|
|
808
|
+
};
|
|
809
|
+
const off = this.subscribe(did, checkWake), timer = setTimeout(done, leaseWait);
|
|
810
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
811
|
+
// Cover the gap between the length check and listener installation.
|
|
812
|
+
if (signal?.aborted || this.hasMatching(did, filter))
|
|
813
|
+
done();
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
snapshot() {
|
|
817
|
+
this.evictExpired();
|
|
818
|
+
const envelopeExpiry = [];
|
|
819
|
+
const leaseExpiry = [];
|
|
820
|
+
const queues = [];
|
|
821
|
+
for (const [did, queue] of this.queues) {
|
|
822
|
+
const envelopes = [];
|
|
823
|
+
for (const item of queue) {
|
|
824
|
+
envelopes.push(item.envelope);
|
|
825
|
+
if (item.leasedUntil)
|
|
826
|
+
leaseExpiry.push([item.envelope.payload.envelopeId, item.leasedUntil]);
|
|
827
|
+
envelopeExpiry.push([item.envelope.payload.envelopeId, item.expiresAt]);
|
|
828
|
+
}
|
|
829
|
+
queues.push([did, envelopes]);
|
|
830
|
+
}
|
|
831
|
+
return structuredClone({
|
|
832
|
+
operations: [...this.operations],
|
|
833
|
+
queues,
|
|
834
|
+
seen: [...this.seen.keys()],
|
|
835
|
+
authSeen: [...this.authSeen.keys()],
|
|
836
|
+
agents: [...this.agents.values()],
|
|
837
|
+
envelopeExpiry,
|
|
838
|
+
leaseExpiry,
|
|
839
|
+
seenExpiry: [...this.seen],
|
|
840
|
+
authExpiry: [...this.authSeen],
|
|
841
|
+
agentExpiry: [...this.agentExpiry],
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
restore(snapshot) {
|
|
845
|
+
const seenExpiry = new Map(snapshot.seenExpiry ?? []);
|
|
846
|
+
const authExpiry = new Map(snapshot.authExpiry ?? []);
|
|
847
|
+
const envelopeExpiry = new Map(snapshot.envelopeExpiry ?? []);
|
|
848
|
+
const leaseExpiry = new Map(snapshot.leaseExpiry ?? []);
|
|
849
|
+
const agentExpiry = new Map(snapshot.agentExpiry ?? []);
|
|
850
|
+
const now = this.now();
|
|
851
|
+
this.assertValidAgentSnapshot(snapshot, agentExpiry);
|
|
852
|
+
if (snapshot.seen.length > this.maxSeenEntries ||
|
|
853
|
+
(snapshot.authSeen?.length ?? 0) > this.maxAuthSeenEntries ||
|
|
854
|
+
snapshot.queues.length > this.maxQueues ||
|
|
855
|
+
snapshot.agents.length > this.maxAgents ||
|
|
856
|
+
snapshot.queues.some(([did, queue]) => {
|
|
857
|
+
if (queue.length > this.maxQueueSize)
|
|
858
|
+
return true;
|
|
859
|
+
let bytes = 0;
|
|
860
|
+
for (const envelope of queue) {
|
|
861
|
+
if (!verifyRelayEnvelope(envelope, did))
|
|
862
|
+
return true;
|
|
863
|
+
bytes += Buffer.byteLength(JSON.stringify(envelope));
|
|
864
|
+
if (bytes > this.maxQueueBytesPerRecipient || bytes > this.maxEnvelopeBytes * queue.length)
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
return queue.some((envelope) => Buffer.byteLength(JSON.stringify(envelope)) > this.maxEnvelopeBytes);
|
|
868
|
+
}))
|
|
869
|
+
throw new Error('INVALID_RELAY_SNAPSHOT');
|
|
870
|
+
this.operations = new Map(structuredClone(snapshot.operations ?? []));
|
|
871
|
+
for (const kind of ['registration', 'wake', 'expiry'])
|
|
872
|
+
if ([...this.operations.values()].filter(op => op.kind === kind).length > this.maxSeenEntries)
|
|
873
|
+
throw new Error('INVALID_RELAY_SNAPSHOT');
|
|
874
|
+
this.queues = new Map();
|
|
875
|
+
this.taskStates = new Map();
|
|
876
|
+
for (const [did, envelopes] of structuredClone(snapshot.queues)) {
|
|
877
|
+
this.queues.set(did, envelopes.map((envelope) => ({
|
|
878
|
+
envelope,
|
|
879
|
+
leasedUntil: leaseExpiry.get(envelope.payload.envelopeId),
|
|
880
|
+
bytes: Buffer.byteLength(JSON.stringify(envelope)),
|
|
881
|
+
expiresAt: envelopeExpiry.get(envelope.payload.envelopeId) ?? this.now() + this.envelopeTtlMs,
|
|
882
|
+
role: classifyEnvelopeRole(envelope, this.stateForTask(envelope.payload.taskId)),
|
|
883
|
+
})));
|
|
884
|
+
}
|
|
885
|
+
this.seen = new Map(snapshot.seen.map((id) => [id, seenExpiry.get(id) ?? this.now() + this.seenTtlMs]));
|
|
886
|
+
this.authSeen = new Map((snapshot.authSeen ?? []).map((id) => [id, authExpiry.get(id) ?? this.now() + this.authSeenTtlMs]));
|
|
887
|
+
this.agents.clear();
|
|
888
|
+
this.agentExpiry.clear();
|
|
889
|
+
for (const card of snapshot.agents) {
|
|
890
|
+
const key = agentRegistryKey(card.payload.ownerDid, card.payload.agentDid);
|
|
891
|
+
const effective = Math.min(agentExpiry.get(key) ?? now + this.agentCardTtlMs, Date.parse(card.payload.expiresAt));
|
|
892
|
+
if (effective <= now)
|
|
893
|
+
continue;
|
|
894
|
+
const copy = structuredClone(card);
|
|
895
|
+
this.agents.set(key, copy);
|
|
896
|
+
this.agentExpiry.set(key, effective);
|
|
897
|
+
}
|
|
898
|
+
this.version++;
|
|
899
|
+
}
|
|
900
|
+
/** Authenticate even expired cards before allowing restore to discard them. */
|
|
901
|
+
assertValidAgentSnapshot(snapshot, agentExpiry) {
|
|
902
|
+
if (!Array.isArray(snapshot.agents))
|
|
903
|
+
throw new Error('INVALID_AGENT_SNAPSHOT');
|
|
904
|
+
const seen = new Set();
|
|
905
|
+
for (const card of snapshot.agents) {
|
|
906
|
+
// Verification at the last millisecond of its lifetime skips only current
|
|
907
|
+
// expiry, retaining all signature, delegation, structure and TTL checks.
|
|
908
|
+
const expiresAt = Date.parse(card?.payload?.expiresAt);
|
|
909
|
+
if (!verifySignedAgentCard(card, expiresAt - 1))
|
|
910
|
+
throw new Error('INVALID_AGENT_SNAPSHOT');
|
|
911
|
+
const key = agentRegistryKey(card.payload.ownerDid, card.payload.agentDid);
|
|
912
|
+
const stored = agentExpiry.get(key);
|
|
913
|
+
if (seen.has(key) || (stored !== undefined && !Number.isFinite(stored)))
|
|
914
|
+
throw new Error('INVALID_AGENT_SNAPSHOT');
|
|
915
|
+
seen.add(key);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
discover() {
|
|
919
|
+
this.evictExpired();
|
|
920
|
+
const now = this.now();
|
|
921
|
+
return structuredClone([...this.agents.values()].filter((card) => verifySignedAgentCard(card, now)));
|
|
922
|
+
}
|
|
923
|
+
register(card) {
|
|
924
|
+
if (!verifySignedAgentCard(card, this.now()))
|
|
925
|
+
throw new Error('INVALID_AGENT_CARD');
|
|
926
|
+
this.evictExpired();
|
|
927
|
+
// A different ownerDid is a different registry entry and cannot squat this agentDid.
|
|
928
|
+
const key = agentRegistryKey(card.payload.ownerDid, card.payload.agentDid);
|
|
929
|
+
const old = this.agents.get(key);
|
|
930
|
+
if (old && Date.parse(old.signature.signedAt) > Date.parse(card.signature.signedAt))
|
|
931
|
+
throw new Error('AGENT_OWNER_OR_VERSION_MISMATCH');
|
|
932
|
+
if (!old && this.agents.size >= this.maxAgents)
|
|
933
|
+
throw new Error('RELAY_CAPACITY');
|
|
934
|
+
const copy = structuredClone(card);
|
|
935
|
+
const expiresAt = Math.min(this.now() + this.agentCardTtlMs, Date.parse(copy.payload.expiresAt));
|
|
936
|
+
if (!(expiresAt > this.now()))
|
|
937
|
+
throw new Error('INVALID_AGENT_CARD');
|
|
938
|
+
this.agents.set(key, copy);
|
|
939
|
+
this.agentExpiry.set(key, expiresAt);
|
|
940
|
+
this.version++;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
/** Defensive headers for any relay URL opened directly in a browser. */
|
|
944
|
+
export function relayHttpHeaders(kind = 'json') {
|
|
945
|
+
return {
|
|
946
|
+
'x-content-type-options': 'nosniff',
|
|
947
|
+
'content-security-policy': "default-src 'none'; frame-ancestors 'none'",
|
|
948
|
+
'x-frame-options': 'DENY',
|
|
949
|
+
'referrer-policy': 'no-referrer',
|
|
950
|
+
'cache-control': kind === 'sse' ? 'no-cache, no-store' : 'no-store',
|
|
951
|
+
...(kind === 'json' ? { 'content-type': 'application/json; charset=utf-8' } : {}),
|
|
952
|
+
...(kind === 'sse' ? { 'content-type': 'text/event-stream', 'x-accel-buffering': 'no' } : {}),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function relayJson(value, status = 200) {
|
|
956
|
+
return Response.json(value, { status, headers: relayHttpHeaders('json') });
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Protocol codes the relay may place in an HTTP or WebSocket error body.
|
|
960
|
+
* A thrown message is returned only when it matches one of these strings exactly.
|
|
961
|
+
* Names that the live relay does not throw today stay listed so a later
|
|
962
|
+
* constant code is not replaced by RELAY_INTERNAL_ERROR.
|
|
963
|
+
*/
|
|
964
|
+
export const KNOWN_RELAY_ERROR_CODES = [
|
|
965
|
+
'INVALID_ENVELOPE',
|
|
966
|
+
'INVALID_SIGNED_ENVELOPE',
|
|
967
|
+
'ENVELOPE_EXPIRED',
|
|
968
|
+
'REPLAYED_ENVELOPE',
|
|
969
|
+
'DUPLICATE_ENVELOPE_ID',
|
|
970
|
+
'ENVELOPE_TOO_LARGE',
|
|
971
|
+
'RELAY_CAPACITY',
|
|
972
|
+
'RELAY_QUEUE_FULL',
|
|
973
|
+
'QUEUE_BYTES_EXCEEDED',
|
|
974
|
+
'QUEUE_LIMIT_EXCEEDED',
|
|
975
|
+
'RELAY_BYTE_BUDGET_EXCEEDED',
|
|
976
|
+
'SENDER_QUOTA_EXCEEDED',
|
|
977
|
+
'INVALID_RECIPIENT_DID',
|
|
978
|
+
'MISSING_RECIPIENT_DID',
|
|
979
|
+
'INVALID_DID',
|
|
980
|
+
'RELAY_TAMPERING_DETECTED',
|
|
981
|
+
'INVALID_AGENT_CARD',
|
|
982
|
+
'INVALID_AGENT_SNAPSHOT',
|
|
983
|
+
'AMBIGUOUS_AGENT_CARD_OWNER',
|
|
984
|
+
'AGENT_CARD_EXPIRED',
|
|
985
|
+
'AGENT_CARD_EXPIRY_TOO_LONG',
|
|
986
|
+
'AGENT_CARD_DELEGATION_REQUIRED',
|
|
987
|
+
'AGENT_DELEGATION_REQUIRED',
|
|
988
|
+
'AGENT_CARD_DELEGATION_INVALID',
|
|
989
|
+
'AGENT_CARD_TOO_LARGE',
|
|
990
|
+
'AGENT_REGISTRY_FULL',
|
|
991
|
+
'AGENT_CARD_TTL_INVALID',
|
|
992
|
+
'AGENT_OWNER_OR_VERSION_MISMATCH',
|
|
993
|
+
'NO_AGENT_REGISTERED',
|
|
994
|
+
'RELAY_UNAUTHORIZED',
|
|
995
|
+
'RELAY_AUTH_REQUIRED',
|
|
996
|
+
'RELAY_AUTH_INVALID',
|
|
997
|
+
'RELAY_AUTH_SIGNATURE_INVALID',
|
|
998
|
+
'RELAY_AUTH_RECIPIENT_MISMATCH',
|
|
999
|
+
'RELAY_AUTH_ACTION_MISMATCH',
|
|
1000
|
+
'RELAY_AUTH_EXPIRED',
|
|
1001
|
+
'RELAY_AUTH_FUTURE_ISSUED_AT',
|
|
1002
|
+
'RELAY_AUTH_REPLAYED',
|
|
1003
|
+
'RELAY_AUTH_NONCE_INVALID',
|
|
1004
|
+
'RELAY_AUTH_ORIGIN_REQUIRED',
|
|
1005
|
+
'RELAY_AUTH_ORIGIN_MISMATCH',
|
|
1006
|
+
'RELAY_QUERY_AUTH_DISABLED',
|
|
1007
|
+
'INVALID_RELAY_ORIGIN',
|
|
1008
|
+
'RATE_LIMITED',
|
|
1009
|
+
'RATE_LIMITER_UNAVAILABLE',
|
|
1010
|
+
'PAYLOAD_TOO_LARGE',
|
|
1011
|
+
'BODY_TOO_LARGE',
|
|
1012
|
+
'MISSING_BODY',
|
|
1013
|
+
'INVALID_JSON',
|
|
1014
|
+
'INVALID_UNICODE',
|
|
1015
|
+
'INVALID_LIMIT',
|
|
1016
|
+
'INVALID_RELAY_LIMIT',
|
|
1017
|
+
'INVALID_TIMEOUT',
|
|
1018
|
+
'INVALID_WAIT',
|
|
1019
|
+
'MISSING_ENVELOPE_ID',
|
|
1020
|
+
'UNSUPPORTED_METHOD',
|
|
1021
|
+
'NOT_FOUND',
|
|
1022
|
+
'WEBSOCKET_UPGRADE_REQUIRED',
|
|
1023
|
+
'INVALID_REQUEST',
|
|
1024
|
+
'INVALID_RELAY_SNAPSHOT',
|
|
1025
|
+
'RELAY_INTERNAL_ERROR',
|
|
1026
|
+
];
|
|
1027
|
+
const KNOWN_RELAY_ERROR_CODE_SET = new Set(KNOWN_RELAY_ERROR_CODES);
|
|
1028
|
+
/** HTTP status for each public code. Anything else is 500 RELAY_INTERNAL_ERROR. */
|
|
1029
|
+
const RELAY_ERROR_STATUS = {
|
|
1030
|
+
INVALID_ENVELOPE: 400,
|
|
1031
|
+
INVALID_SIGNED_ENVELOPE: 400,
|
|
1032
|
+
ENVELOPE_EXPIRED: 400,
|
|
1033
|
+
REPLAYED_ENVELOPE: 400,
|
|
1034
|
+
DUPLICATE_ENVELOPE_ID: 400,
|
|
1035
|
+
ENVELOPE_TOO_LARGE: 400,
|
|
1036
|
+
RELAY_CAPACITY: 400,
|
|
1037
|
+
RELAY_QUEUE_FULL: 400,
|
|
1038
|
+
INVALID_RECIPIENT_DID: 400,
|
|
1039
|
+
MISSING_RECIPIENT_DID: 400,
|
|
1040
|
+
INVALID_DID: 400,
|
|
1041
|
+
RELAY_TAMPERING_DETECTED: 400,
|
|
1042
|
+
INVALID_AGENT_CARD: 400,
|
|
1043
|
+
INVALID_AGENT_SNAPSHOT: 400,
|
|
1044
|
+
AMBIGUOUS_AGENT_CARD_OWNER: 409,
|
|
1045
|
+
AGENT_CARD_EXPIRED: 400,
|
|
1046
|
+
AGENT_CARD_EXPIRY_TOO_LONG: 400,
|
|
1047
|
+
AGENT_CARD_DELEGATION_REQUIRED: 400,
|
|
1048
|
+
AGENT_DELEGATION_REQUIRED: 400,
|
|
1049
|
+
AGENT_CARD_DELEGATION_INVALID: 400,
|
|
1050
|
+
AGENT_CARD_TOO_LARGE: 400,
|
|
1051
|
+
AGENT_REGISTRY_FULL: 400,
|
|
1052
|
+
AGENT_CARD_TTL_INVALID: 400,
|
|
1053
|
+
AGENT_OWNER_OR_VERSION_MISMATCH: 400,
|
|
1054
|
+
INVALID_RELAY_ORIGIN: 400,
|
|
1055
|
+
PAYLOAD_TOO_LARGE: 400,
|
|
1056
|
+
BODY_TOO_LARGE: 400,
|
|
1057
|
+
MISSING_BODY: 400,
|
|
1058
|
+
INVALID_JSON: 400,
|
|
1059
|
+
INVALID_UNICODE: 400,
|
|
1060
|
+
INVALID_LIMIT: 400,
|
|
1061
|
+
INVALID_RELAY_LIMIT: 400,
|
|
1062
|
+
INVALID_TIMEOUT: 400,
|
|
1063
|
+
INVALID_WAIT: 400,
|
|
1064
|
+
MISSING_ENVELOPE_ID: 400,
|
|
1065
|
+
UNSUPPORTED_METHOD: 400,
|
|
1066
|
+
WEBSOCKET_UPGRADE_REQUIRED: 400,
|
|
1067
|
+
INVALID_REQUEST: 400,
|
|
1068
|
+
INVALID_RELAY_SNAPSHOT: 400,
|
|
1069
|
+
RELAY_UNAUTHORIZED: 401,
|
|
1070
|
+
RELAY_AUTH_REQUIRED: 401,
|
|
1071
|
+
RELAY_AUTH_INVALID: 401,
|
|
1072
|
+
RELAY_AUTH_SIGNATURE_INVALID: 401,
|
|
1073
|
+
RELAY_AUTH_RECIPIENT_MISMATCH: 401,
|
|
1074
|
+
RELAY_AUTH_ACTION_MISMATCH: 401,
|
|
1075
|
+
RELAY_AUTH_EXPIRED: 401,
|
|
1076
|
+
RELAY_AUTH_FUTURE_ISSUED_AT: 401,
|
|
1077
|
+
RELAY_AUTH_REPLAYED: 401,
|
|
1078
|
+
RELAY_AUTH_NONCE_INVALID: 401,
|
|
1079
|
+
RELAY_AUTH_ORIGIN_REQUIRED: 401,
|
|
1080
|
+
RELAY_AUTH_ORIGIN_MISMATCH: 401,
|
|
1081
|
+
RELAY_QUERY_AUTH_DISABLED: 401,
|
|
1082
|
+
QUEUE_BYTES_EXCEEDED: 429,
|
|
1083
|
+
QUEUE_LIMIT_EXCEEDED: 429,
|
|
1084
|
+
RELAY_BYTE_BUDGET_EXCEEDED: 429,
|
|
1085
|
+
SENDER_QUOTA_EXCEEDED: 429,
|
|
1086
|
+
RATE_LIMITED: 429,
|
|
1087
|
+
NOT_FOUND: 404,
|
|
1088
|
+
NO_AGENT_REGISTERED: 404,
|
|
1089
|
+
RATE_LIMITER_UNAVAILABLE: 503,
|
|
1090
|
+
RELAY_INTERNAL_ERROR: 500,
|
|
1091
|
+
};
|
|
1092
|
+
if (KNOWN_RELAY_ERROR_CODE_SET.size !== KNOWN_RELAY_ERROR_CODES.length ||
|
|
1093
|
+
KNOWN_RELAY_ERROR_CODES.some((code) => RELAY_ERROR_STATUS[code] === undefined) ||
|
|
1094
|
+
Object.keys(RELAY_ERROR_STATUS).some((code) => !KNOWN_RELAY_ERROR_CODE_SET.has(code)))
|
|
1095
|
+
throw new Error('RELAY_ERROR_CATALOG_MISMATCH');
|
|
1096
|
+
function malformedClientCode(error) {
|
|
1097
|
+
if (error instanceof SyntaxError || error instanceof URIError)
|
|
1098
|
+
return 'INVALID_REQUEST';
|
|
1099
|
+
if (error instanceof TypeError && error.message === 'Invalid URL')
|
|
1100
|
+
return 'INVALID_REQUEST';
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Public error code for an untrusted caller.
|
|
1105
|
+
* Parser and URL failures become INVALID_REQUEST. Any other message is returned
|
|
1106
|
+
* only when it is an allowlisted code; otherwise the result is the fallback
|
|
1107
|
+
* when that fallback is itself allowlisted, or RELAY_INTERNAL_ERROR.
|
|
1108
|
+
*/
|
|
1109
|
+
export function sanitizeRelayErrorCode(error, fallback = 'RELAY_INTERNAL_ERROR') {
|
|
1110
|
+
const raw = malformedClientCode(error) ?? (error instanceof Error ? error.message : String(error));
|
|
1111
|
+
if (KNOWN_RELAY_ERROR_CODE_SET.has(raw))
|
|
1112
|
+
return raw;
|
|
1113
|
+
return KNOWN_RELAY_ERROR_CODE_SET.has(fallback) ? fallback : 'RELAY_INTERNAL_ERROR';
|
|
1114
|
+
}
|
|
1115
|
+
/** Status and public code for one relay failure. Internal detail never leaves this function. */
|
|
1116
|
+
export function mapRelayError(error) {
|
|
1117
|
+
const code = sanitizeRelayErrorCode(error);
|
|
1118
|
+
return { status: RELAY_ERROR_STATUS[code] ?? 500, error: code };
|
|
1119
|
+
}
|
|
1120
|
+
/** Auth failures that must stay 401s with a stable error code. */
|
|
1121
|
+
export function relayAuthFailure(error) {
|
|
1122
|
+
if (!(error instanceof Error))
|
|
1123
|
+
return undefined;
|
|
1124
|
+
const code = sanitizeRelayErrorCode(error);
|
|
1125
|
+
return RELAY_ERROR_STATUS[code] === 401 ? code : undefined;
|
|
1126
|
+
}
|
|
1127
|
+
export function relayErrorResponse(error, relay) {
|
|
1128
|
+
const mapped = mapRelayError(error);
|
|
1129
|
+
if (mapped.status === 429) {
|
|
1130
|
+
return Response.json({ error: mapped.error }, {
|
|
1131
|
+
status: 429,
|
|
1132
|
+
headers: {
|
|
1133
|
+
...relayHttpHeaders('json'),
|
|
1134
|
+
'retry-after': String(relay?.retryAfterSeconds() ?? 60),
|
|
1135
|
+
},
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
return relayJson({ error: mapped.error }, mapped.status);
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Bearer credential for a relay request.
|
|
1142
|
+
* `?auth=` is ignored unless `allowQueryAuth` is set, so tokens do not land in
|
|
1143
|
+
* access logs, proxy logs, or Referer headers by default.
|
|
1144
|
+
*/
|
|
1145
|
+
export function extractRelayAuthToken(request, options = {}, url = new URL(request.url)) {
|
|
1146
|
+
if (url.searchParams.has('auth') && options.allowQueryAuth !== true)
|
|
1147
|
+
throw new Error('RELAY_QUERY_AUTH_DISABLED');
|
|
1148
|
+
return (request.headers.get('authorization')?.match(/^Bearer (\S+)$/i)?.[1] ??
|
|
1149
|
+
request.headers.get('x-acp-relay-auth') ??
|
|
1150
|
+
(options.allowQueryAuth === true ? (url.searchParams.get('auth') ?? undefined) : undefined));
|
|
1151
|
+
}
|
|
1152
|
+
export function relayRequestToken(request, url = new URL(request.url), options) {
|
|
1153
|
+
return extractRelayAuthToken(request, options, url);
|
|
1154
|
+
}
|
|
1155
|
+
export function relayAuthContext(request, relay, options = {}) {
|
|
1156
|
+
const requireOrigin = options.requireOrigin ??
|
|
1157
|
+
!(options.allowUnboundRelayAuth === true || relay.allowUnboundRelayAuth);
|
|
1158
|
+
return { expectedOrigin: new URL(request.url).origin, requireOrigin };
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Authenticate a long-poll and pop envelopes that are already queued.
|
|
1162
|
+
* The caller waits outside the durable-object lock when the inbox is empty.
|
|
1163
|
+
*/
|
|
1164
|
+
export function beginDurableLongPoll(relay, request, auth = {}) {
|
|
1165
|
+
try {
|
|
1166
|
+
const url = new URL(request.url);
|
|
1167
|
+
const path = canonicalRelayPath(url.pathname);
|
|
1168
|
+
const recipient = inboxRecipientFromRequest(path, url) ?? '';
|
|
1169
|
+
resolveDid(recipient);
|
|
1170
|
+
const ms = Number(url.searchParams.get('waitMs'));
|
|
1171
|
+
if (!url.searchParams.has('waitMs') || !Number.isFinite(ms) || ms < 0)
|
|
1172
|
+
throw new Error('INVALID_WAIT');
|
|
1173
|
+
const filter = parseReceiveFilterFromUrl(url);
|
|
1174
|
+
const token = extractRelayAuthToken(request, auth, url);
|
|
1175
|
+
// A request that is already aborted must not authenticate or pop.
|
|
1176
|
+
if (request.signal.aborted)
|
|
1177
|
+
return { mode: 'response', response: relayJson({ envelopes: [] }) };
|
|
1178
|
+
const receive = relay.openInbox(recipient, token, 'receive', relayAuthContext(request, relay, auth), filter);
|
|
1179
|
+
if (request.signal.aborted)
|
|
1180
|
+
return { mode: 'response', response: relayJson({ envelopes: [] }) };
|
|
1181
|
+
const buffered = [];
|
|
1182
|
+
let envelope;
|
|
1183
|
+
while ((envelope = receive()))
|
|
1184
|
+
buffered.push(envelope);
|
|
1185
|
+
if (buffered.length)
|
|
1186
|
+
return { mode: 'response', response: relayJson({ envelopes: buffered }) };
|
|
1187
|
+
return { mode: 'wait', recipient, waitMs: Math.min(ms, 30000), filter, receive };
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
return { mode: 'response', response: relayErrorResponse(error, relay) };
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
export function createRelayFetchHandler(options = {}) {
|
|
1194
|
+
const relay = options.relay ?? new InMemoryRelay();
|
|
1195
|
+
const authOptions = {
|
|
1196
|
+
allowQueryAuth: options.allowQueryAuth,
|
|
1197
|
+
requireOrigin: options.requireOrigin,
|
|
1198
|
+
allowUnboundRelayAuth: options.allowUnboundRelayAuth,
|
|
1199
|
+
};
|
|
1200
|
+
const json = (value, status = 200) => Response.json(value, { status, headers: relayHttpHeaders('json') });
|
|
1201
|
+
const empty = (status) => new Response(null, { status, headers: relayHttpHeaders('empty') });
|
|
1202
|
+
async function body(request) {
|
|
1203
|
+
const limit = relay.maxEnvelopeBytes;
|
|
1204
|
+
if (Number(request.headers.get('content-length')) > limit)
|
|
1205
|
+
throw new Error('BODY_TOO_LARGE');
|
|
1206
|
+
const reader = request.body?.getReader();
|
|
1207
|
+
if (!reader)
|
|
1208
|
+
throw new Error('MISSING_BODY');
|
|
1209
|
+
const chunks = [];
|
|
1210
|
+
let size = 0;
|
|
1211
|
+
try {
|
|
1212
|
+
while (true) {
|
|
1213
|
+
const { value, done } = await reader.read();
|
|
1214
|
+
if (done)
|
|
1215
|
+
break;
|
|
1216
|
+
size += value.byteLength;
|
|
1217
|
+
if (size > limit) {
|
|
1218
|
+
await reader.cancel();
|
|
1219
|
+
throw new Error('BODY_TOO_LARGE');
|
|
1220
|
+
}
|
|
1221
|
+
chunks.push(value);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
finally {
|
|
1225
|
+
reader.releaseLock();
|
|
1226
|
+
}
|
|
1227
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1228
|
+
}
|
|
1229
|
+
return async (request) => {
|
|
1230
|
+
try {
|
|
1231
|
+
relay.evictExpired();
|
|
1232
|
+
const url = new URL(request.url), path = canonicalRelayPath(url.pathname);
|
|
1233
|
+
if (request.method === 'GET' && path === '/health')
|
|
1234
|
+
return json({ status: 'ok' });
|
|
1235
|
+
if (request.method === 'POST' && path === '/agents') {
|
|
1236
|
+
relay.register((await body(request)));
|
|
1237
|
+
return json({ registered: true }, 201);
|
|
1238
|
+
}
|
|
1239
|
+
if (request.method === 'GET' && path === '/agents') {
|
|
1240
|
+
const ownerDid = url.searchParams.get('ownerDid');
|
|
1241
|
+
const agentDid = url.searchParams.get('agentDid');
|
|
1242
|
+
let cards = [...relay.agents.values()];
|
|
1243
|
+
if (ownerDid !== null)
|
|
1244
|
+
cards = cards.filter((card) => card.payload.ownerDid === ownerDid);
|
|
1245
|
+
if (agentDid !== null)
|
|
1246
|
+
cards = cards.filter((card) => card.payload.agentDid === agentDid);
|
|
1247
|
+
return json(cards);
|
|
1248
|
+
}
|
|
1249
|
+
if (request.method === 'GET' && path.startsWith('/agents/')) {
|
|
1250
|
+
const segments = path
|
|
1251
|
+
.slice('/agents/'.length)
|
|
1252
|
+
.split('/')
|
|
1253
|
+
.filter((segment) => segment.length > 0)
|
|
1254
|
+
.map((segment) => decodeURIComponent(segment));
|
|
1255
|
+
const card = segments.length === 2
|
|
1256
|
+
? relay.agents.get(agentRegistryKey(segments[0], segments[1]))
|
|
1257
|
+
: segments.length === 1
|
|
1258
|
+
? selectAgentCard(relay.agents.values(), segments[0])
|
|
1259
|
+
: undefined;
|
|
1260
|
+
return card ? json(card) : json({ error: 'NOT_FOUND' }, 404);
|
|
1261
|
+
}
|
|
1262
|
+
if (request.method === 'GET' && path === '/.well-known/agent.json') {
|
|
1263
|
+
const card = options.agentCard ?? relay.agents.values().next().value;
|
|
1264
|
+
return card ? json(toA2AAgentCard(card)) : json({ error: 'NO_AGENT_REGISTERED' }, 404);
|
|
1265
|
+
}
|
|
1266
|
+
if (request.method === 'POST' && path === '/wake') {
|
|
1267
|
+
const recipient = url.searchParams.get('recipient') ?? '';
|
|
1268
|
+
resolveDid(recipient);
|
|
1269
|
+
const value = await body(request);
|
|
1270
|
+
if (value.channel !== null && typeof value.channel !== 'string')
|
|
1271
|
+
throw new Error('INVALID_REQUEST');
|
|
1272
|
+
relay.registerWake(recipient, value.channel, extractRelayAuthToken(request, authOptions, url), relayAuthContext(request, relay, authOptions));
|
|
1273
|
+
return json({ registered: value.channel !== null, relayDid: relay.notificationDid });
|
|
1274
|
+
}
|
|
1275
|
+
if (request.method === 'GET' && path === '/delivery-status') {
|
|
1276
|
+
const recipient = url.searchParams.get('recipient') ?? '';
|
|
1277
|
+
const sender = url.searchParams.get('sender') ?? '';
|
|
1278
|
+
resolveDid(recipient);
|
|
1279
|
+
resolveDid(sender);
|
|
1280
|
+
const id = url.searchParams.get('envelopeId') ?? '';
|
|
1281
|
+
if (!id)
|
|
1282
|
+
throw new Error('MISSING_ENVELOPE_ID');
|
|
1283
|
+
return json(relay.getDeliveryStatus(recipient, id, sender, url.origin, extractRelayAuthToken(request, authOptions, url), relayAuthContext(request, relay, authOptions)));
|
|
1284
|
+
}
|
|
1285
|
+
if (request.method === 'POST' && path === '/envelopes') {
|
|
1286
|
+
relay.send((await body(request)));
|
|
1287
|
+
if (!options.deferNotifications)
|
|
1288
|
+
void relay.flushWakeNotifications().catch(() => { });
|
|
1289
|
+
return new Response(null, { status: 202, headers: { ...relayHttpHeaders('empty'),
|
|
1290
|
+
'x-acp-relay-did': relay.notificationDid } });
|
|
1291
|
+
}
|
|
1292
|
+
if (request.method === 'DELETE' &&
|
|
1293
|
+
(path === '/envelopes' || path.startsWith('/envelopes/'))) {
|
|
1294
|
+
const envelopeId = path.startsWith('/envelopes/')
|
|
1295
|
+
? decodeURIComponent(path.slice('/envelopes/'.length))
|
|
1296
|
+
: (url.searchParams.get('id') ?? '');
|
|
1297
|
+
if (!envelopeId)
|
|
1298
|
+
throw new Error('MISSING_ENVELOPE_ID');
|
|
1299
|
+
const recipient = url.searchParams.get('recipient') ?? '';
|
|
1300
|
+
resolveDid(recipient);
|
|
1301
|
+
const removed = relay.acknowledge(recipient, envelopeId, extractRelayAuthToken(request, authOptions, url), relayAuthContext(request, relay, authOptions));
|
|
1302
|
+
return removed ? empty(204) : json({ error: 'NOT_FOUND' }, 404);
|
|
1303
|
+
}
|
|
1304
|
+
const inboxRecipient = request.method === 'GET' ? inboxRecipientFromRequest(path, url) : undefined;
|
|
1305
|
+
if (request.method === 'GET' && inboxRecipient !== undefined) {
|
|
1306
|
+
const recipient = inboxRecipient;
|
|
1307
|
+
resolveDid(recipient);
|
|
1308
|
+
const ms = url.searchParams.has('waitMs')
|
|
1309
|
+
? Number(url.searchParams.get('waitMs'))
|
|
1310
|
+
: undefined;
|
|
1311
|
+
if (ms !== undefined && (!Number.isFinite(ms) || ms < 0))
|
|
1312
|
+
throw new Error('INVALID_WAIT');
|
|
1313
|
+
const filter = parseReceiveFilterFromUrl(url);
|
|
1314
|
+
const token = extractRelayAuthToken(request, authOptions, url);
|
|
1315
|
+
const receive = relay.openInbox(recipient, token, path === '/stream' ? 'stream' : 'receive', relayAuthContext(request, relay, authOptions), filter);
|
|
1316
|
+
if (path === '/stream') {
|
|
1317
|
+
let stop = () => { };
|
|
1318
|
+
let flush = () => { };
|
|
1319
|
+
// One queued chunk keeps an unread stream from draining the relay queue.
|
|
1320
|
+
const stream = new ReadableStream({
|
|
1321
|
+
start(controller) {
|
|
1322
|
+
const encoder = new TextEncoder();
|
|
1323
|
+
let closed = false;
|
|
1324
|
+
const close = () => {
|
|
1325
|
+
if (closed)
|
|
1326
|
+
return;
|
|
1327
|
+
closed = true;
|
|
1328
|
+
off();
|
|
1329
|
+
clearInterval(heartbeat);
|
|
1330
|
+
request.signal.removeEventListener('abort', close);
|
|
1331
|
+
try {
|
|
1332
|
+
controller.close();
|
|
1333
|
+
}
|
|
1334
|
+
catch {
|
|
1335
|
+
/* Stream cancellation already closed the controller. */
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
flush = () => {
|
|
1339
|
+
if (closed)
|
|
1340
|
+
return;
|
|
1341
|
+
try {
|
|
1342
|
+
let envelope;
|
|
1343
|
+
// Leave envelopes in the bounded recipient queue until the client pulls.
|
|
1344
|
+
while (controller.desiredSize !== null &&
|
|
1345
|
+
controller.desiredSize > 0 &&
|
|
1346
|
+
(envelope = receive()))
|
|
1347
|
+
controller.enqueue(encoder.encode('data: ' + JSON.stringify(envelope) + '\n\n'));
|
|
1348
|
+
}
|
|
1349
|
+
catch {
|
|
1350
|
+
close();
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
const off = relay.subscribe(recipient, flush);
|
|
1354
|
+
const heartbeat = setInterval(() => {
|
|
1355
|
+
if (!closed && controller.desiredSize !== null && controller.desiredSize > 0)
|
|
1356
|
+
controller.enqueue(encoder.encode(': heartbeat\n\n'));
|
|
1357
|
+
}, 15000);
|
|
1358
|
+
stop = close;
|
|
1359
|
+
request.signal.addEventListener('abort', close, { once: true });
|
|
1360
|
+
controller.enqueue(encoder.encode(': connected\n\n'));
|
|
1361
|
+
flush();
|
|
1362
|
+
if (request.signal.aborted)
|
|
1363
|
+
close();
|
|
1364
|
+
},
|
|
1365
|
+
pull() {
|
|
1366
|
+
flush();
|
|
1367
|
+
},
|
|
1368
|
+
cancel() {
|
|
1369
|
+
stop();
|
|
1370
|
+
},
|
|
1371
|
+
}, { highWaterMark: 1 });
|
|
1372
|
+
return new Response(stream, { headers: relayHttpHeaders('sse') });
|
|
1373
|
+
}
|
|
1374
|
+
if (!url.searchParams.has('waitMs')) {
|
|
1375
|
+
const e = receive();
|
|
1376
|
+
return e ? json(e) : empty(204);
|
|
1377
|
+
}
|
|
1378
|
+
// An already-aborted poll must not pop envelopes that are still queued.
|
|
1379
|
+
if (request.signal.aborted)
|
|
1380
|
+
return json({ envelopes: [] });
|
|
1381
|
+
const ready = [];
|
|
1382
|
+
let queued;
|
|
1383
|
+
while ((queued = receive()))
|
|
1384
|
+
ready.push(queued);
|
|
1385
|
+
if (!ready.length) {
|
|
1386
|
+
await relay.wait(recipient, Math.min(ms, 30000), request.signal, filter);
|
|
1387
|
+
if (request.signal.aborted)
|
|
1388
|
+
return json({ envelopes: [] });
|
|
1389
|
+
while ((queued = receive()))
|
|
1390
|
+
ready.push(queued);
|
|
1391
|
+
}
|
|
1392
|
+
return json({ envelopes: ready });
|
|
1393
|
+
}
|
|
1394
|
+
return json({ error: 'NOT_FOUND' }, 404);
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
return relayErrorResponse(error, relay);
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
//# sourceMappingURL=fetch.js.map
|