@wraps.dev/cli 3.15.0 → 3.17.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/dist/cli.js +1593 -1402
- package/dist/cli.js.map +1 -1
- package/dist/lambda/agent-enforcer/.bundled +1 -1
- package/dist/lambda/event-processor/.bundled +1 -1
- package/dist/lambda/event-processor/cloudformation-parity.test.ts +230 -0
- package/dist/lambda/inbound-processor/.bundled +1 -1
- package/dist/lambda/inbound-processor/index.js +47 -47
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
Built at: 2026-
|
|
1
|
+
Built at: 2026-10-06T16:19:03.255Z
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Built at: 2026-
|
|
1
|
+
Built at: 2026-10-06T16:19:03.062Z
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
DynamoDBClient,
|
|
4
|
+
PutItemCommand,
|
|
5
|
+
UpdateItemCommand,
|
|
6
|
+
} from "@aws-sdk/client-dynamodb";
|
|
7
|
+
import type { Context } from "aws-lambda";
|
|
8
|
+
import { mockClient } from "aws-sdk-client-mock";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { handler as coreHandler } from "./index.ts";
|
|
11
|
+
|
|
12
|
+
// The published CloudFormation template carries a hand-written copy of this
|
|
13
|
+
// Lambda. It drifted twice (mailSentAt, suppression ledger) because nothing
|
|
14
|
+
// compared them. This file runs both on the same SES events and requires
|
|
15
|
+
// identical DynamoDB commands.
|
|
16
|
+
|
|
17
|
+
const TEMPLATE = new URL(
|
|
18
|
+
"../../../../cloudformation/wraps-email-infrastructure.yaml",
|
|
19
|
+
import.meta.url
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/** Body of the WrapsEmailProcessor ZipFile block scalar, dedented. */
|
|
23
|
+
function inlineProcessorSource(): string {
|
|
24
|
+
const lines = readFileSync(TEMPLATE, "utf-8").split("\n");
|
|
25
|
+
const fnLine = lines.findIndex((l) => l.trim() === "WrapsEmailProcessor:");
|
|
26
|
+
const zipLine = lines.findIndex(
|
|
27
|
+
(l, i) => i > fnLine && l.trim() === "ZipFile: |"
|
|
28
|
+
);
|
|
29
|
+
const indent =
|
|
30
|
+
lines[zipLine + 1].length - lines[zipLine + 1].trimStart().length;
|
|
31
|
+
const body: string[] = [];
|
|
32
|
+
for (const line of lines.slice(zipLine + 1)) {
|
|
33
|
+
if (line.trim() !== "" && line.length - line.trimStart().length < indent) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
body.push(line.slice(indent));
|
|
37
|
+
}
|
|
38
|
+
return body.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ddbMock = mockClient(DynamoDBClient);
|
|
42
|
+
|
|
43
|
+
function loadInlineHandler() {
|
|
44
|
+
const required: string[] = [];
|
|
45
|
+
const fakeRequire = (id: string) => {
|
|
46
|
+
required.push(id);
|
|
47
|
+
if (id !== "@aws-sdk/client-dynamodb") {
|
|
48
|
+
throw new Error(`unexpected require: ${id}`);
|
|
49
|
+
}
|
|
50
|
+
return { DynamoDBClient, PutItemCommand, UpdateItemCommand };
|
|
51
|
+
};
|
|
52
|
+
const module = {
|
|
53
|
+
exports: {} as { handler: (e: unknown) => Promise<unknown> },
|
|
54
|
+
};
|
|
55
|
+
new Function(
|
|
56
|
+
"require",
|
|
57
|
+
"module",
|
|
58
|
+
"exports",
|
|
59
|
+
"process",
|
|
60
|
+
"console",
|
|
61
|
+
inlineProcessorSource()
|
|
62
|
+
)(fakeRequire, module, module.exports, process, console);
|
|
63
|
+
return { handler: module.exports.handler, required };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function recorded() {
|
|
67
|
+
return ddbMock.calls().map((c) => {
|
|
68
|
+
const cmd = c.args[0];
|
|
69
|
+
return { command: cmd.constructor.name, input: cmd.input };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const MAIL = {
|
|
74
|
+
messageId: "ses-msg-1",
|
|
75
|
+
timestamp: "2026-09-01T00:00:00.000Z",
|
|
76
|
+
source: "a@wraps.dev",
|
|
77
|
+
destination: ["X@Example.com", "other@example.com"],
|
|
78
|
+
commonHeaders: { subject: "hi" },
|
|
79
|
+
};
|
|
80
|
+
const EVENT_TS = "2026-09-01T00:00:05.000Z";
|
|
81
|
+
|
|
82
|
+
function sqsEvent(...details: unknown[]) {
|
|
83
|
+
return {
|
|
84
|
+
Records: details.map((detail, i) => ({
|
|
85
|
+
messageId: `sqs-${i + 1}`,
|
|
86
|
+
body: JSON.stringify({ detail }),
|
|
87
|
+
})),
|
|
88
|
+
} as never;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function bounce(bounceOverrides: Record<string, unknown> = {}) {
|
|
92
|
+
return {
|
|
93
|
+
eventType: "Bounce",
|
|
94
|
+
mail: MAIL,
|
|
95
|
+
bounce: {
|
|
96
|
+
bounceType: "Permanent",
|
|
97
|
+
bounceSubType: "General",
|
|
98
|
+
timestamp: EVENT_TS,
|
|
99
|
+
feedbackId: "fb-1",
|
|
100
|
+
bouncedRecipients: [{ emailAddress: "X@Example.com " }],
|
|
101
|
+
...bounceOverrides,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function runBoth(event: never, setupMock?: () => void) {
|
|
107
|
+
setupMock?.();
|
|
108
|
+
await coreHandler(event, { awsRequestId: "r" } as Context);
|
|
109
|
+
const core = recorded();
|
|
110
|
+
|
|
111
|
+
ddbMock.reset();
|
|
112
|
+
setupMock?.();
|
|
113
|
+
await loadInlineHandler().handler(event);
|
|
114
|
+
const inline = recorded();
|
|
115
|
+
|
|
116
|
+
return { core, inline };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
ddbMock.reset();
|
|
121
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
122
|
+
vi.setSystemTime(new Date("2026-09-01T00:10:00.000Z"));
|
|
123
|
+
process.env.TABLE_NAME = "wraps-email-history";
|
|
124
|
+
process.env.AWS_ACCOUNT_ID = "123456789012";
|
|
125
|
+
process.env.RETENTION_DAYS = "90";
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
vi.useRealTimers();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("CloudFormation inline event processor parity with @wraps/core", () => {
|
|
133
|
+
const cases: [string, unknown, (() => void)?][] = [
|
|
134
|
+
["Send event", { eventType: "Send", mail: MAIL, send: {} }],
|
|
135
|
+
[
|
|
136
|
+
"Delivery event",
|
|
137
|
+
{
|
|
138
|
+
eventType: "Delivery",
|
|
139
|
+
mail: MAIL,
|
|
140
|
+
delivery: {
|
|
141
|
+
timestamp: EVENT_TS,
|
|
142
|
+
processingTimeMillis: 12,
|
|
143
|
+
recipients: ["x@example.com"],
|
|
144
|
+
smtpResponse: "250 OK",
|
|
145
|
+
remoteMtaIp: "1.2.3.4",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
[
|
|
150
|
+
"Open event",
|
|
151
|
+
{
|
|
152
|
+
eventType: "Open",
|
|
153
|
+
mail: MAIL,
|
|
154
|
+
open: { timestamp: EVENT_TS, userAgent: "ua", ipAddress: "1.2.3.4" },
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
["permanent bounce", bounce()],
|
|
158
|
+
[
|
|
159
|
+
"transient bounce",
|
|
160
|
+
bounce({ bounceType: "Transient", bounceSubType: "General" }),
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"OnAccountSuppressionList bounce",
|
|
164
|
+
bounce({
|
|
165
|
+
bounceType: "Permanent",
|
|
166
|
+
bounceSubType: "OnAccountSuppressionList",
|
|
167
|
+
}),
|
|
168
|
+
],
|
|
169
|
+
[
|
|
170
|
+
"EmailValidationSuppressed bounce",
|
|
171
|
+
bounce({
|
|
172
|
+
bounceType: "Undetermined",
|
|
173
|
+
bounceSubType: "EmailValidationSuppressed",
|
|
174
|
+
}),
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"complaint",
|
|
178
|
+
{
|
|
179
|
+
eventType: "Complaint",
|
|
180
|
+
mail: MAIL,
|
|
181
|
+
complaint: {
|
|
182
|
+
timestamp: EVENT_TS,
|
|
183
|
+
feedbackId: "fb-2",
|
|
184
|
+
complaintFeedbackType: "abuse",
|
|
185
|
+
complainedRecipients: [{ emailAddress: "X@Example.com " }],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
"bounce with no bouncedRecipients",
|
|
191
|
+
bounce({ bouncedRecipients: undefined }),
|
|
192
|
+
],
|
|
193
|
+
[
|
|
194
|
+
"two-recipient bounce with first ledger write failing",
|
|
195
|
+
bounce({
|
|
196
|
+
bouncedRecipients: [
|
|
197
|
+
{ emailAddress: "a@example.com" },
|
|
198
|
+
{ emailAddress: "b@example.com" },
|
|
199
|
+
],
|
|
200
|
+
}),
|
|
201
|
+
() => {
|
|
202
|
+
ddbMock
|
|
203
|
+
.on(UpdateItemCommand)
|
|
204
|
+
.rejectsOnce(new Error("boom"))
|
|
205
|
+
.resolves({});
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
for (const [name, detail, setup] of cases) {
|
|
211
|
+
it(`sends identical commands for: ${name}`, async () => {
|
|
212
|
+
const { core, inline } = await runBoth(sqsEvent(detail), setup);
|
|
213
|
+
expect(core.length).toBeGreaterThan(0);
|
|
214
|
+
expect(inline).toEqual(core);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
it("requires only the AWS SDK, which the Lambda runtime provides", () => {
|
|
219
|
+
expect(loadInlineHandler().required).toEqual(["@aws-sdk/client-dynamodb"]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("mocks intercept the inline client", async () => {
|
|
223
|
+
await loadInlineHandler().handler(sqsEvent(bounce()));
|
|
224
|
+
const calls = recorded();
|
|
225
|
+
expect(calls.map((c) => c.command)).toEqual([
|
|
226
|
+
"PutItemCommand",
|
|
227
|
+
"UpdateItemCommand",
|
|
228
|
+
]);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Built at: 2026-
|
|
1
|
+
Built at: 2026-10-06T16:19:03.214Z
|