agent-inbox 0.2.4 → 0.2.5

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.
Files changed (138) hide show
  1. package/CLAUDE.md +1 -92
  2. package/README.md +6 -73
  3. package/dist/federation/connection-manager.d.ts +0 -8
  4. package/dist/federation/connection-manager.d.ts.map +1 -1
  5. package/dist/federation/connection-manager.js +0 -12
  6. package/dist/federation/connection-manager.js.map +1 -1
  7. package/dist/federation/delivery-queue.d.ts +3 -11
  8. package/dist/federation/delivery-queue.d.ts.map +1 -1
  9. package/dist/federation/delivery-queue.js +8 -38
  10. package/dist/federation/delivery-queue.js.map +1 -1
  11. package/dist/index.d.ts +0 -17
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +0 -98
  14. package/dist/index.js.map +1 -1
  15. package/dist/jsonrpc/mail-push-types.d.ts +22 -2
  16. package/dist/jsonrpc/mail-push-types.d.ts.map +1 -1
  17. package/dist/jsonrpc/mail-push-types.js +18 -1
  18. package/dist/jsonrpc/mail-push-types.js.map +1 -1
  19. package/dist/jsonrpc/mail-push.d.ts +12 -1
  20. package/dist/jsonrpc/mail-push.d.ts.map +1 -1
  21. package/dist/jsonrpc/mail-push.js +13 -2
  22. package/dist/jsonrpc/mail-push.js.map +1 -1
  23. package/dist/jsonrpc/mail-server.d.ts.map +1 -1
  24. package/dist/jsonrpc/mail-server.js +42 -18
  25. package/dist/jsonrpc/mail-server.js.map +1 -1
  26. package/dist/router/message-router.d.ts +0 -15
  27. package/dist/router/message-router.d.ts.map +1 -1
  28. package/dist/router/message-router.js +3 -25
  29. package/dist/router/message-router.js.map +1 -1
  30. package/dist/storage/interface.d.ts +2 -9
  31. package/dist/storage/interface.d.ts.map +1 -1
  32. package/dist/storage/memory.d.ts +1 -4
  33. package/dist/storage/memory.d.ts.map +1 -1
  34. package/dist/storage/memory.js +6 -12
  35. package/dist/storage/memory.js.map +1 -1
  36. package/dist/storage/sqlite.d.ts +1 -6
  37. package/dist/storage/sqlite.d.ts.map +1 -1
  38. package/dist/storage/sqlite.js +6 -28
  39. package/dist/storage/sqlite.js.map +1 -1
  40. package/dist/types.d.ts +0 -79
  41. package/dist/types.d.ts.map +1 -1
  42. package/docs/DESIGN.md +0 -15
  43. package/package.json +3 -28
  44. package/rules/agent-inbox.md +0 -1
  45. package/src/federation/connection-manager.ts +0 -12
  46. package/src/federation/delivery-queue.ts +8 -38
  47. package/src/index.ts +0 -148
  48. package/src/jsonrpc/mail-push-types.ts +43 -2
  49. package/src/jsonrpc/mail-push.ts +34 -3
  50. package/src/jsonrpc/mail-server.ts +45 -26
  51. package/src/router/message-router.ts +4 -41
  52. package/src/storage/interface.ts +2 -11
  53. package/src/storage/memory.ts +8 -15
  54. package/src/storage/sqlite.ts +9 -36
  55. package/src/types.ts +0 -73
  56. package/test/load.test.ts +1 -1
  57. package/test/mail-push.test.ts +101 -1
  58. package/test/mail-server.test.ts +108 -0
  59. package/AGENTS.md +0 -18
  60. package/dist/federation/queue-store.d.ts +0 -42
  61. package/dist/federation/queue-store.d.ts.map +0 -1
  62. package/dist/federation/queue-store.js +0 -87
  63. package/dist/federation/queue-store.js.map +0 -1
  64. package/dist/index.d.mts +0 -2
  65. package/dist/index.mjs +0 -1
  66. package/dist/index.mjs.map +0 -1
  67. package/dist/mail/address-book.d.ts +0 -43
  68. package/dist/mail/address-book.d.ts.map +0 -1
  69. package/dist/mail/address-book.js +0 -95
  70. package/dist/mail/address-book.js.map +0 -1
  71. package/dist/mail/attachment-store.d.ts +0 -31
  72. package/dist/mail/attachment-store.d.ts.map +0 -1
  73. package/dist/mail/attachment-store.js +0 -74
  74. package/dist/mail/attachment-store.js.map +0 -1
  75. package/dist/mail/email-mapper.d.ts +0 -41
  76. package/dist/mail/email-mapper.d.ts.map +0 -1
  77. package/dist/mail/email-mapper.js +0 -216
  78. package/dist/mail/email-mapper.js.map +0 -1
  79. package/dist/mail/fs-attachment-store.d.ts +0 -38
  80. package/dist/mail/fs-attachment-store.d.ts.map +0 -1
  81. package/dist/mail/fs-attachment-store.js +0 -165
  82. package/dist/mail/fs-attachment-store.js.map +0 -1
  83. package/dist/mail/mail-gateway.d.ts +0 -114
  84. package/dist/mail/mail-gateway.d.ts.map +0 -1
  85. package/dist/mail/mail-gateway.js +0 -402
  86. package/dist/mail/mail-gateway.js.map +0 -1
  87. package/dist/mail/provider-transport.d.ts +0 -138
  88. package/dist/mail/provider-transport.d.ts.map +0 -1
  89. package/dist/mail/provider-transport.js +0 -434
  90. package/dist/mail/provider-transport.js.map +0 -1
  91. package/dist/mail/rate-limiter.d.ts +0 -20
  92. package/dist/mail/rate-limiter.d.ts.map +0 -1
  93. package/dist/mail/rate-limiter.js +0 -56
  94. package/dist/mail/rate-limiter.js.map +0 -1
  95. package/dist/mail/smtp-transport.d.ts +0 -141
  96. package/dist/mail/smtp-transport.d.ts.map +0 -1
  97. package/dist/mail/smtp-transport.js +0 -415
  98. package/dist/mail/smtp-transport.js.map +0 -1
  99. package/dist/mail/types.d.ts +0 -177
  100. package/dist/mail/types.d.ts.map +0 -1
  101. package/dist/mail/types.js +0 -11
  102. package/dist/mail/types.js.map +0 -1
  103. package/dist/router/destination.d.ts +0 -69
  104. package/dist/router/destination.d.ts.map +0 -1
  105. package/dist/router/destination.js +0 -106
  106. package/dist/router/destination.js.map +0 -1
  107. package/docs/MAIL-INTEROP-PLAN.md +0 -660
  108. package/renovate.json5 +0 -6
  109. package/src/federation/queue-store.ts +0 -124
  110. package/src/mail/address-book.ts +0 -111
  111. package/src/mail/attachment-store.ts +0 -90
  112. package/src/mail/email-mapper.ts +0 -288
  113. package/src/mail/fs-attachment-store.ts +0 -163
  114. package/src/mail/mail-gateway.ts +0 -505
  115. package/src/mail/provider-transport.ts +0 -577
  116. package/src/mail/rate-limiter.ts +0 -51
  117. package/src/mail/smtp-transport.ts +0 -589
  118. package/src/mail/types.ts +0 -221
  119. package/src/router/destination.ts +0 -140
  120. package/test/federation/delivery-queue-sqlite.test.ts +0 -158
  121. package/test/mail/address-book.test.ts +0 -111
  122. package/test/mail/attachment-store-contract.test.ts +0 -92
  123. package/test/mail/attachment-store.test.ts +0 -69
  124. package/test/mail/destination.test.ts +0 -115
  125. package/test/mail/dsn-parse.test.ts +0 -239
  126. package/test/mail/email-mapper.test.ts +0 -341
  127. package/test/mail/external-id.test.ts +0 -43
  128. package/test/mail/fs-attachment-store.test.ts +0 -134
  129. package/test/mail/full-flow-e2e.test.ts +0 -200
  130. package/test/mail/mail-gateway.test.ts +0 -419
  131. package/test/mail/mail-transport-contract.test.ts +0 -134
  132. package/test/mail/mock-mail.ts +0 -161
  133. package/test/mail/mock-postmark.ts +0 -66
  134. package/test/mail/provider-transport.test.ts +0 -381
  135. package/test/mail/rate-limiter.test.ts +0 -48
  136. package/test/mail/router-mail-integration.test.ts +0 -138
  137. package/test/mail/smtp-e2e.test.ts +0 -98
  138. package/test/mail/smtp-transport.test.ts +0 -138
