@wraps.dev/cli 3.17.3 → 3.17.4
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 +2507 -1314
- 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/inbound-processor/.bundled +1 -1
- package/dist/lambda/inbound-processor/index.js +55 -55
- package/dist/lambda/inbound-processor/index.test.ts +364 -4
- package/dist/lambda/inbound-processor/index.ts +249 -11
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/package.json +26 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
EventBridgeClient,
|
|
4
4
|
PutEventsCommand,
|
|
@@ -38,6 +38,205 @@ const INBOUND_EVENT_SOURCE =
|
|
|
38
38
|
/** Max HTML size in EventBridge detail (200KB) */
|
|
39
39
|
const MAX_HTML_SIZE = 200 * 1024;
|
|
40
40
|
|
|
41
|
+
const EMAIL_RECEIVED_DETAIL_TYPE = "email.received";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* EventBridge PutEvents size limits. AWS raised the limit from 256 KB to
|
|
45
|
+
* 1 MB on 2026-01-29 ("the total request size … must be less than 1 MB
|
|
46
|
+
* (1,048,576 bytes)"), except in the Regions listed below, which keep the
|
|
47
|
+
* old 256 KB limit. GovCloud was not part of that announcement.
|
|
48
|
+
* https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-putevents.html#eb-putevent-size
|
|
49
|
+
* https://aws.amazon.com/about-aws/whats-new/2026/01/amazon-eventbridge-increases-event-payload-size-256-kb-1-mb
|
|
50
|
+
*/
|
|
51
|
+
const EVENTBRIDGE_LIMIT_BYTES = 1024 * 1024;
|
|
52
|
+
const EVENTBRIDGE_LEGACY_LIMIT_BYTES = 256 * 1024;
|
|
53
|
+
const LEGACY_LIMIT_REGIONS = new Set([
|
|
54
|
+
"ap-southeast-5", // Asia Pacific (Malaysia)
|
|
55
|
+
"ap-southeast-6", // Asia Pacific (New Zealand)
|
|
56
|
+
"ap-southeast-7", // Asia Pacific (Thailand)
|
|
57
|
+
"ap-east-2", // Asia Pacific (Taipei)
|
|
58
|
+
"mx-central-1", // Mexico (Central)
|
|
59
|
+
]);
|
|
60
|
+
/** Headroom below the hard limit, so the entry is strictly smaller. */
|
|
61
|
+
const EVENT_SIZE_MARGIN_BYTES = 1024;
|
|
62
|
+
|
|
63
|
+
/** Longest subject kept in the minimal fallback detail. */
|
|
64
|
+
const MAX_FALLBACK_SUBJECT_LENGTH = 1000;
|
|
65
|
+
|
|
66
|
+
export function maxEventEntryBytes(
|
|
67
|
+
region = process.env.AWS_REGION ?? ""
|
|
68
|
+
): number {
|
|
69
|
+
const limit =
|
|
70
|
+
LEGACY_LIMIT_REGIONS.has(region) || region.startsWith("us-gov-")
|
|
71
|
+
? EVENTBRIDGE_LEGACY_LIMIT_BYTES
|
|
72
|
+
: EVENTBRIDGE_LIMIT_BYTES;
|
|
73
|
+
return limit - EVENT_SIZE_MARGIN_BYTES;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Size of a PutEvents entry as EventBridge counts it: the UTF-8 bytes of
|
|
78
|
+
* Source, DetailType and Detail (no Time or Resources are sent).
|
|
79
|
+
*/
|
|
80
|
+
export function putEventsEntrySize(entry: {
|
|
81
|
+
Source: string;
|
|
82
|
+
DetailType: string;
|
|
83
|
+
Detail: string;
|
|
84
|
+
}): number {
|
|
85
|
+
return (
|
|
86
|
+
Buffer.byteLength(entry.Source, "utf8") +
|
|
87
|
+
Buffer.byteLength(entry.DetailType, "utf8") +
|
|
88
|
+
Buffer.byteLength(entry.Detail, "utf8")
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type TrimmableEmail = {
|
|
93
|
+
emailId: string;
|
|
94
|
+
messageId: string;
|
|
95
|
+
receivingDomain: string | null;
|
|
96
|
+
from: unknown;
|
|
97
|
+
to: unknown[];
|
|
98
|
+
cc: unknown[];
|
|
99
|
+
subject: string;
|
|
100
|
+
date: string;
|
|
101
|
+
html: string | null;
|
|
102
|
+
htmlTruncated: boolean;
|
|
103
|
+
text: string | null;
|
|
104
|
+
headers: Record<string, unknown>;
|
|
105
|
+
attachments: unknown[];
|
|
106
|
+
spamVerdict: unknown;
|
|
107
|
+
virusVerdict: unknown;
|
|
108
|
+
rawS3Key: string;
|
|
109
|
+
receivedAt: string;
|
|
110
|
+
replyToken: unknown;
|
|
111
|
+
autoReply: boolean;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the `email.received` Detail string so the entry fits under `maxBytes`.
|
|
116
|
+
*
|
|
117
|
+
* If the full parsed email fits, it is sent unchanged. Otherwise the large
|
|
118
|
+
* fields are dropped one at a time until it fits:
|
|
119
|
+
* 1. html, text and headers, largest first
|
|
120
|
+
* (html → null with htmlTruncated: true, text → null, headers → {})
|
|
121
|
+
* 2. attachments → [] (attachmentCount keeps the number)
|
|
122
|
+
* 3. to/cc → [] (toCount/ccCount kept), subject cut to 1000 characters
|
|
123
|
+
* 4. last resort ("envelope"): only emailId, rawS3Key, parsedS3Key,
|
|
124
|
+
* receivedAt, replyToken and autoReply, which are all bounded
|
|
125
|
+
* Ties keep the html, text, headers order; fields that are already empty
|
|
126
|
+
* are skipped. A trimmed detail carries
|
|
127
|
+
* `truncated: true`, `truncatedFields`, and `parsedS3Key`, the key of the
|
|
128
|
+
* full parsed JSON in the same bucket as `rawS3Key`.
|
|
129
|
+
*/
|
|
130
|
+
export function buildEventDetail(
|
|
131
|
+
email: TrimmableEmail,
|
|
132
|
+
parsedS3Key: string,
|
|
133
|
+
source: string,
|
|
134
|
+
maxBytes: number
|
|
135
|
+
): { detail: string; truncatedFields: string[]; fullBytes: number } {
|
|
136
|
+
const sizeOf = (candidate: string) =>
|
|
137
|
+
putEventsEntrySize({
|
|
138
|
+
Source: source,
|
|
139
|
+
DetailType: EMAIL_RECEIVED_DETAIL_TYPE,
|
|
140
|
+
Detail: candidate,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const full = JSON.stringify(email);
|
|
144
|
+
const fullBytes = sizeOf(full);
|
|
145
|
+
if (fullBytes <= maxBytes) {
|
|
146
|
+
return { detail: full, truncatedFields: [], fullBytes };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const truncatedFields: string[] = [];
|
|
150
|
+
const trimmed: Record<string, unknown> = {
|
|
151
|
+
...email,
|
|
152
|
+
truncated: true,
|
|
153
|
+
truncatedFields,
|
|
154
|
+
parsedS3Key,
|
|
155
|
+
};
|
|
156
|
+
const bodySteps: [string, number, () => void][] = [
|
|
157
|
+
[
|
|
158
|
+
"html",
|
|
159
|
+
Buffer.byteLength(JSON.stringify(email.html), "utf8"),
|
|
160
|
+
() => {
|
|
161
|
+
trimmed.html = null;
|
|
162
|
+
trimmed.htmlTruncated = true;
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
"text",
|
|
167
|
+
Buffer.byteLength(JSON.stringify(email.text), "utf8"),
|
|
168
|
+
() => {
|
|
169
|
+
trimmed.text = null;
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
"headers",
|
|
174
|
+
Buffer.byteLength(JSON.stringify(email.headers), "utf8"),
|
|
175
|
+
() => {
|
|
176
|
+
trimmed.headers = {};
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
];
|
|
180
|
+
const present: Record<string, boolean> = {
|
|
181
|
+
html: email.html !== null,
|
|
182
|
+
text: email.text !== null,
|
|
183
|
+
headers: Object.keys(email.headers).length > 0,
|
|
184
|
+
};
|
|
185
|
+
// Stable sort: equal sizes keep the html, text, headers order. Fields
|
|
186
|
+
// that are already empty are skipped so they are not reported as dropped.
|
|
187
|
+
bodySteps.sort((a, b) => b[1] - a[1]);
|
|
188
|
+
const steps: [string, () => void][] = [
|
|
189
|
+
...bodySteps
|
|
190
|
+
.filter(([field]) => present[field])
|
|
191
|
+
.map(([field, , drop]): [string, () => void] => [field, drop]),
|
|
192
|
+
[
|
|
193
|
+
"attachments",
|
|
194
|
+
() => {
|
|
195
|
+
trimmed.attachmentCount = email.attachments.length;
|
|
196
|
+
trimmed.attachments = [];
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
[
|
|
200
|
+
"recipients",
|
|
201
|
+
() => {
|
|
202
|
+
trimmed.toCount = email.to.length;
|
|
203
|
+
trimmed.ccCount = email.cc.length;
|
|
204
|
+
trimmed.to = [];
|
|
205
|
+
trimmed.cc = [];
|
|
206
|
+
trimmed.subject = email.subject.slice(0, MAX_FALLBACK_SUBJECT_LENGTH);
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
let detail = JSON.stringify(trimmed);
|
|
212
|
+
for (const [field, drop] of steps) {
|
|
213
|
+
if (sizeOf(detail) <= maxBytes) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
drop();
|
|
217
|
+
truncatedFields.push(field);
|
|
218
|
+
detail = JSON.stringify(trimmed);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Sender-controlled fields that are left (from.name, messageId, verdict
|
|
222
|
+
// headers) are unbounded. If they alone are too big, send an envelope of
|
|
223
|
+
// bounded fields only, so the event can never exceed the limit.
|
|
224
|
+
if (sizeOf(detail) > maxBytes) {
|
|
225
|
+
truncatedFields.push("envelope");
|
|
226
|
+
detail = JSON.stringify({
|
|
227
|
+
emailId: email.emailId,
|
|
228
|
+
rawS3Key: email.rawS3Key,
|
|
229
|
+
parsedS3Key,
|
|
230
|
+
receivedAt: email.receivedAt,
|
|
231
|
+
replyToken: email.replyToken,
|
|
232
|
+
autoReply: email.autoReply,
|
|
233
|
+
truncated: true,
|
|
234
|
+
truncatedFields,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return { detail, truncatedFields, fullBytes };
|
|
238
|
+
}
|
|
239
|
+
|
|
41
240
|
type CachedSecret = {
|
|
42
241
|
kid: number;
|
|
43
242
|
current: Buffer;
|
|
@@ -217,7 +416,9 @@ function detectAutoReply(headers: Record<string, unknown>): boolean {
|
|
|
217
416
|
* 3. Parse MIME → extract headers, body, attachments
|
|
218
417
|
* 4. Store attachments → S3 (attachments/{emailId}/{attId}-{filename})
|
|
219
418
|
* 5. Store parsed JSON → S3 (parsed/{emailId}.json)
|
|
220
|
-
* 6. Put event → EventBridge (source: "wraps.inbound", detail-type: "email.received")
|
|
419
|
+
* 6. Put event → EventBridge (source: "wraps.inbound", detail-type: "email.received").
|
|
420
|
+
* If the full parsed email does not fit the PutEvents size limit, the
|
|
421
|
+
* detail is trimmed (see buildEventDetail) and points at parsed/{emailId}.json.
|
|
221
422
|
*/
|
|
222
423
|
export async function handler(event: S3Event, context: Context) {
|
|
223
424
|
const requestId = context.awsRequestId;
|
|
@@ -249,6 +450,10 @@ export async function handler(event: S3Event, context: Context) {
|
|
|
249
450
|
for (const record of event.Records) {
|
|
250
451
|
const s3Key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
|
|
251
452
|
const bucket = record.s3.bucket.name;
|
|
453
|
+
// S3 event time is identical across retries; new Date() would differ per attempt.
|
|
454
|
+
const receivedAt = record.eventTime
|
|
455
|
+
? new Date(record.eventTime).toISOString()
|
|
456
|
+
: new Date().toISOString();
|
|
252
457
|
|
|
253
458
|
try {
|
|
254
459
|
log("Processing raw email", { bucket, key: s3Key });
|
|
@@ -265,8 +470,9 @@ export async function handler(event: S3Event, context: Context) {
|
|
|
265
470
|
// 2. Parse MIME
|
|
266
471
|
const parsed = await simpleParser(rawBody);
|
|
267
472
|
|
|
268
|
-
//
|
|
269
|
-
|
|
473
|
+
// Deterministic per raw object: a Lambda retry re-processes the same S3 key,
|
|
474
|
+
// so it must produce the same id (parsed JSON, attachments, event all key off it).
|
|
475
|
+
const emailId = `inb_${createHash("sha256").update(s3Key).digest("hex").slice(0, 16)}`;
|
|
270
476
|
|
|
271
477
|
// 3. Extract headers as key-value pairs
|
|
272
478
|
// mailparser returns most headers as strings, but some (list, received,
|
|
@@ -461,7 +667,7 @@ export async function handler(event: S3Event, context: Context) {
|
|
|
461
667
|
to: toAddresses,
|
|
462
668
|
cc: ccAddresses,
|
|
463
669
|
subject: parsed.subject || "",
|
|
464
|
-
date: parsed.date?.toISOString() ||
|
|
670
|
+
date: parsed.date?.toISOString() || receivedAt,
|
|
465
671
|
html,
|
|
466
672
|
htmlTruncated,
|
|
467
673
|
text: parsed.text || null,
|
|
@@ -470,16 +676,17 @@ export async function handler(event: S3Event, context: Context) {
|
|
|
470
676
|
spamVerdict,
|
|
471
677
|
virusVerdict,
|
|
472
678
|
rawS3Key: s3Key,
|
|
473
|
-
receivedAt
|
|
679
|
+
receivedAt,
|
|
474
680
|
replyToken,
|
|
475
681
|
autoReply,
|
|
476
682
|
};
|
|
477
683
|
|
|
478
684
|
// 7. Store parsed JSON at parsed/{emailId}.json
|
|
685
|
+
const parsedS3Key = `parsed/${emailId}.json`;
|
|
479
686
|
await s3.send(
|
|
480
687
|
new PutObjectCommand({
|
|
481
688
|
Bucket: BUCKET_NAME,
|
|
482
|
-
Key:
|
|
689
|
+
Key: parsedS3Key,
|
|
483
690
|
Body: JSON.stringify(parsedEmail),
|
|
484
691
|
ContentType: "application/json",
|
|
485
692
|
})
|
|
@@ -490,19 +697,50 @@ export async function handler(event: S3Event, context: Context) {
|
|
|
490
697
|
attachmentCount: attachments.length,
|
|
491
698
|
});
|
|
492
699
|
|
|
493
|
-
// 8. Put event to EventBridge
|
|
494
|
-
|
|
700
|
+
// 8. Put event to EventBridge, trimmed to fit the PutEvents size limit
|
|
701
|
+
const maxBytes = maxEventEntryBytes();
|
|
702
|
+
const { detail, truncatedFields, fullBytes } = buildEventDetail(
|
|
703
|
+
parsedEmail,
|
|
704
|
+
parsedS3Key,
|
|
705
|
+
INBOUND_EVENT_SOURCE,
|
|
706
|
+
maxBytes
|
|
707
|
+
);
|
|
708
|
+
if (truncatedFields.length > 0) {
|
|
709
|
+
log("Trimmed email.received detail to fit EventBridge limit", {
|
|
710
|
+
emailId,
|
|
711
|
+
fullBytes,
|
|
712
|
+
trimmedBytes: putEventsEntrySize({
|
|
713
|
+
Source: INBOUND_EVENT_SOURCE,
|
|
714
|
+
DetailType: EMAIL_RECEIVED_DETAIL_TYPE,
|
|
715
|
+
Detail: detail,
|
|
716
|
+
}),
|
|
717
|
+
maxBytes,
|
|
718
|
+
truncatedFields,
|
|
719
|
+
parsedS3Key,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const putResult = await eventbridge.send(
|
|
495
724
|
new PutEventsCommand({
|
|
496
725
|
Entries: [
|
|
497
726
|
{
|
|
498
727
|
Source: INBOUND_EVENT_SOURCE,
|
|
499
|
-
DetailType:
|
|
500
|
-
Detail:
|
|
728
|
+
DetailType: EMAIL_RECEIVED_DETAIL_TYPE,
|
|
729
|
+
Detail: detail,
|
|
501
730
|
},
|
|
502
731
|
],
|
|
503
732
|
})
|
|
504
733
|
);
|
|
505
734
|
|
|
735
|
+
// PutEvents reports per-entry failures in a 200 response, so a
|
|
736
|
+
// failed entry must be surfaced explicitly to trigger a retry.
|
|
737
|
+
if ((putResult.FailedEntryCount ?? 0) > 0) {
|
|
738
|
+
const failed = (putResult.Entries ?? []).find((e) => e.ErrorCode);
|
|
739
|
+
throw new Error(
|
|
740
|
+
`EventBridge PutEvents failed for ${emailId}: ${failed?.ErrorCode ?? "UnknownError"} ${failed?.ErrorMessage ?? ""}`.trim()
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
506
744
|
log("Processed inbound", {
|
|
507
745
|
emailId,
|
|
508
746
|
sendingDomain: replyRecipient?.sendingDomain ?? null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Built at: 2026-10-
|
|
1
|
+
Built at: 2026-10-26T19:27:42.372Z
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wraps.dev/cli",
|
|
3
|
-
"version": "3.17.
|
|
3
|
+
"version": "3.17.4",
|
|
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",
|
|
@@ -46,32 +46,32 @@
|
|
|
46
46
|
"author": "Wraps",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@aws-sdk/client-acm": "3.
|
|
50
|
-
"@aws-sdk/client-cloudformation": "3.
|
|
51
|
-
"@aws-sdk/client-cloudfront": "3.
|
|
52
|
-
"@aws-sdk/client-cloudwatch": "3.
|
|
53
|
-
"@aws-sdk/client-cloudwatch-logs": "3.
|
|
54
|
-
"@aws-sdk/client-dynamodb": "3.
|
|
55
|
-
"@aws-sdk/client-eventbridge": "3.
|
|
56
|
-
"@aws-sdk/client-iam": "3.
|
|
57
|
-
"@aws-sdk/client-lambda": "3.
|
|
58
|
-
"@aws-sdk/client-mailmanager": "3.
|
|
59
|
-
"@aws-sdk/client-pinpoint-sms-voice-v2": "3.
|
|
60
|
-
"@aws-sdk/client-route-53": "3.
|
|
61
|
-
"@aws-sdk/client-s3": "3.
|
|
62
|
-
"@aws-sdk/client-ses": "3.
|
|
63
|
-
"@aws-sdk/client-sesv2": "3.
|
|
64
|
-
"@aws-sdk/client-sns": "3.
|
|
65
|
-
"@aws-sdk/client-sqs": "3.
|
|
66
|
-
"@aws-sdk/client-ssm": "3.
|
|
67
|
-
"@aws-sdk/client-sts": "3.
|
|
68
|
-
"@aws-sdk/s3-request-presigner": "3.
|
|
69
|
-
"@aws-sdk/util-dynamodb": "3.996.
|
|
49
|
+
"@aws-sdk/client-acm": "3.1152.0",
|
|
50
|
+
"@aws-sdk/client-cloudformation": "3.1152.0",
|
|
51
|
+
"@aws-sdk/client-cloudfront": "3.1152.0",
|
|
52
|
+
"@aws-sdk/client-cloudwatch": "3.1152.0",
|
|
53
|
+
"@aws-sdk/client-cloudwatch-logs": "3.1152.0",
|
|
54
|
+
"@aws-sdk/client-dynamodb": "3.1152.0",
|
|
55
|
+
"@aws-sdk/client-eventbridge": "3.1152.0",
|
|
56
|
+
"@aws-sdk/client-iam": "3.1152.0",
|
|
57
|
+
"@aws-sdk/client-lambda": "3.1152.0",
|
|
58
|
+
"@aws-sdk/client-mailmanager": "3.1152.0",
|
|
59
|
+
"@aws-sdk/client-pinpoint-sms-voice-v2": "3.1152.0",
|
|
60
|
+
"@aws-sdk/client-route-53": "3.1152.0",
|
|
61
|
+
"@aws-sdk/client-s3": "3.1152.0",
|
|
62
|
+
"@aws-sdk/client-ses": "3.1152.0",
|
|
63
|
+
"@aws-sdk/client-sesv2": "3.1152.0",
|
|
64
|
+
"@aws-sdk/client-sns": "3.1152.0",
|
|
65
|
+
"@aws-sdk/client-sqs": "3.1152.0",
|
|
66
|
+
"@aws-sdk/client-ssm": "3.1152.0",
|
|
67
|
+
"@aws-sdk/client-sts": "3.1152.0",
|
|
68
|
+
"@aws-sdk/s3-request-presigner": "3.1152.0",
|
|
69
|
+
"@aws-sdk/util-dynamodb": "3.996.9",
|
|
70
70
|
"@clack/prompts": "^0.11.0",
|
|
71
71
|
"@pulumi/aws": "^7.39.0",
|
|
72
72
|
"@pulumi/pulumi": "^3.254.0",
|
|
73
73
|
"@react-email/render": "2.1.0",
|
|
74
|
-
"better-auth": "1.7.
|
|
74
|
+
"better-auth": "1.7.11",
|
|
75
75
|
"conf": "^15.1.0",
|
|
76
76
|
"cosmiconfig": "^9.0.2",
|
|
77
77
|
"drizzle-orm": "^0.45.2",
|
|
@@ -99,16 +99,16 @@
|
|
|
99
99
|
"@vitest/coverage-v8": "4.1.11",
|
|
100
100
|
"aws-sdk-client-mock": "4.1.0",
|
|
101
101
|
"aws-sdk-client-mock-vitest": "7.0.1",
|
|
102
|
-
"handlebars": "4.7.
|
|
102
|
+
"handlebars": "4.7.10",
|
|
103
103
|
"tsup": "^8.5.1",
|
|
104
104
|
"tsx": "4.20.6",
|
|
105
105
|
"typescript": "^5.9.3",
|
|
106
106
|
"vitest": "4.1.11",
|
|
107
|
-
"@wraps.dev/pulumi": "0.6.0",
|
|
108
107
|
"@wraps.dev/email-check": "1.2.0",
|
|
109
108
|
"@wraps/core": "0.1.2",
|
|
109
|
+
"@wraps/template-render": "0.1.0",
|
|
110
110
|
"@wraps/db": "0.0.0",
|
|
111
|
-
"@wraps/
|
|
111
|
+
"@wraps.dev/pulumi": "0.6.0"
|
|
112
112
|
},
|
|
113
113
|
"engines": {
|
|
114
114
|
"node": ">=22.0.0"
|