@wraps.dev/cli 3.15.0 → 3.16.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.
@@ -1 +1 @@
1
- Built at: 2026-09-29T21:45:54.899Z
1
+ Built at: 2026-09-30T17:20:07.445Z
@@ -1 +1 @@
1
- Built at: 2026-09-29T21:45:54.715Z
1
+ Built at: 2026-09-30T17:20:07.270Z
@@ -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-09-29T21:45:54.858Z
1
+ Built at: 2026-09-30T17:20:07.414Z
@@ -1 +1 @@
1
- Built at: 2026-09-29T21:45:54.763Z
1
+ Built at: 2026-09-30T17:20:07.323Z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wraps.dev/cli",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "description": "Deploy email, SMS, and CDN infrastructure to your AWS account with one command. Sets up SES with DKIM/SPF/DMARC, event processing, and history automatically.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -104,11 +104,11 @@
104
104
  "tsx": "4.20.6",
105
105
  "typescript": "^5.9.3",
106
106
  "vitest": "4.1.9",
107
- "@wraps.dev/pulumi": "0.5.0",
107
+ "@wraps.dev/email-check": "1.2.0",
108
108
  "@wraps/core": "0.1.2",
109
+ "@wraps.dev/pulumi": "0.6.0",
109
110
  "@wraps/db": "0.0.0",
110
- "@wraps/template-render": "0.1.0",
111
- "@wraps.dev/email-check": "1.2.0"
111
+ "@wraps/template-render": "0.1.0"
112
112
  },
113
113
  "engines": {
114
114
  "node": ">=22.0.0"