@@ -1,134 +0,0 @@
1
- /**
2
- * Shared MailTransport contract suite.
3
- *
4
- * Any backend that provides a MailTransportHarness must pass this suite
5
- * unchanged — it encodes the hardening guarantees from §2/§3 of the plan.
6
- * Run it against new backends by adding another `runMailTransportContract(...)`.
7
- */
8
-
9
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
10
- import type { MailTransportHarness } from "./mock-mail.js";
11
- import { mockMailHarness, makeInboundMail, makeOutboundMail } from "./mock-mail.js";
12
- import { postmarkHarness } from "./mock-postmark.js";
13
-
14
- export function runMailTransportContract(
15
- name: string,
16
- makeHarness: () => MailTransportHarness
17
- ): void {
18
- describe(`MailTransport contract: ${name}`, () => {
19
- let h: MailTransportHarness;
20
-
21
- beforeEach(async () => {
22
- h = makeHarness();
23
- await h.transport.start();
24
- });
25
-
26
- afterEach(async () => {
27
- await h.transport.stop();
28
- });
29
-
30
- it("start() leaves the transport in a ready state", async () => {
31
- expect(h.transport.state).toBe("ready");
32
- expect((await h.transport.health()).state).toBe("ready");
33
- });
34
-
35
- it("start() is idempotent", async () => {
36
- await h.transport.start();
37
- await h.transport.start();
38
- expect(h.transport.state).toBe("ready");
39
- });
40
-
41
- it("send() returns delivered (never throws) on success", async () => {
42
- h.programNextSend({ disposition: "delivered", remoteMessageId: "x" });
43
- const res = await h.transport.send(makeOutboundMail());
44
- expect(res.disposition).toBe("delivered");
45
- });
46
-
47
- it("send() returns — does not throw — on transient failure", async () => {
48
- h.programNextSend({ disposition: "transient", code: 451 });
49
- const res = await h.transport.send(makeOutboundMail());
50
- expect(res.disposition).toBe("transient");
51
- });
52
-
53
- it("send() returns — does not throw — on permanent failure", async () => {
54
- h.programNextSend({ disposition: "permanent", code: 550 });
55
- const res = await h.transport.send(makeOutboundMail());
56
- expect(res.disposition).toBe("permanent");
57
- });
58
-
59
- it("is idempotent on idempotencyKey: a duplicate delivered send produces no second email", async () => {
60
- const env = makeOutboundMail({ idempotencyKey: "dup-1" });
61
- const first = await h.transport.send(env);
62
- const second = await h.transport.send(env);
63
- expect(first.disposition).toBe("delivered");
64
- expect(second.disposition).toBe("delivered");
65
- // Both report delivered, but the second is a replay, not a new email.
66
- expect(second.detail).toMatch(/idempotent|replay/i);
67
- });
68
-
69
- it("ACKs inbound only after the handler resolves", async () => {
70
- const order: string[] = [];
71
- h.transport.onReceive(async () => {
72
- order.push("handler-start");
73
- await Promise.resolve();
74
- order.push("handler-end");
75
- });
76
- await h.injectInbound(makeInboundMail());
77
- // injectInbound resolving (the ACK) must come strictly after handler-end.
78
- expect(order).toEqual(["handler-start", "handler-end"]);
79
- });
80
-
81
- it("NACKs inbound (rejects) when the handler throws", async () => {
82
- h.transport.onReceive(async () => {
83
- throw new Error("storage commit failed");
84
- });
85
- await expect(h.injectInbound(makeInboundMail())).rejects.toThrow(
86
- /storage commit failed/
87
- );
88
- });
89
-
90
- it("delivers the inbound message to the handler intact", async () => {
91
- let received: unknown;
92
- h.transport.onReceive(async (mail) => {
93
- received = mail;
94
- });
95
- const mail = makeInboundMail({ subject: "specific-subject" });
96
- await h.injectInbound(mail);
97
- expect((received as { subject: string }).subject).toBe("specific-subject");
98
- });
99
- });
100
- }
101
-
102
- // Run the contract against the in-process mock and the Postmark provider
103
- // backend (via a fake client). Real SMTP I/O is covered by smtp-e2e.test.ts.
104
- runMailTransportContract("MockMailTransport", () => mockMailHarness());
105
- runMailTransportContract("PostmarkTransport", () => postmarkHarness());
106
-
107
- describe("MockMailTransport specifics", () => {
108
- let h: ReturnType<typeof mockMailHarness>;
109
-
110
- beforeEach(async () => {
111
- h = mockMailHarness();
112
- await h.transport.start();
113
- });
114
-
115
- it("send() throws (programmer error) when not started", async () => {
116
- const fresh = mockMailHarness();
117
- await expect(fresh.transport.send(makeOutboundMail())).rejects.toThrow(
118
- /state/
119
- );
120
- });
121
-
122
- it("records one entry in sent per delivered email and dedups replays", async () => {
123
- const env = makeOutboundMail({ idempotencyKey: "k1" });
124
- await h.transport.send(env);
125
- await h.transport.send(env);
126
- expect(h.mock.sent).toHaveLength(1);
127
- });
128
-
129
- it("does not record sent for a transient failure", async () => {
130
- h.programNextSend({ disposition: "transient" });
131
- await h.transport.send(makeOutboundMail({ idempotencyKey: "k2" }));
132
- expect(h.mock.sent).toHaveLength(0);
133
- });
134
- });
@@ -1,161 +0,0 @@
1
- /**
2
- * In-process mail transport for tests, plus the harness abstraction the shared
3
- * contract suite runs against.
4
- *
5
- * A real backend's test wrapper implements MailTransportHarness too, so the
6
- * same contract suite (mail-transport-contract.test.ts) validates it without
7
- * modification. See docs/MAIL-INTEROP-PLAN.md §11.
8
- */
9
-
10
- import type {
11
- MailTransport,
12
- MailCapabilities,
13
- MailTransportState,
14
- MailHealth,
15
- OutboundMail,
16
- MailSendResult,
17
- InboundMail,
18
- InboundHandler,
19
- } from "../../src/mail/types.js";
20
-
21
- const DEFAULT_CAPS: MailCapabilities = {
22
- outbound: "api",
23
- signsDkim: true,
24
- verifiesInboundAuth: true,
25
- inbound: "webhook",
26
- maxMessageBytes: 25 * 1024 * 1024,
27
- };
28
-
29
- /**
30
- * The control surface a contract test needs over any transport: drive inbound,
31
- * and program the next outbound outcome. Real backends provide their own.
32
- */
33
- export interface MailTransportHarness {
34
- transport: MailTransport;
35
- /** Drive one inbound message; resolves on ACK, rejects on NACK (handler throw). */
36
- injectInbound(mail: InboundMail): Promise<void>;
37
- /** Force the next send() to produce this result. */
38
- programNextSend(result: MailSendResult): void;
39
- }
40
-
41
- export class MockMailTransport implements MailTransport {
42
- readonly capabilities: MailCapabilities;
43
- private _state: MailTransportState = "stopped";
44
- private handler?: InboundHandler;
45
-
46
- /** Recorded successfully-issued sends (one entry per external email produced). */
47
- readonly sent: OutboundMail[] = [];
48
- /** idempotencyKeys that have already produced a delivered external email. */
49
- private delivered = new Set<string>();
50
- private programmed: MailSendResult[] = [];
51
-
52
- constructor(caps: Partial<MailCapabilities> = {}) {
53
- this.capabilities = { ...DEFAULT_CAPS, ...caps };
54
- }
55
-
56
- get state(): MailTransportState {
57
- return this._state;
58
- }
59
-
60
- async start(): Promise<void> {
61
- if (this._state === "ready") return; // idempotent
62
- this._state = "starting";
63
- this._state = "ready";
64
- }
65
-
66
- async stop(): Promise<void> {
67
- this._state = "stopping";
68
- this._state = "stopped";
69
- }
70
-
71
- async health(): Promise<MailHealth> {
72
- return { state: this._state };
73
- }
74
-
75
- onReceive(handler: InboundHandler): void {
76
- this.handler = handler;
77
- }
78
-
79
- programNextSend(result: MailSendResult): void {
80
- this.programmed.push(result);
81
- }
82
-
83
- async send(envelope: OutboundMail): Promise<MailSendResult> {
84
- if (this._state !== "ready") {
85
- throw new Error(`send() called while transport state is "${this._state}"`);
86
- }
87
-
88
- // Idempotent: a duplicate key for an already-delivered message must not
89
- // produce a second external email.
90
- if (this.delivered.has(envelope.idempotencyKey)) {
91
- return {
92
- disposition: "delivered",
93
- remoteMessageId: `mock-dup-${envelope.idempotencyKey}`,
94
- detail: "idempotent replay",
95
- };
96
- }
97
-
98
- const result: MailSendResult =
99
- this.programmed.shift() ?? {
100
- disposition: "delivered",
101
- remoteMessageId: `mock-${envelope.idempotencyKey}`,
102
- };
103
-
104
- if (result.disposition === "delivered") {
105
- this.sent.push(envelope);
106
- this.delivered.add(envelope.idempotencyKey);
107
- }
108
- return result;
109
- }
110
-
111
- /** Test helper: drive an inbound message through the registered handler. */
112
- async deliver(mail: InboundMail): Promise<void> {
113
- if (!this.handler) throw new Error("no inbound handler registered");
114
- if (this._state !== "ready") {
115
- throw new Error(`deliver() while state is "${this._state}"`);
116
- }
117
- await this.handler(mail); // throw bubbles up as a NACK
118
- }
119
- }
120
-
121
- /** Build a MailTransportHarness around a MockMailTransport. */
122
- export function mockMailHarness(
123
- caps?: Partial<MailCapabilities>
124
- ): MailTransportHarness & { mock: MockMailTransport } {
125
- const mock = new MockMailTransport(caps);
126
- return {
127
- mock,
128
- transport: mock,
129
- injectInbound: (mail) => mock.deliver(mail),
130
- programNextSend: (result) => mock.programNextSend(result),
131
- };
132
- }
133
-
134
- /** Minimal valid InboundMail for tests; override fields as needed. */
135
- export function makeInboundMail(over: Partial<InboundMail> = {}): InboundMail {
136
- return {
137
- envelopeFrom: "ext@example.org",
138
- envelopeTo: ["agent@example.com"],
139
- from: { address: "ext@example.org" },
140
- to: [{ address: "agent@example.com" }],
141
- subject: "hello",
142
- text: "hi there",
143
- headers: { raw: {} },
144
- sizeBytes: 100,
145
- receivedAt: new Date().toISOString(),
146
- ...over,
147
- };
148
- }
149
-
150
- /** Minimal valid OutboundMail for tests. */
151
- export function makeOutboundMail(over: Partial<OutboundMail> = {}): OutboundMail {
152
- return {
153
- idempotencyKey: "msg-1",
154
- from: { address: "agent@example.com" },
155
- to: [{ address: "ext@example.org" }],
156
- subject: "re: hello",
157
- text: "reply body",
158
- headers: { messageId: "<msg-1@example.com>" },
159
- ...over,
160
- };
161
- }
@@ -1,66 +0,0 @@
1
- /**
2
- * Fake Postmark client + harness so the shared MailTransport contract suite can
3
- * run against PostmarkTransport without the SDK or network. The fake produces
4
- * raw Postmark responses/errors keyed by a desired disposition, and the real
5
- * transport mapping turns them into MailSendResults — so the contract suite
6
- * also exercises postmarkResponseToResult / postmarkErrorToResult.
7
- */
8
-
9
- import {
10
- PostmarkTransport,
11
- type PostmarkClient,
12
- type PostmarkSendResponse,
13
- } from "../../src/mail/provider-transport.js";
14
- import type { MailSendResult } from "../../src/mail/types.js";
15
- import type { MailTransportHarness } from "./mock-mail.js";
16
-
17
- type Disposition = MailSendResult["disposition"];
18
-
19
- export interface FakePostmarkClient extends PostmarkClient {
20
- /** Raw messages passed to sendEmail. */
21
- sent: Record<string, unknown>[];
22
- /** Program the next send's outcome. */
23
- program(disposition: Disposition): void;
24
- }
25
-
26
- export function fakePostmarkClient(): FakePostmarkClient {
27
- const queue: Disposition[] = [];
28
- const sent: Record<string, unknown>[] = [];
29
- let counter = 0;
30
- return {
31
- sent,
32
- program(d) {
33
- queue.push(d);
34
- },
35
- async sendEmail(msg): Promise<PostmarkSendResponse> {
36
- sent.push(msg);
37
- const d = queue.shift() ?? "delivered";
38
- if (d === "delivered") {
39
- return { ErrorCode: 0, MessageID: `pm-${++counter}` };
40
- }
41
- if (d === "transient") {
42
- throw Object.assign(new Error("rate limited"), { statusCode: 429 });
43
- }
44
- // permanent — inactive recipient (Postmark code 406, HTTP 422)
45
- throw Object.assign(new Error("inactive recipient"), {
46
- statusCode: 422,
47
- code: 406,
48
- });
49
- },
50
- };
51
- }
52
-
53
- export function postmarkHarness(): MailTransportHarness & {
54
- client: FakePostmarkClient;
55
- postmark: PostmarkTransport;
56
- } {
57
- const client = fakePostmarkClient();
58
- const transport = new PostmarkTransport({ client });
59
- return {
60
- client,
61
- postmark: transport,
62
- transport,
63
- injectInbound: (mail) => transport.receiveInbound(mail),
64
- programNextSend: (result) => client.program(result.disposition),
65
- };
66
- }