agent-protocols 0.10.0 → 0.11.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/README.md +117 -2
- package/dist/discourse.d.ts.map +1 -1
- package/dist/discourse.js +5 -5
- package/dist/discourse.js.map +1 -1
- package/dist/encoding.d.ts +8 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +47 -0
- package/dist/encoding.js.map +1 -0
- package/dist/identity.d.ts +14 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +87 -13
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +5 -4
- package/dist/knowledge.js.map +1 -1
- package/dist/local-connector/engine.d.ts.map +1 -1
- package/dist/local-connector/engine.js +1 -3
- package/dist/local-connector/engine.js.map +1 -1
- package/dist/mail-client.d.ts +38 -0
- package/dist/mail-client.d.ts.map +1 -0
- package/dist/mail-client.js +176 -0
- package/dist/mail-client.js.map +1 -0
- package/dist/mail-schema.d.ts +353 -0
- package/dist/mail-schema.d.ts.map +1 -0
- package/dist/mail-schema.js +369 -0
- package/dist/mail-schema.js.map +1 -0
- package/dist/mail.d.ts +232 -0
- package/dist/mail.d.ts.map +1 -0
- package/dist/mail.js +752 -0
- package/dist/mail.js.map +1 -0
- package/package.json +14 -2
package/dist/mail.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/** Agent Mail 1.0. State helpers are process-local; persist snapshots before acknowledging delivery. */
|
|
2
|
+
import { CipherSuite, HkdfSha256 } from "@hpke/core";
|
|
3
|
+
import { DhkemX25519HkdfSha256 } from "@hpke/dhkem-x25519";
|
|
4
|
+
import { Chacha20Poly1305 } from "@hpke/chacha20poly1305";
|
|
5
|
+
import { sha3_256 } from "@noble/hashes/sha3.js";
|
|
6
|
+
import { Validator } from "@cfworker/json-schema";
|
|
7
|
+
import canonicalize from "canonicalize";
|
|
8
|
+
import nacl from "tweetnacl";
|
|
9
|
+
import { base64UrlDecodeCanonical, base64UrlEncode } from "./encoding.js";
|
|
10
|
+
import { protocolError } from "./errors.js";
|
|
11
|
+
import { MemoryNonceStore, createEvent, formatAgentUrl, parseAgentUrl, parseStrictJson, validateAgentId, validateOrigin, verifyEnvelope, verifyRequestJwt, verifyTimestamp, } from "./identity.js";
|
|
12
|
+
import { MAIL_SCHEMA } from "./mail-schema.js";
|
|
13
|
+
export { MAIL_SCHEMA } from "./mail-schema.js";
|
|
14
|
+
export const MAIL_PROTOCOL = "agent-mail/1.0";
|
|
15
|
+
export const MAIL_MAX_TTL_MS = 30 * 86400000;
|
|
16
|
+
export const MAIL_FUTURE_SKEW_MS = 300000;
|
|
17
|
+
export const MAIL_MAX_PACKET_BYTES = 1048576;
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
const INFO = encoder.encode(MAIL_PROTOCOL);
|
|
20
|
+
const clone = (v) => structuredClone(v);
|
|
21
|
+
const validators = new Map();
|
|
22
|
+
const LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
|
|
23
|
+
function requireMail(ok, code, message = code) {
|
|
24
|
+
if (!ok)
|
|
25
|
+
throw protocolError(code, message);
|
|
26
|
+
}
|
|
27
|
+
function safeTime(now) {
|
|
28
|
+
requireMail(Number.isSafeInteger(now) && now >= 0, "invalid_request", "clock must be nonnegative safe milliseconds");
|
|
29
|
+
}
|
|
30
|
+
function strictString(value) {
|
|
31
|
+
requireMail(!LONE_SURROGATE.test(value), "invalid_event", "lone surrogate");
|
|
32
|
+
}
|
|
33
|
+
function jsonValue(value, seen = new Set()) {
|
|
34
|
+
if (value === null || typeof value === "boolean")
|
|
35
|
+
return;
|
|
36
|
+
if (typeof value === "string")
|
|
37
|
+
return strictString(value);
|
|
38
|
+
if (typeof value === "number") {
|
|
39
|
+
requireMail(Number.isFinite(value) &&
|
|
40
|
+
(!Number.isInteger(value) || Number.isSafeInteger(value)), "invalid_event", "invalid JSON number");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
requireMail(typeof value === "object", "invalid_event", "non-JSON value");
|
|
44
|
+
requireMail(!seen.has(value), "invalid_event", "cyclic JSON");
|
|
45
|
+
requireMail(Array.isArray(value) ||
|
|
46
|
+
Object.getPrototypeOf(value) === Object.prototype ||
|
|
47
|
+
Object.getPrototypeOf(value) === null, "invalid_event", "expected plain JSON object");
|
|
48
|
+
seen.add(value);
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
for (const child of value)
|
|
51
|
+
jsonValue(child, seen);
|
|
52
|
+
else
|
|
53
|
+
for (const [key, child] of Object.entries(value)) {
|
|
54
|
+
strictString(key);
|
|
55
|
+
requireMail(child !== undefined, "invalid_event", "undefined member");
|
|
56
|
+
jsonValue(child, seen);
|
|
57
|
+
}
|
|
58
|
+
seen.delete(value);
|
|
59
|
+
}
|
|
60
|
+
/** Canonical, strict I-JSON bytes. This never silently drops undefined members. */
|
|
61
|
+
export function mailCanonicalBytes(value) {
|
|
62
|
+
jsonValue(value);
|
|
63
|
+
return encoder.encode(canonicalize(value));
|
|
64
|
+
}
|
|
65
|
+
export function validateMailSchema(value, definition = "envelope") {
|
|
66
|
+
jsonValue(value);
|
|
67
|
+
let validator = validators.get(definition);
|
|
68
|
+
if (!validator) {
|
|
69
|
+
requireMail(Object.hasOwn(MAIL_SCHEMA.$defs, definition), "invalid_request", "unknown Mail schema definition");
|
|
70
|
+
validator = new Validator({
|
|
71
|
+
$ref: `#/$defs/${definition}`,
|
|
72
|
+
$defs: MAIL_SCHEMA.$defs,
|
|
73
|
+
}, "2020-12");
|
|
74
|
+
validators.set(definition, validator);
|
|
75
|
+
}
|
|
76
|
+
requireMail(validator.validate(value).valid, "invalid_event", `${definition} schema violation`);
|
|
77
|
+
}
|
|
78
|
+
export function decodeMailBytes(value, length) {
|
|
79
|
+
requireMail(typeof value === "string", "invalid_encoding");
|
|
80
|
+
const bytes = base64UrlDecodeCanonical(value);
|
|
81
|
+
requireMail(bytes !== undefined && (length === undefined || bytes.length === length), "invalid_encoding", "invalid canonical base64url or byte length");
|
|
82
|
+
return bytes;
|
|
83
|
+
}
|
|
84
|
+
function pathId(value, length) {
|
|
85
|
+
const bytes = typeof value === "string" ? base64UrlDecodeCanonical(value) : undefined;
|
|
86
|
+
requireMail(bytes?.length === length, "invalid_request", "invalid path ID");
|
|
87
|
+
}
|
|
88
|
+
export function newMailId() {
|
|
89
|
+
return base64UrlEncode(nacl.randomBytes(16));
|
|
90
|
+
}
|
|
91
|
+
/** Parse `did:agent:<key>/mail/<mailbox_id>[?route=<origin>...]`. */
|
|
92
|
+
export function parseMailAddress(value) {
|
|
93
|
+
const url = parseAgentUrl(value);
|
|
94
|
+
requireMail(url.protocol === "mail" &&
|
|
95
|
+
url.resource !== undefined &&
|
|
96
|
+
base64UrlDecodeCanonical(url.resource)?.length === 16, "invalid_url", "not a mailbox address");
|
|
97
|
+
return { owner: url.agent_id, mailbox_id: url.resource, routes: url.routes };
|
|
98
|
+
}
|
|
99
|
+
/** Format the stable address, or a contact address when routes are given. */
|
|
100
|
+
export function formatMailAddress(owner, mailboxId, routes = []) {
|
|
101
|
+
requireMail(base64UrlDecodeCanonical(mailboxId)?.length === 16, "invalid_url", "mailbox_id must be an id16");
|
|
102
|
+
return formatAgentUrl({
|
|
103
|
+
agent_id: owner,
|
|
104
|
+
protocol: "mail",
|
|
105
|
+
resource: mailboxId,
|
|
106
|
+
routes,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function mailTextPart(text, name) {
|
|
110
|
+
strictString(text);
|
|
111
|
+
return {
|
|
112
|
+
media_type: "text/plain",
|
|
113
|
+
data: base64UrlEncode(encoder.encode(text)),
|
|
114
|
+
...(name === undefined ? {} : { name }),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Returns inert text only; does not render markup or perform network/file actions. */
|
|
118
|
+
export function mailPartText(part) {
|
|
119
|
+
validateMailSchema(part, "part");
|
|
120
|
+
requireMail(part.media_type.startsWith("text/"), "invalid_event", "part is not text");
|
|
121
|
+
try {
|
|
122
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decodeMailBytes(part.data));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw protocolError("invalid_event", "invalid UTF-8 text part");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function lifetime(created, expires) {
|
|
129
|
+
requireMail(created < expires && expires - created <= MAIL_MAX_TTL_MS, "invalid_event", "invalid lifetime");
|
|
130
|
+
}
|
|
131
|
+
function usableX25519(publicKey) {
|
|
132
|
+
const key = decodeMailBytes(publicKey, 32);
|
|
133
|
+
// A public, disposable scalar detects all low-order peers; no shared secret is used for encryption.
|
|
134
|
+
requireMail(nacl.scalarMult(new Uint8Array(32).fill(42), key).some((x) => x !== 0), "invalid_event", "unusable X25519 public key");
|
|
135
|
+
}
|
|
136
|
+
/** Historical verification: shape, signature, owner, intrinsic times, routes, and key. */
|
|
137
|
+
export function validateMailboxCard(card, owner) {
|
|
138
|
+
validateMailSchema(card, "mailboxCardEnvelope");
|
|
139
|
+
const c = card;
|
|
140
|
+
verifyEnvelope(c);
|
|
141
|
+
if (owner !== undefined)
|
|
142
|
+
requireMail(c.event.actor === owner, "invalid_actor", "card owner mismatch");
|
|
143
|
+
const p = c.event.payload;
|
|
144
|
+
lifetime(c.event.created_at, p.expires_at);
|
|
145
|
+
requireMail(p.expires_at <= p.receive_until &&
|
|
146
|
+
p.receive_until - p.expires_at <= MAIL_MAX_TTL_MS, "invalid_event", "invalid receive_until");
|
|
147
|
+
p.routes.forEach(validateOrigin);
|
|
148
|
+
usableX25519(p.public_key);
|
|
149
|
+
}
|
|
150
|
+
/** A card usable for new encryption: not future-dated, open (non-empty routes), unexpired. */
|
|
151
|
+
export function validateMailSenderCard(card, owner, now = Date.now()) {
|
|
152
|
+
safeTime(now);
|
|
153
|
+
validateMailboxCard(card, owner);
|
|
154
|
+
const c = card;
|
|
155
|
+
requireMail(c.event.created_at <= now + MAIL_FUTURE_SKEW_MS, "timestamp_out_of_window", "card is future-dated");
|
|
156
|
+
requireMail(c.event.payload.routes.length > 0, "mailbox_unavailable", "mailbox closed");
|
|
157
|
+
requireMail(now < c.event.payload.expires_at, "stale_card", "card expired");
|
|
158
|
+
}
|
|
159
|
+
/** Historical verification of a signed letter; never advances live nonce state. */
|
|
160
|
+
export function validateMailLetter(letter) {
|
|
161
|
+
validateMailSchema(letter, "messageEnvelope");
|
|
162
|
+
const l = letter;
|
|
163
|
+
verifyEnvelope(l);
|
|
164
|
+
const p = l.event.payload;
|
|
165
|
+
lifetime(l.event.created_at, p.expires_at);
|
|
166
|
+
for (const part of p.parts) {
|
|
167
|
+
decodeMailBytes(part.data);
|
|
168
|
+
if (part.media_type.startsWith("text/"))
|
|
169
|
+
mailPartText(part);
|
|
170
|
+
}
|
|
171
|
+
if (p.reply_card !== undefined)
|
|
172
|
+
validateMailboxCard(p.reply_card, l.event.actor);
|
|
173
|
+
}
|
|
174
|
+
export function mailboxPublishEvent(actor, createdAt, nonce, payload) {
|
|
175
|
+
return createEvent(MAIL_PROTOCOL, "mailbox.publish", actor, createdAt, nonce, clone(payload));
|
|
176
|
+
}
|
|
177
|
+
export function mailMessageEvent(actor, createdAt, nonce, payload) {
|
|
178
|
+
return createEvent(MAIL_PROTOCOL, "mail.message", actor, createdAt, nonce, clone(payload));
|
|
179
|
+
}
|
|
180
|
+
/** Bind a reply to a known parent: participants, parent letter ID, and thread. */
|
|
181
|
+
export function validateMailReply(reply, parent) {
|
|
182
|
+
validateMailLetter(reply);
|
|
183
|
+
validateMailLetter(parent);
|
|
184
|
+
requireMail(reply.event.payload.in_reply_to === parent.hash &&
|
|
185
|
+
reply.event.payload.thread_id === parent.event.payload.thread_id &&
|
|
186
|
+
reply.event.actor === parent.event.payload.to &&
|
|
187
|
+
reply.event.payload.to === parent.event.actor, "invalid_event", "reply participants/thread binding mismatch");
|
|
188
|
+
}
|
|
189
|
+
/** Validate a packet and return its canonical bytes, computed once for ID and size checks. */
|
|
190
|
+
function packetBytes(packet, expectedId) {
|
|
191
|
+
validateMailSchema(packet, "packet");
|
|
192
|
+
const p = packet;
|
|
193
|
+
decodeMailBytes(p.enc, 32);
|
|
194
|
+
const ct = decodeMailBytes(p.ciphertext);
|
|
195
|
+
requireMail(ct.length >= 1040 && ct.length % 1024 === 16, "invalid_packet", "invalid ciphertext length");
|
|
196
|
+
const bytes = mailCanonicalBytes(p);
|
|
197
|
+
requireMail(bytes.length <= MAIL_MAX_PACKET_BYTES, "payload_too_large", "packet exceeds 1 MiB");
|
|
198
|
+
if (expectedId !== undefined)
|
|
199
|
+
requireMail(base64UrlEncode(sha3_256(bytes)) === expectedId, "invalid_packet", "packet ID mismatch");
|
|
200
|
+
return bytes;
|
|
201
|
+
}
|
|
202
|
+
export function validateMailPacket(packet, expectedId) {
|
|
203
|
+
packetBytes(packet, expectedId);
|
|
204
|
+
}
|
|
205
|
+
export function mailPacketId(packet) {
|
|
206
|
+
return base64UrlEncode(sha3_256(mailCanonicalBytes(packet)));
|
|
207
|
+
}
|
|
208
|
+
export function frameMailBytes(bytes) {
|
|
209
|
+
requireMail(bytes.length > 0 && bytes.length <= MAIL_MAX_PACKET_BYTES, "invalid_packet", "invalid frame input length");
|
|
210
|
+
const frame = new Uint8Array(1024 * Math.ceil((4 + bytes.length) / 1024));
|
|
211
|
+
new DataView(frame.buffer).setUint32(0, bytes.length, false);
|
|
212
|
+
frame.set(bytes, 4);
|
|
213
|
+
return frame;
|
|
214
|
+
}
|
|
215
|
+
export function unframeMailLetter(frame) {
|
|
216
|
+
requireMail(frame.length >= 1024 &&
|
|
217
|
+
frame.length <= MAIL_MAX_PACKET_BYTES &&
|
|
218
|
+
frame.length % 1024 === 0, "invalid_packet", "invalid frame length");
|
|
219
|
+
const n = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(0, false);
|
|
220
|
+
requireMail(n > 0 &&
|
|
221
|
+
n <= frame.length - 4 &&
|
|
222
|
+
frame.length === 1024 * Math.ceil((4 + n) / 1024), "invalid_packet", "invalid/nonminimal frame length");
|
|
223
|
+
requireMail(frame.subarray(4 + n).every((x) => x === 0), "invalid_packet", "nonzero padding");
|
|
224
|
+
let text;
|
|
225
|
+
try {
|
|
226
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(frame.subarray(4, 4 + n));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
throw protocolError("invalid_packet", "invalid UTF-8 frame");
|
|
230
|
+
}
|
|
231
|
+
const letter = parseStrictJson(text);
|
|
232
|
+
requireMail(canonicalize(letter) === text, "invalid_packet", "noncanonical letter JSON");
|
|
233
|
+
validateMailLetter(letter);
|
|
234
|
+
return letter;
|
|
235
|
+
}
|
|
236
|
+
const hpke = () => new CipherSuite({
|
|
237
|
+
kem: new DhkemX25519HkdfSha256(),
|
|
238
|
+
kdf: new HkdfSha256(),
|
|
239
|
+
aead: new Chacha20Poly1305(),
|
|
240
|
+
});
|
|
241
|
+
/** Independent raw X25519 secret. Never derive this from an Identity signing key. */
|
|
242
|
+
export class MailEncryptionKey {
|
|
243
|
+
secret;
|
|
244
|
+
destroyed = false;
|
|
245
|
+
constructor(secret) {
|
|
246
|
+
requireMail(secret.length === 32, "invalid_private_key");
|
|
247
|
+
this.secret = new Uint8Array(secret);
|
|
248
|
+
}
|
|
249
|
+
static generate() {
|
|
250
|
+
return new MailEncryptionKey(nacl.randomBytes(32));
|
|
251
|
+
}
|
|
252
|
+
static fromBytes(secret) {
|
|
253
|
+
return new MailEncryptionKey(secret);
|
|
254
|
+
}
|
|
255
|
+
publicKey() {
|
|
256
|
+
requireMail(!this.destroyed, "invalid_private_key", "key destroyed");
|
|
257
|
+
return base64UrlEncode(nacl.scalarMult.base(this.secret));
|
|
258
|
+
}
|
|
259
|
+
/** Sensitive backup material: store only in an owner-controlled encrypted key store. */
|
|
260
|
+
exportSecret() {
|
|
261
|
+
requireMail(!this.destroyed, "invalid_private_key", "key destroyed");
|
|
262
|
+
return new Uint8Array(this.secret);
|
|
263
|
+
}
|
|
264
|
+
destroy() {
|
|
265
|
+
this.secret.fill(0);
|
|
266
|
+
this.destroyed = true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Stateless encryption with fresh HPKE randomness. Pin the card with MailCardCache first. */
|
|
270
|
+
export async function sealMailPacket(letter, card, now = Date.now()) {
|
|
271
|
+
letter = clone(letter);
|
|
272
|
+
card = clone(card);
|
|
273
|
+
validateMailLetter(letter);
|
|
274
|
+
validateMailSenderCard(card, letter.event.payload.to, now);
|
|
275
|
+
requireMail(letter.event.created_at <= now + MAIL_FUTURE_SKEW_MS &&
|
|
276
|
+
now < letter.event.payload.expires_at, "packet_expired", "letter is expired or future-dated");
|
|
277
|
+
const c = card.event.payload;
|
|
278
|
+
requireMail(letter.event.payload.expires_at <= c.receive_until, "invalid_packet", "letter exceeds receive_until");
|
|
279
|
+
const header = {
|
|
280
|
+
protocol: MAIL_PROTOCOL,
|
|
281
|
+
mailbox_id: c.mailbox_id,
|
|
282
|
+
card_hash: card.hash,
|
|
283
|
+
expires_at: letter.event.payload.expires_at,
|
|
284
|
+
};
|
|
285
|
+
const plaintext = frameMailBytes(mailCanonicalBytes(letter));
|
|
286
|
+
// Check base64/JSON overhead before allocating an HPKE context.
|
|
287
|
+
const estimate = {
|
|
288
|
+
header,
|
|
289
|
+
enc: base64UrlEncode(new Uint8Array(32)),
|
|
290
|
+
ciphertext: base64UrlEncode(new Uint8Array(plaintext.length + 16)),
|
|
291
|
+
};
|
|
292
|
+
requireMail(mailCanonicalBytes(estimate).length <= c.max_packet_bytes, "payload_too_large", "packet exceeds card limit");
|
|
293
|
+
const suite = hpke();
|
|
294
|
+
const sender = await suite.createSenderContext({
|
|
295
|
+
recipientPublicKey: await suite.kem.deserializePublicKey(decodeMailBytes(c.public_key, 32)),
|
|
296
|
+
info: INFO,
|
|
297
|
+
});
|
|
298
|
+
try {
|
|
299
|
+
const ct = await sender.seal(plaintext, mailCanonicalBytes(header));
|
|
300
|
+
return {
|
|
301
|
+
header,
|
|
302
|
+
enc: base64UrlEncode(new Uint8Array(sender.enc)),
|
|
303
|
+
ciphertext: base64UrlEncode(new Uint8Array(ct)),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
plaintext.fill(0);
|
|
308
|
+
} // Context is never reused; JS cannot guarantee erasure of provider-owned/GC copies.
|
|
309
|
+
}
|
|
310
|
+
/** Verify/decrypt a queued letter; expiration of new acceptance is checked by MailInbox. */
|
|
311
|
+
export async function openMailPacket(packet, card, key, owner, now = Date.now(), expectedId) {
|
|
312
|
+
packet = clone(packet);
|
|
313
|
+
card = clone(card);
|
|
314
|
+
safeTime(now);
|
|
315
|
+
const bytes = packetBytes(packet, expectedId);
|
|
316
|
+
validateMailboxCard(card, owner);
|
|
317
|
+
return openVerified(packet, bytes, card, key, owner, now);
|
|
318
|
+
}
|
|
319
|
+
/** Open with a card that was already verified for `owner` (the keyring verifies at `add`). */
|
|
320
|
+
async function openVerified(packet, bytes, card, key, owner, now) {
|
|
321
|
+
const c = card.event.payload, h = packet.header;
|
|
322
|
+
requireMail(h.card_hash === card.hash && h.mailbox_id === c.mailbox_id, "invalid_packet", "card/header binding mismatch");
|
|
323
|
+
requireMail(bytes.length <= c.max_packet_bytes, "payload_too_large", "packet exceeds card limit");
|
|
324
|
+
requireMail(h.expires_at <= c.receive_until, "invalid_packet", "packet exceeds receive_until");
|
|
325
|
+
requireMail(key.publicKey() === c.public_key, "invalid_private_key", "recipient secret does not match card");
|
|
326
|
+
const secret = key.exportSecret();
|
|
327
|
+
let plaintext;
|
|
328
|
+
try {
|
|
329
|
+
const suite = hpke();
|
|
330
|
+
const ctx = await suite.createRecipientContext({
|
|
331
|
+
recipientKey: await suite.kem.deserializePrivateKey(secret),
|
|
332
|
+
enc: decodeMailBytes(packet.enc, 32),
|
|
333
|
+
info: INFO,
|
|
334
|
+
});
|
|
335
|
+
plaintext = new Uint8Array(await ctx.open(decodeMailBytes(packet.ciphertext), mailCanonicalBytes(h)));
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
throw protocolError("invalid_packet", "HPKE opening failed");
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
secret.fill(0);
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const letter = unframeMailLetter(plaintext);
|
|
345
|
+
requireMail(letter.event.payload.to === owner, "invalid_packet", "letter recipient mismatch");
|
|
346
|
+
requireMail(letter.event.payload.expires_at === h.expires_at, "invalid_packet", "letter/header expiration mismatch");
|
|
347
|
+
requireMail(letter.event.created_at <= now + MAIL_FUTURE_SKEW_MS, "timestamp_out_of_window", "future letter");
|
|
348
|
+
return letter;
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
plaintext.fill(0);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Sender-side card pins: the greatest-nonce card seen per (owner, mailbox),
|
|
356
|
+
* including closed or expired cards. Persist the snapshot after every observe,
|
|
357
|
+
* even one that throws because the new pin is unusable.
|
|
358
|
+
*/
|
|
359
|
+
export class MailCardCache {
|
|
360
|
+
pins = new Map();
|
|
361
|
+
constructor(snapshot) {
|
|
362
|
+
if (snapshot) {
|
|
363
|
+
requireMail(snapshot.version === 1, "invalid_request");
|
|
364
|
+
for (const card of snapshot.pins) {
|
|
365
|
+
validateMailSchema(card, "mailboxCardEnvelope");
|
|
366
|
+
this.pins.set(this.id(card.event.actor, card.event.payload.mailbox_id), clone(card));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
id(owner, mailbox) {
|
|
371
|
+
return owner + ":" + mailbox;
|
|
372
|
+
}
|
|
373
|
+
snapshot() {
|
|
374
|
+
return { version: 1, pins: clone([...this.pins.values()]) };
|
|
375
|
+
}
|
|
376
|
+
/** Pin a verified card, then (by default) require that it is usable for new encryption. */
|
|
377
|
+
observe(card, owner, now = Date.now(), requireUsable = true) {
|
|
378
|
+
card = clone(card);
|
|
379
|
+
safeTime(now);
|
|
380
|
+
validateMailboxCard(card, owner);
|
|
381
|
+
requireMail(card.event.created_at <= now + MAIL_FUTURE_SKEW_MS, "timestamp_out_of_window", "card is future-dated");
|
|
382
|
+
const id = this.id(owner, card.event.payload.mailbox_id), pin = this.pins.get(id);
|
|
383
|
+
if (pin && pin.hash !== card.hash)
|
|
384
|
+
requireMail(card.event.nonce > pin.event.nonce, "stale_card", "card is older than or conflicts with the pinned card");
|
|
385
|
+
this.pins.set(id, card);
|
|
386
|
+
if (requireUsable)
|
|
387
|
+
validateMailSenderCard(card, owner, now);
|
|
388
|
+
return clone(card);
|
|
389
|
+
}
|
|
390
|
+
/** Drop pins old enough that every earlier card of the mailbox has expired. */
|
|
391
|
+
prune(now = Date.now()) {
|
|
392
|
+
safeTime(now);
|
|
393
|
+
for (const [id, pin] of this.pins)
|
|
394
|
+
if (now >= pin.event.created_at + MAIL_MAX_TTL_MS + MAIL_FUTURE_SKEW_MS)
|
|
395
|
+
this.pins.delete(id);
|
|
396
|
+
}
|
|
397
|
+
async seal(letter, card, now = Date.now()) {
|
|
398
|
+
return sealMailPacket(letter, this.observe(card, letter.event.payload.to, now), now);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Retained verified cards and their secrets, looked up by card hash until receive_until. */
|
|
402
|
+
export class MailKeyring {
|
|
403
|
+
owner;
|
|
404
|
+
entries = new Map();
|
|
405
|
+
constructor(owner) {
|
|
406
|
+
this.owner = owner;
|
|
407
|
+
validateAgentId(owner);
|
|
408
|
+
}
|
|
409
|
+
add(card, key) {
|
|
410
|
+
card = clone(card);
|
|
411
|
+
validateMailboxCard(card, this.owner);
|
|
412
|
+
requireMail(key.publicKey() === card.event.payload.public_key, "invalid_private_key", "recipient secret does not match card");
|
|
413
|
+
this.entries.get(card.hash)?.key.destroy();
|
|
414
|
+
this.entries.set(card.hash, {
|
|
415
|
+
card,
|
|
416
|
+
key: MailEncryptionKey.fromBytes(key.exportSecret()),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
/** Sensitive snapshot, including raw encryption secrets. Protect it separately from logs and application data. */
|
|
420
|
+
exportSnapshot() {
|
|
421
|
+
return {
|
|
422
|
+
version: 1,
|
|
423
|
+
owner: this.owner,
|
|
424
|
+
entries: [...this.entries.values()].map((x) => ({
|
|
425
|
+
card: clone(x.card),
|
|
426
|
+
secret: base64UrlEncode(x.key.exportSecret()),
|
|
427
|
+
})),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
static restore(snapshot) {
|
|
431
|
+
requireMail(snapshot.version === 1, "invalid_request");
|
|
432
|
+
const r = new MailKeyring(snapshot.owner);
|
|
433
|
+
for (const x of snapshot.entries)
|
|
434
|
+
r.add(x.card, MailEncryptionKey.fromBytes(decodeMailBytes(x.secret, 32)));
|
|
435
|
+
return r;
|
|
436
|
+
}
|
|
437
|
+
prune(now = Date.now()) {
|
|
438
|
+
safeTime(now);
|
|
439
|
+
for (const [hash, x] of this.entries)
|
|
440
|
+
if (now >= x.card.event.payload.receive_until) {
|
|
441
|
+
x.key.destroy();
|
|
442
|
+
this.entries.delete(hash);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async open(packet, now = Date.now(), expectedId) {
|
|
446
|
+
packet = clone(packet);
|
|
447
|
+
safeTime(now);
|
|
448
|
+
const bytes = packetBytes(packet, expectedId);
|
|
449
|
+
const x = this.entries.get(packet.header.card_hash);
|
|
450
|
+
requireMail(x, "invalid_packet", "unknown retained card");
|
|
451
|
+
return openVerified(packet, bytes, x.card, x.key, this.owner, now);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Cross-route letter deduplication. The application stores accepted letters;
|
|
456
|
+
* persist the snapshot with them before deleting relay copies or acting.
|
|
457
|
+
*/
|
|
458
|
+
export class MailInbox {
|
|
459
|
+
keyring;
|
|
460
|
+
accepted = new Map();
|
|
461
|
+
constructor(keyring, snapshot) {
|
|
462
|
+
this.keyring = keyring;
|
|
463
|
+
if (snapshot) {
|
|
464
|
+
requireMail(snapshot.version === 1 && snapshot.owner === keyring.owner, "invalid_request");
|
|
465
|
+
for (const [id, expires] of snapshot.accepted)
|
|
466
|
+
this.accepted.set(id, expires);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
snapshot() {
|
|
470
|
+
return {
|
|
471
|
+
version: 1,
|
|
472
|
+
owner: this.keyring.owner,
|
|
473
|
+
accepted: [...this.accepted],
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
has(letterId) {
|
|
477
|
+
return this.accepted.has(letterId);
|
|
478
|
+
}
|
|
479
|
+
async accept(packet, now = Date.now(), expectedId) {
|
|
480
|
+
const letter = await this.keyring.open(packet, now, expectedId);
|
|
481
|
+
// No await between lookup and commit: concurrent decryptions cannot both accept.
|
|
482
|
+
if (this.accepted.has(letter.hash))
|
|
483
|
+
return { kind: "duplicate", letter };
|
|
484
|
+
requireMail(now < letter.event.payload.expires_at, "packet_expired", "expired new letter");
|
|
485
|
+
this.accepted.set(letter.hash, letter.event.payload.expires_at);
|
|
486
|
+
return { kind: "accepted", letter };
|
|
487
|
+
}
|
|
488
|
+
/** Forget letters whose signed expiration has passed; they can no longer be newly accepted. */
|
|
489
|
+
prune(now = Date.now()) {
|
|
490
|
+
safeTime(now);
|
|
491
|
+
for (const [id, expires] of this.accepted)
|
|
492
|
+
if (now >= expires)
|
|
493
|
+
this.accepted.delete(id);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
export function validateMailDiscovery(value, service) {
|
|
497
|
+
validateMailSchema(value, "discoveryDocument");
|
|
498
|
+
validateOrigin(service);
|
|
499
|
+
requireMail(value.service === service, "invalid_response", "discovery origin mismatch");
|
|
500
|
+
}
|
|
501
|
+
function ownerClaims(token, service, now) {
|
|
502
|
+
safeTime(now);
|
|
503
|
+
try {
|
|
504
|
+
return verifyRequestJwt(token, {
|
|
505
|
+
audience: service,
|
|
506
|
+
nowSecs: Math.floor(now / 1000),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
throw protocolError("invalid_token", "invalid mailbox owner JWT");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** Identity request JWT for this relay origin whose subject is the mailbox owner. */
|
|
514
|
+
export function validateMailOwnerJwt(token, service, owner, now = Date.now()) {
|
|
515
|
+
validateOrigin(service);
|
|
516
|
+
validateAgentId(owner);
|
|
517
|
+
requireMail(ownerClaims(token, service, now).iss === owner, "permission_denied", "not mailbox owner");
|
|
518
|
+
}
|
|
519
|
+
export function validateMailCardRecord(value, owner, mailboxId, hash) {
|
|
520
|
+
validateMailSchema(value, "cardAcceptedRecord");
|
|
521
|
+
const r = value;
|
|
522
|
+
validateMailboxCard(r.envelope, owner);
|
|
523
|
+
requireMail((mailboxId === undefined ||
|
|
524
|
+
r.envelope.event.payload.mailbox_id === mailboxId) &&
|
|
525
|
+
(hash === undefined || r.envelope.hash === hash), "invalid_response", "card record mismatch");
|
|
526
|
+
}
|
|
527
|
+
export function validateMailDeliveryResult(value, packet) {
|
|
528
|
+
validateMailSchema(value, "deliveryResult");
|
|
529
|
+
const r = value;
|
|
530
|
+
if (packet !== undefined)
|
|
531
|
+
requireMail(r.packet_id === mailPacketId(packet) &&
|
|
532
|
+
r.accepted_at < packet.header.expires_at, "invalid_response", "delivery result mismatch");
|
|
533
|
+
}
|
|
534
|
+
export function validateMailPacketRecord(value, mailboxId) {
|
|
535
|
+
validateMailSchema(value, "packetRecord");
|
|
536
|
+
const r = value;
|
|
537
|
+
validateMailPacket(r.packet, r.packet_id);
|
|
538
|
+
requireMail(r.accepted_at < r.packet.header.expires_at &&
|
|
539
|
+
(mailboxId === undefined || r.packet.header.mailbox_id === mailboxId), "invalid_response", "packet record mismatch");
|
|
540
|
+
}
|
|
541
|
+
export function validateMailPacketList(value, mailboxId, limit = 100) {
|
|
542
|
+
validateMailSchema(value, "packetList");
|
|
543
|
+
const p = value;
|
|
544
|
+
requireMail(p.result.length <= limit && (!p.next_cursor || p.result.length > 0), "invalid_response", "invalid page size");
|
|
545
|
+
let seq = 0;
|
|
546
|
+
for (const r of p.result) {
|
|
547
|
+
validateMailPacketRecord(r, mailboxId);
|
|
548
|
+
requireMail(r.seq > seq, "invalid_response", "unordered packets");
|
|
549
|
+
seq = r.seq;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Synchronous process-local relay model. All validation precedes mutation;
|
|
554
|
+
* persist the snapshot atomically before emitting a successful HTTP response.
|
|
555
|
+
* The short-lived nonce cache is not part of the snapshot.
|
|
556
|
+
*/
|
|
557
|
+
export class MailRelayStore {
|
|
558
|
+
origin;
|
|
559
|
+
boxes = new Map();
|
|
560
|
+
nonces;
|
|
561
|
+
clock;
|
|
562
|
+
maxPackets;
|
|
563
|
+
maxBytes;
|
|
564
|
+
constructor(origin, options = {}) {
|
|
565
|
+
this.origin = origin;
|
|
566
|
+
validateOrigin(origin);
|
|
567
|
+
this.nonces = options.nonceStore ?? new MemoryNonceStore();
|
|
568
|
+
this.clock = options.clock ?? Date.now;
|
|
569
|
+
this.maxPackets = options.maxPackets ?? 10000;
|
|
570
|
+
this.maxBytes = options.maxBytes ?? 64 * MAIL_MAX_PACKET_BYTES;
|
|
571
|
+
requireMail(Number.isSafeInteger(this.maxPackets) &&
|
|
572
|
+
this.maxPackets > 0 &&
|
|
573
|
+
Number.isSafeInteger(this.maxBytes) &&
|
|
574
|
+
this.maxBytes > 0, "invalid_request", "invalid quota");
|
|
575
|
+
const s = options.snapshot;
|
|
576
|
+
if (s) {
|
|
577
|
+
requireMail(s.version === 1 && s.origin === origin, "invalid_request", "snapshot origin mismatch");
|
|
578
|
+
for (const m of s.mailboxes) {
|
|
579
|
+
const box = {
|
|
580
|
+
current: clone(m.current),
|
|
581
|
+
lastSeq: m.last_seq,
|
|
582
|
+
bytes: 0,
|
|
583
|
+
packets: new Map(),
|
|
584
|
+
tombstones: new Map(),
|
|
585
|
+
};
|
|
586
|
+
for (const record of m.packets) {
|
|
587
|
+
const size = mailCanonicalBytes(record.packet).length;
|
|
588
|
+
box.packets.set(record.packet_id, { record: clone(record), size });
|
|
589
|
+
box.bytes += size;
|
|
590
|
+
}
|
|
591
|
+
for (const t of m.tombstones)
|
|
592
|
+
box.tombstones.set(t.packet_id, {
|
|
593
|
+
accepted_at: t.accepted_at,
|
|
594
|
+
expires_at: t.expires_at,
|
|
595
|
+
});
|
|
596
|
+
this.boxes.set(m.current.envelope.event.payload.mailbox_id, box);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
snapshot() {
|
|
601
|
+
return {
|
|
602
|
+
version: 1,
|
|
603
|
+
origin: this.origin,
|
|
604
|
+
mailboxes: [...this.boxes.values()].map((b) => ({
|
|
605
|
+
current: clone(b.current),
|
|
606
|
+
last_seq: b.lastSeq,
|
|
607
|
+
packets: [...b.packets.values()].map((p) => clone(p.record)),
|
|
608
|
+
tombstones: [...b.tombstones].map(([packet_id, t]) => ({
|
|
609
|
+
packet_id,
|
|
610
|
+
...t,
|
|
611
|
+
})),
|
|
612
|
+
})),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
discovery() {
|
|
616
|
+
return { protocol: MAIL_PROTOCOL, service: this.origin };
|
|
617
|
+
}
|
|
618
|
+
publish(card, now = this.clock()) {
|
|
619
|
+
card = clone(card);
|
|
620
|
+
safeTime(now);
|
|
621
|
+
validateMailboxCard(card);
|
|
622
|
+
const p = card.event.payload, b = this.boxes.get(p.mailbox_id);
|
|
623
|
+
if (b) {
|
|
624
|
+
const current = b.current.envelope;
|
|
625
|
+
requireMail(current.event.actor === card.event.actor, "mailbox_conflict", "mailbox belongs to another owner");
|
|
626
|
+
if (current.hash === card.hash)
|
|
627
|
+
return clone(b.current);
|
|
628
|
+
if (card.event.nonce <= current.event.nonce)
|
|
629
|
+
throw protocolError("nonce_not_greater", "card nonce does not advance", {
|
|
630
|
+
max_nonce: current.event.nonce,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
else
|
|
634
|
+
requireMail(p.routes.includes(this.origin), "permission_denied", "first card must list this relay");
|
|
635
|
+
verifyTimestamp(card.event.created_at, now, MAIL_FUTURE_SKEW_MS);
|
|
636
|
+
// The final fallible step, so a rejected card consumes no nonce.
|
|
637
|
+
this.nonces.checkAndUpdate(card.event.actor, card.event.nonce, now, 2 * MAIL_FUTURE_SKEW_MS);
|
|
638
|
+
const record = { envelope: card, accepted_at: now };
|
|
639
|
+
if (b)
|
|
640
|
+
b.current = record;
|
|
641
|
+
else
|
|
642
|
+
this.boxes.set(p.mailbox_id, {
|
|
643
|
+
current: record,
|
|
644
|
+
lastSeq: 0,
|
|
645
|
+
bytes: 0,
|
|
646
|
+
packets: new Map(),
|
|
647
|
+
tombstones: new Map(),
|
|
648
|
+
});
|
|
649
|
+
return clone(record);
|
|
650
|
+
}
|
|
651
|
+
card(mailboxId) {
|
|
652
|
+
pathId(mailboxId, 16);
|
|
653
|
+
const b = this.boxes.get(mailboxId);
|
|
654
|
+
requireMail(b, "mailbox_unavailable");
|
|
655
|
+
return clone(b.current);
|
|
656
|
+
}
|
|
657
|
+
publishJson(text, now = this.clock()) {
|
|
658
|
+
requireMail(encoder.encode(text).length <= MAIL_MAX_PACKET_BYTES, "payload_too_large");
|
|
659
|
+
return this.publish(parseStrictJson(text), now);
|
|
660
|
+
}
|
|
661
|
+
deliverJson(mailboxId, text, now = this.clock()) {
|
|
662
|
+
requireMail(encoder.encode(text).length <= MAIL_MAX_PACKET_BYTES, "payload_too_large");
|
|
663
|
+
return this.deliver(mailboxId, parseStrictJson(text), now);
|
|
664
|
+
}
|
|
665
|
+
deliver(mailboxId, packet, now = this.clock()) {
|
|
666
|
+
packet = clone(packet);
|
|
667
|
+
safeTime(now);
|
|
668
|
+
pathId(mailboxId, 16);
|
|
669
|
+
const bytes = packetBytes(packet), size = bytes.length;
|
|
670
|
+
const h = packet.header;
|
|
671
|
+
requireMail(h.mailbox_id === mailboxId, "invalid_packet", "path mailbox mismatch");
|
|
672
|
+
const b = this.boxes.get(mailboxId);
|
|
673
|
+
requireMail(b, "mailbox_unavailable");
|
|
674
|
+
const id = base64UrlEncode(sha3_256(bytes)), prior = b.tombstones.get(id);
|
|
675
|
+
if (prior)
|
|
676
|
+
return { packet_id: id, accepted_at: prior.accepted_at };
|
|
677
|
+
const c = b.current.envelope, p = c.event.payload;
|
|
678
|
+
requireMail(p.routes.includes(this.origin), "mailbox_unavailable");
|
|
679
|
+
requireMail(h.card_hash === c.hash && now < p.expires_at, "stale_card");
|
|
680
|
+
requireMail(now < h.expires_at, "packet_expired");
|
|
681
|
+
requireMail(h.expires_at <= p.receive_until &&
|
|
682
|
+
h.expires_at - now <= MAIL_MAX_TTL_MS + MAIL_FUTURE_SKEW_MS, "invalid_packet", "packet lifetime exceeds bound");
|
|
683
|
+
requireMail(size <= p.max_packet_bytes, "payload_too_large", "packet exceeds card limit");
|
|
684
|
+
this.dropExpired(b, now);
|
|
685
|
+
requireMail(b.packets.size < this.maxPackets &&
|
|
686
|
+
b.bytes + size <= this.maxBytes &&
|
|
687
|
+
b.lastSeq < Number.MAX_SAFE_INTEGER, "rate_limited", "mailbox storage quota exhausted");
|
|
688
|
+
b.lastSeq += 1;
|
|
689
|
+
b.packets.set(id, {
|
|
690
|
+
record: { packet_id: id, packet, accepted_at: now, seq: b.lastSeq },
|
|
691
|
+
size,
|
|
692
|
+
});
|
|
693
|
+
b.bytes += size;
|
|
694
|
+
b.tombstones.set(id, { accepted_at: now, expires_at: h.expires_at });
|
|
695
|
+
return { packet_id: id, accepted_at: now };
|
|
696
|
+
}
|
|
697
|
+
ownerBox(mailboxId, jwt, now) {
|
|
698
|
+
pathId(mailboxId, 16);
|
|
699
|
+
const claims = ownerClaims(jwt, this.origin, now);
|
|
700
|
+
const b = this.boxes.get(mailboxId);
|
|
701
|
+
requireMail(b, "mailbox_unavailable");
|
|
702
|
+
requireMail(claims.iss === b.current.envelope.event.actor, "permission_denied", "not mailbox owner");
|
|
703
|
+
return b;
|
|
704
|
+
}
|
|
705
|
+
list(mailboxId, jwt, options = {}) {
|
|
706
|
+
const now = options.now ?? this.clock();
|
|
707
|
+
safeTime(now);
|
|
708
|
+
const b = this.ownerBox(mailboxId, jwt, now), limit = options.limit ?? 100, cursor = options.cursor;
|
|
709
|
+
requireMail(Number.isSafeInteger(limit) && limit >= 1 && limit <= 1000, "invalid_request", "invalid limit");
|
|
710
|
+
// The cursor is the last returned seq; ownership is checked by the JWT.
|
|
711
|
+
requireMail(cursor === undefined ||
|
|
712
|
+
(/^(0|[1-9][0-9]*)$/.test(cursor) &&
|
|
713
|
+
Number.isSafeInteger(Number(cursor))), "invalid_request", "invalid cursor");
|
|
714
|
+
const after = cursor === undefined ? 0 : Number(cursor);
|
|
715
|
+
const rows = [...b.packets.values()]
|
|
716
|
+
.map((x) => x.record)
|
|
717
|
+
.filter((r) => r.seq > after && now < r.packet.header.expires_at);
|
|
718
|
+
const result = clone(rows.slice(0, limit));
|
|
719
|
+
return rows.length > limit
|
|
720
|
+
? { result, next_cursor: String(result.at(-1).seq) }
|
|
721
|
+
: { result };
|
|
722
|
+
}
|
|
723
|
+
delete(mailboxId, packetId, jwt, now = this.clock()) {
|
|
724
|
+
const b = this.ownerBox(mailboxId, jwt, now);
|
|
725
|
+
pathId(packetId, 32);
|
|
726
|
+
const stored = b.packets.get(packetId);
|
|
727
|
+
if (stored) {
|
|
728
|
+
b.packets.delete(packetId);
|
|
729
|
+
b.bytes -= stored.size;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/** Drop expired packets and tombstones, then forget mailboxes whose current card's `receive_until` passed. */
|
|
733
|
+
prune(now = this.clock()) {
|
|
734
|
+
safeTime(now);
|
|
735
|
+
for (const [mailboxId, b] of this.boxes) {
|
|
736
|
+
this.dropExpired(b, now);
|
|
737
|
+
for (const [id, t] of b.tombstones)
|
|
738
|
+
if (now >= t.expires_at)
|
|
739
|
+
b.tombstones.delete(id);
|
|
740
|
+
if (now >= b.current.envelope.event.payload.receive_until)
|
|
741
|
+
this.boxes.delete(mailboxId);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
dropExpired(b, now) {
|
|
745
|
+
for (const [id, x] of b.packets)
|
|
746
|
+
if (now >= x.record.packet.header.expires_at) {
|
|
747
|
+
b.packets.delete(id);
|
|
748
|
+
b.bytes -= x.size;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
//# sourceMappingURL=mail.js.map
|