ts-ag 1.1.26 → 1.2.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.
@@ -0,0 +1,1603 @@
1
+ import { parse, stringify } from "devalue";
2
+ import * as v from "valibot";
3
+ import { parse as parse$1 } from "cookie";
4
+ import { Result, ResultAsync } from "neverthrow";
5
+ import { AdminGetUserCommand, AdminInitiateAuthCommand, AdminListGroupsForUserCommand, ChangePasswordCommand, CognitoIdentityProviderClient, ConfirmForgotPasswordCommand, ConfirmSignUpCommand, ForgotPasswordCommand, GlobalSignOutCommand, RespondToAuthChallengeCommand, SignUpCommand } from "@aws-sdk/client-cognito-identity-provider";
6
+ import { createHmac } from "node:crypto";
7
+ import { GetObjectCommand, HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";
8
+ import { getSignedUrl as getSignedUrl$1 } from "@aws-sdk/s3-request-presigner";
9
+ import { DynamoDBToolboxError } from "dynamodb-toolbox";
10
+ import rehypeParse from "rehype-parse";
11
+ import { unified } from "unified";
12
+ import { isEqual, isObject } from "radash";
13
+ import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import { dirname } from "node:path";
15
+ import chalk from "chalk";
16
+ //#region src/lambda/client-types.ts
17
+ const HTTPMethods = [
18
+ "GET",
19
+ "POST",
20
+ "PUT",
21
+ "DELETE",
22
+ "PATCH",
23
+ "OPTIONS",
24
+ "HEAD"
25
+ ];
26
+ //#endregion
27
+ //#region src/lambda/client.ts
28
+ const bodyMethods = [
29
+ "POST",
30
+ "PUT",
31
+ "PATCH"
32
+ ];
33
+ const queryMethods = ["GET", "DELETE"];
34
+ async function _apiRequest(path, method, input, schema, environment, apiUrl, headers) {
35
+ if (schema) if (schema.async === true) await v.parseAsync(schema, input);
36
+ else v.parse(schema, input);
37
+ let url = `${apiUrl}${apiUrl.endsWith("/") ? "" : "/"}${path}`;
38
+ if (queryMethods.includes(method)) {
39
+ const params = new URLSearchParams();
40
+ for (const [key, value] of Object.entries(input ?? {})) if (Array.isArray(value)) value.forEach((item) => params.append(key, String(item)));
41
+ else params.append(key, String(value));
42
+ const queryString = params.toString();
43
+ if (queryString) url += `?${queryString}`;
44
+ }
45
+ headers = {
46
+ "Content-Type": "application/json",
47
+ ...headers
48
+ };
49
+ const response = await fetch(url, {
50
+ method,
51
+ headers,
52
+ body: bodyMethods.includes(method) ? JSON.stringify(input) : void 0,
53
+ credentials: "include"
54
+ });
55
+ const contentType = response.headers.get("content-type") ?? "";
56
+ let retrieved = false;
57
+ response.json = async () => {
58
+ if (retrieved === false) retrieved = response.text();
59
+ if (contentType === "application/devalue") return await parse(await retrieved);
60
+ else return JSON.parse(await retrieved);
61
+ };
62
+ return response;
63
+ }
64
+ /**
65
+ * @returns A function that can be used to make API requests with type safety
66
+ * @example const clientApiRequest = createApiRequest<ApiEndpoints>();
67
+ */
68
+ function createApiRequest(schemas, apiUrl, env) {
69
+ return async function apiRequest(path, method, input, headers) {
70
+ const schema = schemas[path]?.[method];
71
+ if (schema === void 0) throw new Error("Schema is undefined in api request");
72
+ return _apiRequest(path, method, input, schema, env, apiUrl, headers);
73
+ };
74
+ }
75
+ //#endregion
76
+ //#region src/lambda/handlerUtils.ts
77
+ /**
78
+ * Wraps a handler that returns the body as an object rather than a string.
79
+ *
80
+ * This means you can achieve proper type inference on your handler and have standardised serialisation
81
+ *
82
+ * @example
83
+ ```ts
84
+ export type AuthorizerContext = {
85
+ details: JWTPayload;
86
+ } | null;
87
+
88
+ export const wrapHandler = baseWrapHandler<APIGatewayProxyEventV2WithLambdaAuthorizer<AuthorizerContext>>
89
+
90
+ */
91
+ function wrapHandler(handler) {
92
+ return async (event, context) => {
93
+ const result = await handler(event, context);
94
+ if (result.body) {
95
+ const headers = new Headers(result.headers);
96
+ headers.set("Content-Type", "application/devalue");
97
+ return {
98
+ ...result,
99
+ headers: Object.fromEntries(headers),
100
+ body: stringify(result.body)
101
+ };
102
+ } else return {
103
+ ...result,
104
+ body: void 0
105
+ };
106
+ };
107
+ }
108
+ //#endregion
109
+ //#region src/lambda/errors.ts
110
+ /**
111
+ * These are the various errors that should be returned from anything called by a lambda function.
112
+ *
113
+ * Pass a lambda error to the errorResponse function to get a suitable response to return from the lambda handler.
114
+ *
115
+ * The separation means that they can be returned from functions that are certainly run inside a lambda fucntion but theyre not the actual return of the lambda.
116
+ * Im not sure it this is optimal behaviour and if not we will migrate to only using the errorResponse function
117
+ */
118
+ const error_lambda_badRequest = (message, fieldName, fieldValue) => ({
119
+ type: "badRequest",
120
+ message,
121
+ fieldName,
122
+ fieldValue
123
+ });
124
+ const error_lambda_unauthorized = (message) => ({
125
+ type: "unauthorized",
126
+ message
127
+ });
128
+ const error_lambda_forbidden = (message) => ({
129
+ type: "forbidden",
130
+ message
131
+ });
132
+ const error_lambda_notFound = (message, fieldName, fieldValue) => ({
133
+ type: "notFound",
134
+ message,
135
+ fieldName,
136
+ fieldValue
137
+ });
138
+ const error_lambda_conflict = (message, fieldName, fieldValue) => ({
139
+ type: "conflict",
140
+ message,
141
+ fieldName,
142
+ fieldValue
143
+ });
144
+ const error_lambda_internal = (message) => ({
145
+ type: "internal",
146
+ message
147
+ });
148
+ //#endregion
149
+ //#region src/lambda/response.ts
150
+ function field(obj) {
151
+ return obj.fieldName === void 0 || obj.fieldValue === void 0 ? {} : { field: {
152
+ name: obj.fieldName,
153
+ value: obj.fieldValue
154
+ } };
155
+ }
156
+ /**
157
+ * Maps lambda errors to responses suitable to return from lambda functions
158
+ * @param e
159
+ * @param headers
160
+ * @param type
161
+ * @param extras
162
+ * @returns
163
+ */
164
+ function response_error(e, headers, type = "", extras = {}) {
165
+ switch (e.type) {
166
+ case "badRequest": return {
167
+ headers,
168
+ statusCode: 400,
169
+ body: {
170
+ message: e.message,
171
+ type,
172
+ ...field(e),
173
+ ...extras
174
+ }
175
+ };
176
+ case "unauthorized": return {
177
+ headers,
178
+ statusCode: 401,
179
+ body: {
180
+ message: e.message,
181
+ type,
182
+ ...extras
183
+ }
184
+ };
185
+ case "forbidden": return {
186
+ headers,
187
+ statusCode: 403,
188
+ body: {
189
+ message: e.message,
190
+ type,
191
+ ...extras
192
+ }
193
+ };
194
+ case "notFound": return {
195
+ headers,
196
+ statusCode: 404,
197
+ body: {
198
+ message: e.message,
199
+ type,
200
+ ...field(e),
201
+ ...extras
202
+ }
203
+ };
204
+ case "conflict": return {
205
+ headers,
206
+ statusCode: 409,
207
+ body: {
208
+ message: e.message,
209
+ type,
210
+ ...field(e),
211
+ ...extras
212
+ }
213
+ };
214
+ default: return {
215
+ headers,
216
+ statusCode: 500,
217
+ body: {
218
+ message: "Unknown error",
219
+ type,
220
+ ...extras
221
+ }
222
+ };
223
+ }
224
+ }
225
+ /**
226
+ * Helper function to get a reasonable default error response from a valibot parse result
227
+ * @param res - The output from calling safeParse on the input
228
+ * @param headers - The headers to return in the response
229
+ */
230
+ function response_valibotError(res, headers) {
231
+ const issue = res.issues[0];
232
+ if (issue.path && issue.path[0] && typeof issue.path[0].key === "string") return response_error(error_lambda_badRequest("Invalid input", issue.path[0].key, issue.message), headers);
233
+ else return response_error(error_lambda_badRequest(`Invalid input: ${issue.message}`), headers);
234
+ }
235
+ function response_ok(body, headers, cookies) {
236
+ return {
237
+ headers,
238
+ cookies,
239
+ statusCode: 200,
240
+ body
241
+ };
242
+ }
243
+ //#endregion
244
+ //#region src/lambda/server/authentication.ts
245
+ /**
246
+ * Wraps cookies parse along with the api gateway event with neverthrow
247
+ */
248
+ const getCookies = Result.fromThrowable((event) => {
249
+ if (!("headers" in event) || !event.headers) throw new Error("No headers in event");
250
+ const cookieString = Array.isArray(event.cookies) && event.cookies.length > 0 ? event.cookies.join("; ") : event.headers.Cookie || event.headers.cookie;
251
+ if (!cookieString) throw new Error("No cookies found in event");
252
+ return parse$1(cookieString);
253
+ }, (e) => {
254
+ if (e instanceof Error) return error_lambda_unauthorized(e.message);
255
+ return error_lambda_unauthorized("Invalid Cookies");
256
+ });
257
+ //#endregion
258
+ //#region src/cognito/client.ts
259
+ /**
260
+ * Convenience function if process.env.AWS_REGION is set
261
+ */
262
+ function getCognitoClient() {
263
+ return new CognitoIdentityProviderClient({});
264
+ }
265
+ //#endregion
266
+ //#region src/cognito/errors.ts
267
+ const defaultErrors$3 = {
268
+ auth: {
269
+ type: "unauthorized",
270
+ message: "Not authorized"
271
+ },
272
+ forbidden: {
273
+ type: "forbidden",
274
+ message: "Forbidden"
275
+ },
276
+ invalidInput: {
277
+ type: "badRequest",
278
+ message: "There is an issue with your request"
279
+ },
280
+ userNotFound: {
281
+ type: "notFound",
282
+ message: "User not found"
283
+ },
284
+ resourceNotFound: {
285
+ type: "notFound",
286
+ message: "Resource not found"
287
+ },
288
+ tooManyRequests: {
289
+ type: "badRequest",
290
+ message: "Too many requests"
291
+ },
292
+ passwordPolicy: {
293
+ type: "badRequest",
294
+ message: "Password does not meet policy requirements"
295
+ },
296
+ passwordHistory: {
297
+ type: "conflict",
298
+ message: "Password was used recently"
299
+ },
300
+ passwordResetRequired: {
301
+ type: "badRequest",
302
+ message: "Password reset required"
303
+ },
304
+ codeExpired: {
305
+ type: "badRequest",
306
+ message: "Code expired"
307
+ },
308
+ codeMismatch: {
309
+ type: "badRequest",
310
+ message: "Invalid code"
311
+ },
312
+ delivery: {
313
+ type: "internal",
314
+ message: "Internal server error"
315
+ },
316
+ userExists: {
317
+ type: "conflict",
318
+ message: "User already exists"
319
+ },
320
+ conflict: {
321
+ type: "conflict",
322
+ message: "The request conflicts with the current Cognito resource state"
323
+ },
324
+ internal: {
325
+ type: "internal",
326
+ message: "Internal server error"
327
+ }
328
+ };
329
+ /** Wrap an unknown caught value as a Cognito-domain error for neverthrow flows. */
330
+ function error_cognito(error) {
331
+ return {
332
+ type: "cognito",
333
+ error
334
+ };
335
+ }
336
+ /** Convert AWS SDK Cognito errors into a safe lambda error for API responses. */
337
+ function error_lambda_fromCognito(e, options = {}) {
338
+ return fromReason$2(getCognitoReason(e.error), options);
339
+ }
340
+ /** Apply endpoint overrides and build the concrete lambda error object. */
341
+ function fromReason$2(reason, options) {
342
+ const base = defaultErrors$3[reason];
343
+ const override = options[reason];
344
+ const args = {
345
+ ...base,
346
+ ...override
347
+ };
348
+ switch (args.type) {
349
+ case "badRequest": return error_lambda_badRequest(args.message, args.fieldName, args.fieldValue);
350
+ case "unauthorized": return error_lambda_unauthorized(args.message);
351
+ case "forbidden": return error_lambda_forbidden(args.message);
352
+ case "notFound": return error_lambda_notFound(args.message, args.fieldName, args.fieldValue);
353
+ case "conflict": return error_lambda_conflict(args.message, args.fieldName, args.fieldValue);
354
+ default: return error_lambda_internal(args.message);
355
+ }
356
+ }
357
+ /** Classify AWS SDK / Cognito service errors. */
358
+ function getCognitoReason(error) {
359
+ switch (getErrorName$2(error)) {
360
+ case "NotAuthorizedException":
361
+ case "UnauthorizedException":
362
+ case "UserNotConfirmedException":
363
+ case "RefreshTokenReuseException": return "auth";
364
+ case "AccessDeniedException":
365
+ case "ForbiddenException": return "forbidden";
366
+ case "InvalidParameterException":
367
+ case "InvalidOAuthFlowException":
368
+ case "ScopeDoesNotExistException":
369
+ case "UnsupportedIdentityProviderException":
370
+ case "UnsupportedTokenTypeException": return "invalidInput";
371
+ case "UserNotFoundException": return "userNotFound";
372
+ case "ResourceNotFoundException":
373
+ case "MFAMethodNotFoundException":
374
+ case "SoftwareTokenMFANotFoundException":
375
+ case "WebAuthnChallengeNotFoundException": return "resourceNotFound";
376
+ case "LimitExceededException":
377
+ case "TooManyFailedAttemptsException":
378
+ case "TooManyRequestsException": return "tooManyRequests";
379
+ case "InvalidPasswordException": return "passwordPolicy";
380
+ case "PasswordHistoryPolicyViolationException": return "passwordHistory";
381
+ case "PasswordResetRequiredException": return "passwordResetRequired";
382
+ case "ExpiredCodeException": return "codeExpired";
383
+ case "CodeMismatchException": return "codeMismatch";
384
+ case "CodeDeliveryFailureException": return "delivery";
385
+ case "AliasExistsException":
386
+ case "DeviceKeyExistsException":
387
+ case "DuplicateProviderException":
388
+ case "GroupExistsException":
389
+ case "ManagedLoginBrandingExistsException":
390
+ case "TermsExistsException":
391
+ case "UsernameExistsException": return "userExists";
392
+ case "ConcurrentModificationException":
393
+ case "PreconditionNotMetException":
394
+ case "UnsupportedUserStateException": return "conflict";
395
+ case "EnableSoftwareTokenMFAException":
396
+ case "FeatureUnavailableInTierException":
397
+ case "InternalErrorException":
398
+ case "InternalServerException":
399
+ case "InvalidEmailRoleAccessPolicyException":
400
+ case "InvalidLambdaResponseException":
401
+ case "InvalidSmsRoleAccessPolicyException":
402
+ case "InvalidSmsRoleTrustRelationshipException":
403
+ case "InvalidUserPoolConfigurationException":
404
+ case "TierChangeNotAllowedException":
405
+ case "UnexpectedLambdaException":
406
+ case "UnsupportedOperationException":
407
+ case "UserImportInProgressException":
408
+ case "UserLambdaValidationException":
409
+ case "UserPoolAddOnNotEnabledException":
410
+ case "UserPoolTaggingException":
411
+ case "WebAuthnClientMismatchException":
412
+ case "WebAuthnConfigurationMissingException":
413
+ case "WebAuthnCredentialNotSupportedException":
414
+ case "WebAuthnNotEnabledException":
415
+ case "WebAuthnOriginNotAllowedException":
416
+ case "WebAuthnRelyingPartyMismatchException": return "internal";
417
+ default: return getHttpStatusReason$2(error);
418
+ }
419
+ }
420
+ function getHttpStatusReason$2(error) {
421
+ const status = getHttpStatusCode$2(error);
422
+ if (status === 400) return "invalidInput";
423
+ if (status === 401) return "auth";
424
+ if (status === 403) return "forbidden";
425
+ if (status === 404) return "resourceNotFound";
426
+ if (status === 409 || status === 412) return "conflict";
427
+ if (status === 429) return "tooManyRequests";
428
+ return "internal";
429
+ }
430
+ function getErrorName$2(error) {
431
+ if (!isRecord$2(error)) return void 0;
432
+ const name = error.name;
433
+ if (typeof name === "string") return name;
434
+ const code = error.code ?? error.Code;
435
+ if (typeof code === "string") return code;
436
+ }
437
+ function getHttpStatusCode$2(error) {
438
+ if (!isRecord$2(error)) return void 0;
439
+ const metadata = error.$metadata;
440
+ if (!isRecord$2(metadata)) return void 0;
441
+ return typeof metadata.httpStatusCode === "number" ? metadata.httpStatusCode : void 0;
442
+ }
443
+ function isRecord$2(value) {
444
+ return typeof value === "object" && value !== null;
445
+ }
446
+ //#endregion
447
+ //#region src/cognito/user.ts
448
+ /**
449
+ * Performs an AdminGetUserCommand and extracts the user attributes into an object
450
+ */
451
+ const getUserDetails = ResultAsync.fromThrowable(async (a) => {
452
+ console.log("getUserDetails: Getting details for user: ", a.username);
453
+ const res = await getCognitoClient().send(new AdminGetUserCommand({
454
+ UserPoolId: a.userPoolId,
455
+ Username: a.username
456
+ }));
457
+ return {
458
+ ...res,
459
+ UserAttributes: extractAttributes(res.UserAttributes)
460
+ };
461
+ }, (e) => {
462
+ console.error("getUserDetails:error:", e);
463
+ return error_cognito(e);
464
+ });
465
+ /**
466
+ * @returns An object of attributes with their names as keys and values as values.
467
+ */
468
+ function extractAttributes(attrs) {
469
+ const attributes = {};
470
+ if (attrs) {
471
+ for (const attr of attrs) if (attr.Name && attr.Value) attributes[attr.Name] = attr.Value;
472
+ }
473
+ return attributes;
474
+ }
475
+ /**
476
+ * Performs an AdminGetUserCommand and extracts the user attributes into an object
477
+ */
478
+ const getUserGroups = ResultAsync.fromThrowable(async (a) => {
479
+ console.log("getUserGroups: Getting groups for user: ", a.username);
480
+ return await getCognitoClient().send(new AdminListGroupsForUserCommand({
481
+ UserPoolId: a.userPoolId,
482
+ Username: a.username
483
+ }));
484
+ }, (e) => {
485
+ console.error("getUserGroups:error:", e);
486
+ return error_cognito(e);
487
+ });
488
+ //#endregion
489
+ //#region src/cognito/password.ts
490
+ /**
491
+ * Computes Cognito secret hash used for client-side authentication flows.
492
+ *
493
+ * @param username - Cognito username or alias.
494
+ * @param clientId - Cognito app client ID.
495
+ * @param clientSecret - Cognito app client secret.
496
+ */
497
+ function computeSecretHash(username, clientId, clientSecret) {
498
+ console.log("computeSecretHash: ", username, clientId, clientSecret);
499
+ return createHmac("SHA256", clientSecret).update(username + clientId).digest("base64");
500
+ }
501
+ /**
502
+ * Changes a user's password given a valid access token.
503
+ *
504
+ * @param accessToken - Access token for the authenticated user.
505
+ * @param oldPassword - Current password.
506
+ * @param newPassword - New password to set.
507
+ */
508
+ const changePassword = ResultAsync.fromThrowable(async (accessToken, oldPassword, newPassword) => {
509
+ return getCognitoClient().send(new ChangePasswordCommand({
510
+ AccessToken: accessToken,
511
+ PreviousPassword: oldPassword,
512
+ ProposedPassword: newPassword
513
+ }));
514
+ }, (e) => {
515
+ console.error("ChangePasswordCommand error", e);
516
+ return error_cognito(e);
517
+ });
518
+ /**
519
+ * Completes a forgot-password flow by submitting the confirmation code and new password.
520
+ *
521
+ * @param a.username - Cognito username or alias.
522
+ * @param a.confirmationCode - Code sent by Cognito to the user.
523
+ * @param a.newPassword - New password to set.
524
+ * @param a.clientId - Cognito app client ID.
525
+ * @param a.clientSecret - Cognito app client secret.
526
+ */
527
+ const confirmForgotPassword = ResultAsync.fromThrowable((a) => {
528
+ return getCognitoClient().send(new ConfirmForgotPasswordCommand({
529
+ ClientId: a.clientId,
530
+ Username: a.username,
531
+ ConfirmationCode: a.confirmationCode,
532
+ Password: a.newPassword,
533
+ SecretHash: computeSecretHash(a.username, a.clientId, a.clientSecret)
534
+ }));
535
+ }, (e) => {
536
+ console.error("ConfirmForgotPasswordCommand error", e);
537
+ return error_cognito(e);
538
+ });
539
+ /**
540
+ * Confirms a user's signup using the confirmation code sent by Cognito.
541
+ *
542
+ * @param a.username - Cognito username or alias.
543
+ * @param a.confirmationCode - Code sent to the user after signup.
544
+ * @param a.clientId - Cognito app client ID.
545
+ * @param a.clientSecret - Cognito app client secret.
546
+ */
547
+ const confirmSignup = ResultAsync.fromThrowable((a) => {
548
+ return getCognitoClient().send(new ConfirmSignUpCommand({
549
+ ClientId: a.clientId,
550
+ Username: a.username,
551
+ ConfirmationCode: a.confirmationCode,
552
+ SecretHash: computeSecretHash(a.username, a.clientId, a.clientSecret)
553
+ }));
554
+ }, (e) => {
555
+ console.error("ConfirmSignUpCommand error", e);
556
+ return error_cognito(e);
557
+ });
558
+ /**
559
+ * Starts a forgot-password flow by sending a reset code to the user.
560
+ *
561
+ * @param a.username - Cognito username or alias.
562
+ * @param a.clientId - Cognito app client ID.
563
+ * @param a.clientSecret - Cognito app client secret.
564
+ */
565
+ const forgotPassword = ResultAsync.fromThrowable((a) => {
566
+ return getCognitoClient().send(new ForgotPasswordCommand({
567
+ ClientId: a.clientId,
568
+ Username: a.username,
569
+ SecretHash: computeSecretHash(a.username, a.clientId, a.clientSecret)
570
+ }));
571
+ }, (e) => {
572
+ console.error("ForgotPasswordCommand error", e);
573
+ return error_cognito(e);
574
+ });
575
+ /**
576
+ * Signs a user in with ADMIN_USER_PASSWORD_AUTH.
577
+ *
578
+ * @param a.username - Cognito username or alias.
579
+ * @param a.password - User password.
580
+ * @param a.clientId - Cognito app client ID.
581
+ * @param a.clientSecret - Cognito app client secret.
582
+ * @param a.userPoolId - Cognito user pool ID.
583
+ */
584
+ const login = ResultAsync.fromThrowable((a) => {
585
+ return getCognitoClient().send(new AdminInitiateAuthCommand({
586
+ AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
587
+ ClientId: a.clientId,
588
+ UserPoolId: a.userPoolId,
589
+ AuthParameters: {
590
+ USERNAME: a.username,
591
+ PASSWORD: a.password,
592
+ SECRET_HASH: computeSecretHash(a.username, a.clientId, a.clientSecret)
593
+ }
594
+ }));
595
+ }, (e) => {
596
+ console.error("AdminInitiateAuthCommand error", e);
597
+ return error_cognito(e);
598
+ });
599
+ /**
600
+ * Exchanges a refresh token for new tokens.
601
+ *
602
+ * @param a.username - Cognito username or alias used to compute secret hash.
603
+ * @param a.refreshToken - Refresh token to exchange.
604
+ * @param a.clientId - Cognito app client ID.
605
+ * @param a.clientSecret - Cognito app client secret.
606
+ * @param a.userPoolId - Cognito user pool ID.
607
+ */
608
+ const refreshTokens = ResultAsync.fromThrowable((a) => {
609
+ return getCognitoClient().send(new AdminInitiateAuthCommand({
610
+ AuthFlow: "REFRESH_TOKEN_AUTH",
611
+ ClientId: a.clientId,
612
+ UserPoolId: a.userPoolId,
613
+ AuthParameters: {
614
+ REFRESH_TOKEN: a.refreshToken,
615
+ SECRET_HASH: computeSecretHash(a.username, a.clientId, a.clientSecret)
616
+ }
617
+ }));
618
+ }, (e) => {
619
+ console.error("refreshTokens: AdminInitiateAuthCommand error", e);
620
+ return error_cognito(e);
621
+ });
622
+ /**
623
+ * Globally signs out a user by invalidating all refresh tokens.
624
+ *
625
+ * @param accessToken - Access token for the authenticated user.
626
+ */
627
+ const logout = ResultAsync.fromThrowable((accessToken) => {
628
+ return getCognitoClient().send(new GlobalSignOutCommand({ AccessToken: accessToken }));
629
+ }, (e) => {
630
+ console.error("GlobalSignOutCommand error", e);
631
+ return error_cognito(e);
632
+ });
633
+ /**
634
+ * Completes a NEW_PASSWORD_REQUIRED challenge for users who must set a new password.
635
+ *
636
+ * @param a.session - Session returned from the auth challenge.
637
+ * @param a.newPassword - New password to set.
638
+ * @param a.username - Cognito username or alias.
639
+ * @param a.clientId - Cognito app client ID.
640
+ * @param a.clientSecret - Cognito app client secret.
641
+ */
642
+ const resetPassword = ResultAsync.fromThrowable((a) => {
643
+ return getCognitoClient().send(new RespondToAuthChallengeCommand({
644
+ ChallengeName: "NEW_PASSWORD_REQUIRED",
645
+ ClientId: a.clientId,
646
+ Session: a.session,
647
+ ChallengeResponses: {
648
+ SECRET_HASH: computeSecretHash(a.username, a.clientId, a.clientSecret),
649
+ NEW_PASSWORD: a.newPassword,
650
+ USERNAME: a.username
651
+ }
652
+ }));
653
+ }, (e) => {
654
+ console.error("RespondToAuthChallengeCommand error", e);
655
+ return error_cognito(e);
656
+ });
657
+ /**
658
+ * Registers a new user with Cognito and optional custom attributes.
659
+ *
660
+ * @param a.username - Cognito username.
661
+ * @param a.password - User password.
662
+ * @param a.clientId - Cognito app client ID.
663
+ * @param a.clientSecret - Cognito app client secret.
664
+ * @param a.<attribute> - Any additional user attributes to set.
665
+ */
666
+ const signUp = ResultAsync.fromThrowable((a) => {
667
+ const cognitoClient = getCognitoClient();
668
+ const secretHash = computeSecretHash(a.username, a.clientId, a.clientSecret);
669
+ return cognitoClient.send(new SignUpCommand({
670
+ ClientId: a.clientId,
671
+ Username: a.username,
672
+ Password: a.password,
673
+ SecretHash: secretHash,
674
+ UserAttributes: Object.entries(a).filter(([key]) => ![
675
+ "username",
676
+ "password",
677
+ "clientId",
678
+ "clientSecret"
679
+ ].includes(key)).map(([key, value]) => ({
680
+ Name: key,
681
+ Value: value
682
+ }))
683
+ }));
684
+ }, (e) => {
685
+ console.error("SignUpCommand error", e);
686
+ return error_cognito(e);
687
+ });
688
+ /**
689
+ * Exchanges an OAuth2 authorization code for Cognito tokens using the token endpoint.
690
+ * See https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html for request/response fields and grant details.
691
+ *
692
+ * @param a.code - Authorization code returned by the hosted UI.
693
+ * @param a.redirectUri - Redirect URI registered with the app client.
694
+ * @param a.clientId - Cognito app client ID.
695
+ * @param a.clientSecret - Cognito app client secret used for Basic Auth.
696
+ * @param a.cognitoDomain - Cognito domain URL (e.g., your-domain.auth.region.amazoncognito.com).
697
+ * @returns Parsed token payload containing `access_token`, `id_token`, `refresh_token`, token type, and expiry.
698
+ */
699
+ const verifyOAuthToken = ResultAsync.fromThrowable(async (a) => {
700
+ const basicAuth = Buffer.from(`${a.clientId}:${a.clientSecret}`).toString("base64");
701
+ const params = new URLSearchParams();
702
+ params.append("grant_type", "authorization_code");
703
+ params.append("code", a.code);
704
+ params.append("redirect_uri", a.redirectUri);
705
+ console.log("verifyOAuthToken: params", params.toString());
706
+ const tokenRes = await fetch(`https://${a.cognitoDomain}/oauth2/token`, {
707
+ method: "POST",
708
+ headers: {
709
+ "Content-Type": "application/x-www-form-urlencoded",
710
+ Authorization: `Basic ${basicAuth}`
711
+ },
712
+ body: params.toString()
713
+ });
714
+ if (!tokenRes.ok) {
715
+ console.error("verifyOAuthToken: token exchange failed", await tokenRes.text());
716
+ throw Object.assign(/* @__PURE__ */ new Error("OAuth token exchange failed"), { name: "NotAuthorizedException" });
717
+ }
718
+ return await tokenRes.json();
719
+ }, (e) => {
720
+ console.error("verifyOAuthToken:error", e);
721
+ return error_cognito(e);
722
+ });
723
+ /**
724
+ * Exchanges an OAuth2 refresh token for Cognito tokens using the oauth token endpoint.
725
+ * See https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html for request/response fields and grant details.
726
+ *
727
+ * @param a.redirectUri - Redirect URI registered with the app client.
728
+ * @param a.clientId - Cognito app client ID.
729
+ * @param a.clientSecret - Cognito app client secret used for Basic Auth.
730
+ * @param a.cognitoDomain - Cognito domain URL (e.g., your-domain.auth.region.amazoncognito.com).
731
+ * @returns Parsed token payload containing `access_token`, `id_token`, `refresh_token`, token type, and expiry.
732
+ */
733
+ const refreshOAuthToken = ResultAsync.fromThrowable(async (a) => {
734
+ const basicAuth = Buffer.from(`${a.clientId}:${a.clientSecret}`).toString("base64");
735
+ const params = new URLSearchParams();
736
+ params.append("grant_type", "refresh_token");
737
+ params.append("refresh_token", a.refreshToken);
738
+ console.log("refreshOAuthToken: params", params.toString());
739
+ const tokenRes = await fetch(`https://${a.cognitoDomain}/oauth2/token`, {
740
+ method: "POST",
741
+ headers: {
742
+ "Content-Type": "application/x-www-form-urlencoded",
743
+ Authorization: `Basic ${basicAuth}`
744
+ },
745
+ body: params.toString()
746
+ });
747
+ if (!tokenRes.ok) {
748
+ console.error("refreshOAuthToken: token exchange failed", await tokenRes.text());
749
+ throw Object.assign(/* @__PURE__ */ new Error("OAuth token refresh failed"), { name: "NotAuthorizedException" });
750
+ }
751
+ return await tokenRes.json();
752
+ }, (e) => {
753
+ console.error("refreshOAuthToken:error", e);
754
+ return error_cognito(e);
755
+ });
756
+ //#endregion
757
+ //#region src/s3/client.ts
758
+ /**
759
+ * Convenience function for S3Client when process.env.REGION is set
760
+ */
761
+ function getS3() {
762
+ return new S3Client({});
763
+ }
764
+ //#endregion
765
+ //#region src/utils/errors.ts
766
+ function isRecord(value) {
767
+ return typeof value === "object" && value !== null;
768
+ }
769
+ function getErrorName(error) {
770
+ if (!isRecord(error)) return void 0;
771
+ const name = error.name;
772
+ if (typeof name === "string") return name;
773
+ const code = error.code ?? error.Code;
774
+ if (typeof code === "string") return code;
775
+ }
776
+ //#endregion
777
+ //#region src/s3/errors.ts
778
+ const defaultErrors$2 = {
779
+ invalidInput: {
780
+ type: "badRequest",
781
+ message: "Invalid S3 request"
782
+ },
783
+ objectNotFound: {
784
+ type: "notFound",
785
+ message: "S3 object not found"
786
+ },
787
+ bucketNotFound: {
788
+ type: "internal",
789
+ message: "Internal server error"
790
+ },
791
+ conflict: {
792
+ type: "conflict",
793
+ message: "The request conflicts with the current S3 resource state"
794
+ },
795
+ throttled: {
796
+ type: "internal",
797
+ message: "Internal server error"
798
+ },
799
+ accessDenied: {
800
+ type: "internal",
801
+ message: "Internal server error"
802
+ },
803
+ internal: {
804
+ type: "internal",
805
+ message: "Internal server error"
806
+ }
807
+ };
808
+ /** Wrap an unknown caught value as an S3-domain error for neverthrow flows. */
809
+ function error_s3(error) {
810
+ return {
811
+ type: "s3",
812
+ error
813
+ };
814
+ }
815
+ /** Convert AWS SDK S3 errors into a safe lambda error for API responses. */
816
+ function error_lambda_fromS3(e, options = {}) {
817
+ const reason = getS3Reason(e.error);
818
+ const base = defaultErrors$2[reason];
819
+ const override = options[reason];
820
+ const args = {
821
+ ...base,
822
+ ...override
823
+ };
824
+ switch (args.type) {
825
+ case "badRequest": return error_lambda_badRequest(args.message, args.fieldName, args.fieldValue);
826
+ case "unauthorized": return error_lambda_unauthorized(args.message);
827
+ case "forbidden": return error_lambda_forbidden(args.message);
828
+ case "notFound": return error_lambda_notFound(args.message, args.fieldName, args.fieldValue);
829
+ case "conflict": return error_lambda_conflict(args.message, args.fieldName, args.fieldValue);
830
+ default: return error_lambda_internal(args.message);
831
+ }
832
+ }
833
+ /** Returns true for normal S3 object-missing responses. */
834
+ function is_s3_notFound(error) {
835
+ return getS3Reason(error) === "objectNotFound";
836
+ }
837
+ /** Classify AWS SDK / S3 service errors. */
838
+ function getS3Reason(error) {
839
+ switch (getErrorName(error)) {
840
+ case "NoSuchKey":
841
+ case "NotFound":
842
+ case "NoSuchVersion": return "objectNotFound";
843
+ case "NoSuchBucket":
844
+ case "NoSuchBucketPolicy":
845
+ case "NoSuchLifecycleConfiguration":
846
+ case "NoLoggingStatusForKey": return "bucketNotFound";
847
+ case "AmbiguousGrantByEmailAddress":
848
+ case "BadDigest":
849
+ case "EntityTooLarge":
850
+ case "EntityTooSmall":
851
+ case "IncompleteBody":
852
+ case "IncorrectNumberOfFilesInPostRequest":
853
+ case "InlineDataTooLarge":
854
+ case "InvalidAddressingHeader":
855
+ case "InvalidArgument":
856
+ case "InvalidBucketName":
857
+ case "InvalidDigest":
858
+ case "InvalidLocationConstraint":
859
+ case "InvalidPart":
860
+ case "InvalidPartOrder":
861
+ case "InvalidPayer":
862
+ case "InvalidPolicyDocument":
863
+ case "InvalidRange":
864
+ case "InvalidRequest":
865
+ case "InvalidSOAPRequest":
866
+ case "InvalidStorageClass":
867
+ case "InvalidTargetBucketForLogging":
868
+ case "InvalidURI":
869
+ case "KeyTooLongError":
870
+ case "MalformedACLError":
871
+ case "MalformedPOSTRequest":
872
+ case "MalformedXML":
873
+ case "MaxMessageLengthExceeded":
874
+ case "MaxPostPreDataLengthExceededError":
875
+ case "MetadataTooLarge":
876
+ case "MissingAttachment":
877
+ case "MissingContentLength":
878
+ case "MissingRequestBodyError":
879
+ case "RequestIsNotMultiPartContent":
880
+ case "RequestTorrentOfBucketError":
881
+ case "NoSuchUpload": return "invalidInput";
882
+ case "BucketAlreadyExists":
883
+ case "BucketAlreadyOwnedByYou":
884
+ case "BucketNotEmpty":
885
+ case "EncryptionTypeMismatch":
886
+ case "IdempotencyParameterMismatch":
887
+ case "IllegalVersioningConfigurationException":
888
+ case "InvalidBucketState":
889
+ case "InvalidEncryptionAlgorithmError":
890
+ case "InvalidObjectState":
891
+ case "InvalidWriteOffset":
892
+ case "ObjectAlreadyInActiveTierError":
893
+ case "ObjectNotInActiveTierError":
894
+ case "OperationAborted":
895
+ case "PreconditionFailed":
896
+ case "RestoreAlreadyInProgress":
897
+ case "TooManyParts": return "conflict";
898
+ case "RequestLimitExceeded":
899
+ case "RequestTimeout":
900
+ case "ServiceUnavailable":
901
+ case "SlowDown":
902
+ case "Throttling":
903
+ case "ThrottlingException": return "throttled";
904
+ case "AccessDenied":
905
+ case "AccountProblem":
906
+ case "AllAccessDisabled":
907
+ case "CredentialsNotSupported":
908
+ case "CrossLocationLoggingProhibited":
909
+ case "ExpiredToken":
910
+ case "InvalidAccessKeyId":
911
+ case "InvalidSecurity":
912
+ case "InvalidToken":
913
+ case "MethodNotAllowed":
914
+ case "MissingSecurityElement":
915
+ case "MissingSecurityHeader":
916
+ case "NotSignedUp":
917
+ case "SignatureDoesNotMatch": return "accessDenied";
918
+ case "AuthorizationHeaderMalformed":
919
+ case "InternalError":
920
+ case "NotImplemented":
921
+ case "PermanentRedirect":
922
+ case "Redirect":
923
+ case "RequestTimeTooSkewed":
924
+ case "TemporaryRedirect": return "internal";
925
+ default: return getHttpStatusReason$1(error);
926
+ }
927
+ }
928
+ function getHttpStatusReason$1(error) {
929
+ const status = getHttpStatusCode$1(error);
930
+ if (status === 400) return "invalidInput";
931
+ if (status === 404) return "objectNotFound";
932
+ if (status === 409 || status === 412) return "conflict";
933
+ if (status === 429 || status === 503) return "throttled";
934
+ if (status === 401 || status === 403) return "accessDenied";
935
+ return "internal";
936
+ }
937
+ function getHttpStatusCode$1(error) {
938
+ if (!isRecord(error)) return void 0;
939
+ const metadata = error.$metadata;
940
+ if (!isRecord(metadata)) return void 0;
941
+ return typeof metadata.httpStatusCode === "number" ? metadata.httpStatusCode : void 0;
942
+ }
943
+ //#endregion
944
+ //#region src/s3/signedUrl.ts
945
+ const getSignedUrl = ResultAsync.fromThrowable(getSignedUrl$1, (e) => {
946
+ console.error("getSignedUrl: Failed to get signed url", e);
947
+ error_s3(e);
948
+ });
949
+ //#endregion
950
+ //#region src/s3/object.ts
951
+ /**
952
+ * Retrieves an object from an S3 bucket.
953
+ *
954
+ * @param {string} bucketName - The name of the S3 bucket.
955
+ * @param {string} key - The key of the object to retrieve.
956
+ * @returns {Promise<Buffer>} A promise that resolves to the object data as a Buffer.
957
+ */
958
+ const getObject = ResultAsync.fromThrowable(async (bucketName, key) => {
959
+ const s3 = getS3();
960
+ const cmd = new GetObjectCommand({
961
+ Bucket: bucketName,
962
+ Key: key
963
+ });
964
+ const stream = (await s3.send(cmd)).Body;
965
+ return new Promise((resolve, reject) => {
966
+ const chunks = [];
967
+ stream.on("data", (chunk) => chunks.push(chunk));
968
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
969
+ stream.on("error", reject);
970
+ });
971
+ }, (e) => {
972
+ console.error(`getObjectt: Error getting object from S3: ${e}`);
973
+ return error_s3(e);
974
+ });
975
+ /**
976
+ * Convenience function to get an object from S3 and return it as a string.
977
+ */
978
+ function getObjectString(bucketName, key) {
979
+ return getObject(bucketName, key).map((buffer) => buffer.toString("utf-8"));
980
+ }
981
+ /**
982
+ * Checks if an object exists in an s3 bucket by retrieving the HEAD data
983
+ *
984
+ * @param {string} bucketName - The name of the S3 bucket.
985
+ * @param {string} key - The key of the object to retrieve.
986
+ * @returns {Promise<Buffer>} A promise that resolves to a boolean.
987
+ */
988
+ const objectExists = ResultAsync.fromThrowable(async (bucketName, key) => {
989
+ const s3 = getS3();
990
+ try {
991
+ const cmd = new HeadObjectCommand({
992
+ Bucket: bucketName,
993
+ Key: key
994
+ });
995
+ return (await s3.send(cmd)).$metadata.httpStatusCode === 200;
996
+ } catch (e) {
997
+ if (is_s3_notFound(e)) return false;
998
+ throw e;
999
+ }
1000
+ }, (e) => {
1001
+ console.error(`objectExists: Error getting object head from S3: ${e}`);
1002
+ return error_s3(e);
1003
+ });
1004
+ //#endregion
1005
+ //#region src/dynamo/errors.ts
1006
+ const defaultErrors$1 = {
1007
+ invalidInput: {
1008
+ type: "badRequest",
1009
+ message: "Invalid request"
1010
+ },
1011
+ conditionalCheckFailed: {
1012
+ type: "conflict",
1013
+ message: "The request conflicts with the current resource state"
1014
+ },
1015
+ transactionConflict: {
1016
+ type: "conflict",
1017
+ message: "The request conflicts with the current resource state"
1018
+ },
1019
+ resourceNotFound: {
1020
+ type: "internal",
1021
+ message: "Internal server error"
1022
+ },
1023
+ throttled: {
1024
+ type: "internal",
1025
+ message: "Internal server error"
1026
+ },
1027
+ accessDenied: {
1028
+ type: "internal",
1029
+ message: "Internal server error"
1030
+ },
1031
+ internal: {
1032
+ type: "internal",
1033
+ message: "Internal server error"
1034
+ }
1035
+ };
1036
+ /** Wrap an unknown caught value as a Dynamo-domain error for neverthrow flows. */
1037
+ function error_dynamo(error) {
1038
+ return {
1039
+ type: "dynamo",
1040
+ error
1041
+ };
1042
+ }
1043
+ /** Convert DynamoDB Toolbox or AWS SDK errors into a safe lambda error for API responses. */
1044
+ function error_lambda_fromDynamo(e, options = {}) {
1045
+ if (e.error instanceof DynamoDBToolboxError) return fromReason$1(getToolboxReason(e.error), options, toolboxField(e.error, options.includeToolboxPath));
1046
+ return fromReason$1(getAwsReason(e.error), options);
1047
+ }
1048
+ /** Apply endpoint overrides and build the concrete lambda error object. */
1049
+ function fromReason$1(reason, options, defaults = {}) {
1050
+ const base = {
1051
+ ...defaultErrors$1[reason],
1052
+ ...defaults
1053
+ };
1054
+ const override = options[reason];
1055
+ const args = {
1056
+ ...base,
1057
+ ...override
1058
+ };
1059
+ switch (args.type) {
1060
+ case "badRequest": return error_lambda_badRequest(args.message, args.fieldName, args.fieldValue);
1061
+ case "unauthorized": return error_lambda_unauthorized(args.message);
1062
+ case "forbidden": return error_lambda_forbidden(args.message);
1063
+ case "notFound": return error_lambda_notFound(args.message, args.fieldName, args.fieldValue);
1064
+ case "conflict": return error_lambda_conflict(args.message, args.fieldName, args.fieldValue);
1065
+ default: return error_lambda_internal(args.message);
1066
+ }
1067
+ }
1068
+ /** Classify errors produced by Toolbox before sending, or while formatting returned items. */
1069
+ function getToolboxReason(error) {
1070
+ if (error.code.startsWith("parsing.") || error.code === "actions.parsePrimaryKey.invalidKeyPart") return "invalidInput";
1071
+ if (error.code.startsWith("formatter.") || error.code.startsWith("schema.") || error.code.startsWith("entity.")) return "internal";
1072
+ switch (error.code) {
1073
+ case "actions.invalidCondition":
1074
+ case "actions.invalidExpressionAttributePath":
1075
+ case "queryCommand.invalidIndex":
1076
+ case "queryCommand.invalidPartition":
1077
+ case "queryCommand.invalidProjectionExpression":
1078
+ case "queryCommand.invalidRange":
1079
+ case "queryCommand.invalidReverseOption":
1080
+ case "scanCommand.invalidProjectionExpression":
1081
+ case "scanCommand.invalidSegmentOption":
1082
+ case "batchGetCommand.invalidProjectionExpression":
1083
+ case "options.invalidCapacityOption":
1084
+ case "options.invalidClientRequestToken":
1085
+ case "options.invalidConsistentOption":
1086
+ case "options.invalidIndexOption":
1087
+ case "options.invalidLimitOption":
1088
+ case "options.invalidMaxPagesOption":
1089
+ case "options.invalidMetricsOption":
1090
+ case "options.invalidReturnValuesOption":
1091
+ case "options.invalidReturnValuesOnConditionFalseOption":
1092
+ case "options.invalidSelectOption": return "invalidInput";
1093
+ case "actions.incompleteAction":
1094
+ case "actions.invalidAction":
1095
+ case "actions.missingDocumentClient":
1096
+ case "queryCommand.invalidTagEntitiesOption":
1097
+ case "queryCommand.noEntityMatched":
1098
+ case "scanCommand.noEntityMatched":
1099
+ case "options.invalidEntityAttrFilterOption":
1100
+ case "options.invalidNoEntityMatchBehaviorOption":
1101
+ case "options.invalidShowEntityAttrOption":
1102
+ case "options.invalidTableNameOption":
1103
+ case "options.unknownOption":
1104
+ case "table.missingTableName": return "internal";
1105
+ default: return "internal";
1106
+ }
1107
+ }
1108
+ function toolboxField(error, includeToolboxPath) {
1109
+ if (!includeToolboxPath || typeof error.path !== "string") return {};
1110
+ return {
1111
+ fieldName: error.path,
1112
+ fieldValue: error.message
1113
+ };
1114
+ }
1115
+ /** Classify AWS SDK / DynamoDB service errors that bubble out of Toolbox send calls. */
1116
+ function getAwsReason(error) {
1117
+ switch (getErrorName(error)) {
1118
+ case "ConditionalCheckFailedException": return "conditionalCheckFailed";
1119
+ case "TransactionCanceledException": return getTransactionReason(error);
1120
+ case "TransactionConflictException":
1121
+ case "ReplicatedWriteConflictException":
1122
+ case "IdempotentParameterMismatchException":
1123
+ case "ItemCollectionSizeLimitExceededException": return "transactionConflict";
1124
+ case "ValidationException": return "invalidInput";
1125
+ case "ResourceNotFoundException": return "resourceNotFound";
1126
+ case "ProvisionedThroughputExceededException":
1127
+ case "RequestLimitExceeded":
1128
+ case "ThrottlingException":
1129
+ case "ThrottlingError": return "throttled";
1130
+ case "AccessDeniedException":
1131
+ case "ExpiredTokenException":
1132
+ case "IncompleteSignatureException":
1133
+ case "InvalidSignatureException":
1134
+ case "UnrecognizedClientException": return "accessDenied";
1135
+ default: return "internal";
1136
+ }
1137
+ }
1138
+ /** Pick the most useful public reason from DynamoDB transaction cancellation details. */
1139
+ function getTransactionReason(error) {
1140
+ const cancellationReasons = getCancellationReasons(error);
1141
+ if (cancellationReasons.some((reason) => reason === "ConditionalCheckFailed")) return "conditionalCheckFailed";
1142
+ if (cancellationReasons.some((reason) => reason === "TransactionConflict")) return "transactionConflict";
1143
+ if (cancellationReasons.some((reason) => reason === "ValidationError")) return "invalidInput";
1144
+ if (cancellationReasons.some((reason) => [
1145
+ "ProvisionedThroughputExceeded",
1146
+ "RequestLimitExceeded",
1147
+ "ThrottlingError"
1148
+ ].includes(reason))) return "throttled";
1149
+ return "internal";
1150
+ }
1151
+ function getCancellationReasons(error) {
1152
+ if (!isRecord(error)) return [];
1153
+ const cancellationReasons = error.CancellationReasons ?? error.cancellationReasons;
1154
+ if (!Array.isArray(cancellationReasons)) return [];
1155
+ return cancellationReasons.map((reason) => isRecord(reason) && typeof reason.Code === "string" ? reason.Code : void 0).filter((reason) => reason !== void 0);
1156
+ }
1157
+ //#endregion
1158
+ //#region src/ses/errors.ts
1159
+ const defaultErrors = {
1160
+ invalidInput: {
1161
+ type: "badRequest",
1162
+ message: "Invalid SES request"
1163
+ },
1164
+ messageRejected: {
1165
+ type: "badRequest",
1166
+ message: "Email message was rejected"
1167
+ },
1168
+ identityNotVerified: {
1169
+ type: "internal",
1170
+ message: "Internal server error"
1171
+ },
1172
+ notFound: {
1173
+ type: "internal",
1174
+ message: "Internal server error"
1175
+ },
1176
+ alreadyExists: {
1177
+ type: "conflict",
1178
+ message: "SES resource already exists"
1179
+ },
1180
+ conflict: {
1181
+ type: "conflict",
1182
+ message: "The request conflicts with the current SES resource state"
1183
+ },
1184
+ throttled: {
1185
+ type: "internal",
1186
+ message: "Internal server error"
1187
+ },
1188
+ accessDenied: {
1189
+ type: "internal",
1190
+ message: "Internal server error"
1191
+ },
1192
+ internal: {
1193
+ type: "internal",
1194
+ message: "Internal server error"
1195
+ }
1196
+ };
1197
+ /** Wrap an unknown caught value as an SES-domain error for neverthrow flows. */
1198
+ function error_ses(error) {
1199
+ return {
1200
+ type: "ses",
1201
+ error
1202
+ };
1203
+ }
1204
+ /** Convert AWS SDK SES errors into a safe lambda error for API responses. */
1205
+ function error_lambda_fromSes(e, options = {}) {
1206
+ return fromReason(getSesReason(e.error), options);
1207
+ }
1208
+ /** Apply endpoint overrides and build the concrete lambda error object. */
1209
+ function fromReason(reason, options) {
1210
+ const base = defaultErrors[reason];
1211
+ const override = options[reason];
1212
+ const args = {
1213
+ ...base,
1214
+ ...override
1215
+ };
1216
+ switch (args.type) {
1217
+ case "badRequest": return error_lambda_badRequest(args.message, args.fieldName, args.fieldValue);
1218
+ case "unauthorized": return error_lambda_unauthorized(args.message);
1219
+ case "forbidden": return error_lambda_forbidden(args.message);
1220
+ case "notFound": return error_lambda_notFound(args.message, args.fieldName, args.fieldValue);
1221
+ case "conflict": return error_lambda_conflict(args.message, args.fieldName, args.fieldValue);
1222
+ default: return error_lambda_internal(args.message);
1223
+ }
1224
+ }
1225
+ /** Classify AWS SDK / SES service errors. */
1226
+ function getSesReason(error) {
1227
+ switch (getErrorName$1(error)) {
1228
+ case "BadRequestException":
1229
+ case "CustomVerificationEmailInvalidContentException":
1230
+ case "InvalidParameterValue":
1231
+ case "InvalidPolicyException":
1232
+ case "InvalidRenderingParameterException":
1233
+ case "InvalidSnsTopic":
1234
+ case "InvalidSnsTopicException":
1235
+ case "InvalidTemplateException":
1236
+ case "InvalidTrackingOptionsException":
1237
+ case "MissingRenderingAttributeException": return "invalidInput";
1238
+ case "MessageRejected":
1239
+ case "MessageRejectedException": return "messageRejected";
1240
+ case "ConfigurationSetDoesNotExist":
1241
+ case "ConfigurationSetDoesNotExistException":
1242
+ case "NotFoundException":
1243
+ case "RuleSetDoesNotExist":
1244
+ case "TemplateDoesNotExist":
1245
+ case "TemplateDoesNotExistException": return "notFound";
1246
+ case "AlreadyExistsException":
1247
+ case "ConfigurationSetAlreadyExists":
1248
+ case "RuleSetNameAlreadyExists":
1249
+ case "TemplateNameAlreadyExists": return "alreadyExists";
1250
+ case "AccountSendingPausedException":
1251
+ case "ConfigurationSetSendingPausedException":
1252
+ case "ConcurrentModificationException":
1253
+ case "ConflictException":
1254
+ case "MailFromDomainNotVerified":
1255
+ case "MailFromDomainNotVerifiedException":
1256
+ case "SendingPausedException": return "conflict";
1257
+ case "FromEmailAddressNotVerified":
1258
+ case "IdentityNotVerifiedException": return "identityNotVerified";
1259
+ case "LimitExceededException":
1260
+ case "Throttling":
1261
+ case "ThrottlingException":
1262
+ case "TooManyRequestsException": return "throttled";
1263
+ case "AccessDeniedException":
1264
+ case "AccountSuspendedException":
1265
+ case "ExpiredTokenException":
1266
+ case "InvalidClientTokenId":
1267
+ case "InvalidSignatureException":
1268
+ case "ProductionAccessNotGrantedException":
1269
+ case "SignatureDoesNotMatch":
1270
+ case "UnauthorizedException": return "accessDenied";
1271
+ case "InternalFailure":
1272
+ case "InternalServerError":
1273
+ case "InternalServiceError":
1274
+ case "ServiceUnavailable": return "internal";
1275
+ default: return getHttpStatusReason(error);
1276
+ }
1277
+ }
1278
+ function getHttpStatusReason(error) {
1279
+ const status = getHttpStatusCode(error);
1280
+ if (status === 400) return "invalidInput";
1281
+ if (status === 401) return "accessDenied";
1282
+ if (status === 403) return "accessDenied";
1283
+ if (status === 404) return "notFound";
1284
+ if (status === 409 || status === 412) return "conflict";
1285
+ if (status === 429) return "throttled";
1286
+ return "internal";
1287
+ }
1288
+ function getErrorName$1(error) {
1289
+ if (!isRecord$1(error)) return void 0;
1290
+ const name = error.name;
1291
+ if (typeof name === "string") return name;
1292
+ const code = error.code ?? error.Code;
1293
+ if (typeof code === "string") return code;
1294
+ }
1295
+ function getHttpStatusCode(error) {
1296
+ if (!isRecord$1(error)) return void 0;
1297
+ const metadata = error.$metadata;
1298
+ if (!isRecord$1(metadata)) return void 0;
1299
+ return typeof metadata.httpStatusCode === "number" ? metadata.httpStatusCode : void 0;
1300
+ }
1301
+ function isRecord$1(value) {
1302
+ return typeof value === "object" && value !== null;
1303
+ }
1304
+ //#endregion
1305
+ //#region src/rehype/flat-toc.ts
1306
+ /**
1307
+ * This rehype plugin extracts the headings from the markdown elements but also the raw elements.
1308
+ * So we get html headings in the TOC as well
1309
+ *
1310
+ * It sets the file.data.fm.toc to a flat map of the toc
1311
+ */
1312
+ const extractToc = () => {
1313
+ return (tree, file) => {
1314
+ const details = tree.children.flatMap(extractDetails);
1315
+ if (file.data.fm === void 0) file.data.fm = {};
1316
+ file.data.fm.toc = details;
1317
+ };
1318
+ };
1319
+ function extractDetails(content) {
1320
+ if (content.type === "element" && content.tagName.startsWith("h") && "id" in content.properties) {
1321
+ const value = content.children.length === 1 && content.children[0].type === "text" ? content.children[0].value : content.properties.id;
1322
+ return [{
1323
+ level: parseInt(content.tagName.slice(1)),
1324
+ id: content.properties.id,
1325
+ value
1326
+ }];
1327
+ } else if (content.type === "raw") return parseRaw(content.value).flatMap(extractDetails);
1328
+ return [];
1329
+ }
1330
+ /**
1331
+ * Parses raw HTML and returns a flat array of all heading (h1-h6) elements as HAST nodes.
1332
+ */
1333
+ function parseRaw(raw) {
1334
+ const tree = unified().use(rehypeParse, { fragment: true }).parse(raw);
1335
+ function collectHeadings(node) {
1336
+ if (node.type === "element" && /^h[1-6]$/.test(node.tagName)) return [node];
1337
+ if ("children" in node && Array.isArray(node.children)) return node.children.flatMap(collectHeadings);
1338
+ return [];
1339
+ }
1340
+ return tree.children.flatMap(collectHeadings);
1341
+ }
1342
+ //#endregion
1343
+ //#region src/utils/valibot.ts
1344
+ function isSchema(x) {
1345
+ return !!x && typeof x === "object" && "kind" in x && x["kind"] === "schema";
1346
+ }
1347
+ function unwrap(schema) {
1348
+ let curr = schema;
1349
+ const seen = /* @__PURE__ */ new Set();
1350
+ while (curr && typeof curr === "object" && !seen.has(curr) && "wrapped" in curr && isSchema(curr.wrapped)) {
1351
+ seen.add(curr);
1352
+ curr = curr.wrapped;
1353
+ }
1354
+ return curr;
1355
+ }
1356
+ function isIntegerKey(s) {
1357
+ return /^-?\d+$/.test(s);
1358
+ }
1359
+ function getSchemaByPath(root, path, opts = {}) {
1360
+ if (!isSchema(root)) return void 0;
1361
+ if (!path) return root;
1362
+ const keys = path.split(".");
1363
+ let curr = root;
1364
+ for (let i = 0; i < keys.length; i++) {
1365
+ if (!curr) return void 0;
1366
+ curr = unwrap(curr);
1367
+ const seg = keys[i];
1368
+ switch (curr.type) {
1369
+ case "object": {
1370
+ const entries = curr.entries;
1371
+ if (!entries) return void 0;
1372
+ curr = entries[seg];
1373
+ break;
1374
+ }
1375
+ case "record":
1376
+ curr = curr.value;
1377
+ break;
1378
+ case "array":
1379
+ if (!isIntegerKey(seg)) return void 0;
1380
+ curr = curr.item;
1381
+ break;
1382
+ case "tuple": {
1383
+ if (!isIntegerKey(seg)) return void 0;
1384
+ const idx = Number(seg);
1385
+ const items = curr.items;
1386
+ const rest = curr.rest;
1387
+ if (!items) return void 0;
1388
+ curr = idx < items.length ? items[idx] : rest;
1389
+ break;
1390
+ }
1391
+ case "union": {
1392
+ const options = curr.options;
1393
+ if (!options?.length) return void 0;
1394
+ const numeric = isIntegerKey(seg);
1395
+ let next;
1396
+ if (numeric) next = options.find((o) => {
1397
+ const u = unwrap(o);
1398
+ return u?.type === "array" || u?.type === "tuple";
1399
+ }) ?? options[opts.preferOption ?? 0];
1400
+ else next = options.find((o) => {
1401
+ const u = unwrap(o);
1402
+ if (u?.type === "object") {
1403
+ const ent = u.entries;
1404
+ return !!ent && seg in ent;
1405
+ }
1406
+ return u?.type === "record";
1407
+ }) ?? options[opts.preferOption ?? 0];
1408
+ curr = next;
1409
+ i--;
1410
+ break;
1411
+ }
1412
+ case "variant": {
1413
+ const options = curr.options;
1414
+ if (!options?.length) return void 0;
1415
+ const numeric = isIntegerKey(seg);
1416
+ let next;
1417
+ if (numeric) next = options.find((o) => {
1418
+ const u = unwrap(o);
1419
+ return u?.type === "array" || u?.type === "tuple";
1420
+ }) ?? options[opts.preferOption ?? 0];
1421
+ else next = options.find((o) => {
1422
+ const u = unwrap(o);
1423
+ if (u?.type === "object") {
1424
+ const ent = u.entries;
1425
+ return !!ent && seg in ent;
1426
+ }
1427
+ return u?.type === "record";
1428
+ }) ?? options[opts.preferOption ?? 0];
1429
+ curr = next;
1430
+ i--;
1431
+ break;
1432
+ }
1433
+ default: return;
1434
+ }
1435
+ }
1436
+ return curr ? unwrap(curr) : void 0;
1437
+ }
1438
+ //#endregion
1439
+ //#region src/utils/object.ts
1440
+ /**
1441
+ * Sets the value for an object by its dot path
1442
+ * @param obj - any object
1443
+ * @param path - the dot path eg. key1.0.1.key2
1444
+ * @param value - any value
1445
+ * @returns - the modified object
1446
+ */
1447
+ function setByPath(obj, path, value) {
1448
+ const keys = path.split(".");
1449
+ let curr = obj;
1450
+ for (let i = 0; i < keys.length - 1; i++) {
1451
+ const k = keys[i];
1452
+ const next = keys[i + 1];
1453
+ if (Number.isInteger(Number(next))) curr[k] ??= [];
1454
+ else curr[k] ??= {};
1455
+ curr = curr[k];
1456
+ }
1457
+ curr[keys[keys.length - 1]] = value;
1458
+ return obj;
1459
+ }
1460
+ /**
1461
+ * Gets the value from an object by its dot path
1462
+ * @param obj - any object
1463
+ * @param path - the dot path eg. key1.0.1.key2
1464
+ * @returns - the value at the given path or undefined
1465
+ */
1466
+ function getByPath(obj, path) {
1467
+ if (!obj || typeof obj !== "object") return void 0;
1468
+ const keys = path.split(".");
1469
+ let curr = obj;
1470
+ for (const k of keys) {
1471
+ if (curr == null) return void 0;
1472
+ curr = curr[k];
1473
+ }
1474
+ return curr;
1475
+ }
1476
+ const isPlainRecord = (v) => isObject(v) && !Array.isArray(v);
1477
+ /**
1478
+ * Returns a deep "patch" object containing only the fields from `b`
1479
+ * that are different from `a`.
1480
+ *
1481
+ * Behavior:
1482
+ * - Only keys from `b` can appear in the result.
1483
+ * - New keys in `b` are included.
1484
+ * - Changed primitive/array values are included as the value from `b`.
1485
+ * - For nested plain objects, it recurses and returns only the differing nested fields.
1486
+ * - Arrays are treated as atomic (if different, the whole array from `b` is returned).
1487
+ *
1488
+ * Typing:
1489
+ * - Output is `DeepPartial<B>` because only a subset of `b`'s shape is returned.
1490
+ *
1491
+ * @template A
1492
+ * @template B
1493
+ * @param {A} a - Base/original object (can be a different shape than `b`).
1494
+ * @param {B} b - Updated object; output keys come from this object.
1495
+ * @returns {DeepPartial<B>} Deep partial of `b` containing only differences vs `a`.
1496
+ */
1497
+ const deepDiff = (a, b) => {
1498
+ const out = {};
1499
+ for (const key of Object.keys(b)) {
1500
+ const aVal = a?.[key];
1501
+ const bVal = b[key];
1502
+ if (!(key in a)) {
1503
+ out[key] = bVal;
1504
+ continue;
1505
+ }
1506
+ if (isPlainRecord(aVal) && isPlainRecord(bVal)) {
1507
+ const nested = deepDiff(aVal, bVal);
1508
+ if (Object.keys(nested).length) out[key] = nested;
1509
+ continue;
1510
+ }
1511
+ if (!isEqual(aVal, bVal)) out[key] = bVal;
1512
+ }
1513
+ return out;
1514
+ };
1515
+ /**
1516
+ * Deeply prunes `source` to match the *shape* of `shape`.
1517
+ *
1518
+ * Rules:
1519
+ * - Only keys that exist on `shape` are kept.
1520
+ * - Pruning is deep for nested plain objects.
1521
+ * - Arrays are supported by using the first element of `shape` as the element-shape.
1522
+ * - If `shape` is `[]`, returns `[]` (drops all elements).
1523
+ * - Primitive values are kept as-is (no type coercion) if the key exists in `shape`.
1524
+ * - If `shape` expects an object/array but `source` is not compatible, returns an empty object/array of that shape.
1525
+ *
1526
+ * @typeParam S - Source object type.
1527
+ * @typeParam Sh - Shape object type.
1528
+ * @param source - The object to prune.
1529
+ * @param shape - The object whose keys/structure are the allowlist.
1530
+ * @returns A new value derived from `source`, containing only fields present in `shape`, pruned deeply.
1531
+ *
1532
+ * @example
1533
+ * const source = { a: 1, b: { c: 2, d: 3 }, e: [ { x: 1, y: 2 }, { x: 3, y: 4 } ], z: 9 };
1534
+ * const shape = { a: 0, b: { c: 0 }, e: [ { x: 0 } ] };
1535
+ * // => { a: 1, b: { c: 2 }, e: [ { x: 1 }, { x: 3 } ] }
1536
+ * const out = pruneToShape(source, shape);
1537
+ */
1538
+ function pruneToShape(source, shape) {
1539
+ return pruneAny(source, shape);
1540
+ function pruneAny(src, sh) {
1541
+ if (Array.isArray(sh)) {
1542
+ if (!Array.isArray(src)) return [];
1543
+ if (sh.length === 0) return [];
1544
+ const elemShape = sh[0];
1545
+ return src.map((v) => pruneAny(v, elemShape));
1546
+ }
1547
+ if (isPlainObject(sh)) {
1548
+ const out = {};
1549
+ const srcObj = isPlainObject(src) ? src : void 0;
1550
+ for (const key of Object.keys(sh)) {
1551
+ const shVal = sh[key];
1552
+ const srcVal = srcObj ? srcObj[key] : void 0;
1553
+ if (Array.isArray(shVal) || isPlainObject(shVal)) out[key] = pruneAny(srcVal, shVal);
1554
+ else out[key] = srcVal;
1555
+ }
1556
+ return out;
1557
+ }
1558
+ return src;
1559
+ }
1560
+ function isPlainObject(v) {
1561
+ if (v === null || typeof v !== "object") return false;
1562
+ const proto = Object.getPrototypeOf(v);
1563
+ return proto === Object.prototype || proto === null;
1564
+ }
1565
+ }
1566
+ //#endregion
1567
+ //#region src/utils/fs.ts
1568
+ /**
1569
+ * @returns true if a filepath exists
1570
+ */
1571
+ async function exists(filePath) {
1572
+ try {
1573
+ await lstat(filePath);
1574
+ return true;
1575
+ } catch {
1576
+ return false;
1577
+ }
1578
+ }
1579
+ /**
1580
+ * Writes data to a filepath if it is different
1581
+ * @returns true if the file is written to
1582
+ */
1583
+ async function writeIfDifferent(filePath, newData) {
1584
+ const directory = dirname(filePath);
1585
+ if (!await exists(directory)) await mkdir(directory, { recursive: true });
1586
+ if (await exists(filePath)) {
1587
+ if (await readFile(filePath, "utf8") === newData) return false;
1588
+ }
1589
+ await writeFile(filePath, newData, "utf8");
1590
+ console.log(chalk.green("Writing to"), filePath);
1591
+ return true;
1592
+ }
1593
+ /**
1594
+ * @returns the json object packageJson or undefined if it doesnt exist
1595
+ */
1596
+ async function readPackageJson(filePath) {
1597
+ if (!await exists(filePath)) return void 0;
1598
+ return JSON.parse(await readFile(filePath, { encoding: "utf-8" }));
1599
+ }
1600
+ //#endregion
1601
+ export { HTTPMethods, changePassword, computeSecretHash, confirmForgotPassword, confirmSignup, createApiRequest, deepDiff, error_cognito, error_dynamo, error_lambda_badRequest, error_lambda_conflict, error_lambda_forbidden, error_lambda_fromCognito, error_lambda_fromDynamo, error_lambda_fromS3, error_lambda_fromSes, error_lambda_internal, error_lambda_notFound, error_lambda_unauthorized, error_s3, error_ses, exists, extractAttributes, extractToc, forgotPassword, getByPath, getCognitoClient, getCookies, getErrorName, getObject, getObjectString, getS3, getSchemaByPath, getSignedUrl, getUserDetails, getUserGroups, isRecord, isSchema, is_s3_notFound, login, logout, objectExists, parseRaw, pruneToShape, readPackageJson, refreshOAuthToken, refreshTokens, resetPassword, response_error, response_ok, response_valibotError, setByPath, signUp, unwrap, verifyOAuthToken, wrapHandler, writeIfDifferent };
1602
+
1603
+ //# sourceMappingURL=worker.mjs.map