wirejs-deploy-amplify-basic 0.1.175 → 0.1.177

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.
@@ -4,7 +4,8 @@ import { randomUUID } from 'crypto';
4
4
  import {
5
5
  defineBackend,
6
6
  } from '@aws-amplify/backend';
7
- import { Duration, RemovalPolicy, NestedStack } from "aws-cdk-lib";
7
+ import { Duration, Fn, RemovalPolicy, NestedStack } from "aws-cdk-lib";
8
+ import { UserPool } from 'aws-cdk-lib/aws-cognito';
8
9
  import {
9
10
  FunctionUrlAuthType,
10
11
  IFunction,
@@ -16,12 +17,11 @@ import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
16
17
  import { AnyPrincipal, PolicyStatement } from 'aws-cdk-lib/aws-iam';
17
18
  import { Rule, RuleTargetInput, Schedule } from 'aws-cdk-lib/aws-events';
18
19
  import { LambdaFunction as LambdaFunctionTarget } from 'aws-cdk-lib/aws-events-targets';
19
- import { SESClient, VerifyEmailIdentityCommand, GetIdentityVerificationAttributesCommand } from '@aws-sdk/client-ses';
20
+ import { SESClient, VerifyEmailIdentityCommand, GetIdentityVerificationAttributesCommand, VerifyDomainIdentityCommand } from '@aws-sdk/client-ses';
20
21
 
21
22
  import { TableDefinition, indexName, DeploymentConfig } from 'wirejs-resources';
22
23
  import { TableIndexes } from './constructs/table-indexes';
23
24
  import { RealtimeService } from './constructs/realtime-service';
24
- import { auth } from './auth/resource';
25
25
 
26
26
  // @ts-ignore
27
27
  import generatedResources from './generated-resources';
@@ -47,7 +47,7 @@ const SELF_INVOCATION_ID = randomUUID();
47
47
  /**
48
48
  * Amplify resources
49
49
  */
50
- const backend = defineBackend({ auth });
50
+ const backend = defineBackend({});
51
51
 
52
52
  copyFileSync(
53
53
  path.join(__dirname, 'api', 'handler.ts'),
@@ -62,6 +62,51 @@ if (fs.existsSync(CONFIG_PATH)) {
62
62
  console.log('\nNo deployment config found. Using defaults.\n')
63
63
  }
64
64
 
65
+ /**
66
+ * Cognito resources for AuthenticationService
67
+ */
68
+ function isAuthenticationService(resource: any): resource is {
69
+ type: 'AuthenticationService';
70
+ options: { absoluteId: string };
71
+ } {
72
+ return resource.type === 'AuthenticationService';
73
+ }
74
+
75
+ type CognitoResources = {
76
+ absoluteId: string;
77
+ userPool: UserPool;
78
+ clientId: string;
79
+ };
80
+
81
+ const cognitoResources: CognitoResources[] = [];
82
+ for (const resource of generated) {
83
+ if (isAuthenticationService(resource)) {
84
+ const sanitizedId = resource.options.absoluteId.replace(/[^a-zA-Z0-9-_]/g, '_');
85
+ const userPool = new UserPool(backend.stack, `UserPool_${sanitizedId}`, {
86
+ signInAliases: { email: true },
87
+ passwordPolicy: {
88
+ minLength: 8,
89
+ requireLowercase: false,
90
+ requireNumbers: false,
91
+ requireSymbols: false,
92
+ requireUppercase: false,
93
+ },
94
+ selfSignUpEnabled: true,
95
+ });
96
+ const userPoolClient = userPool.addClient('UserPoolClient', {
97
+ authFlows: {
98
+ userPassword: true,
99
+ adminUserPassword: true,
100
+ },
101
+ });
102
+ cognitoResources.push({
103
+ absoluteId: resource.options.absoluteId,
104
+ userPool,
105
+ clientId: userPoolClient.userPoolClientId,
106
+ });
107
+ }
108
+ }
109
+
65
110
  const api = new NodejsFunction(backend.stack, 'ApiHandler', {
66
111
  runtime: {
67
112
  20: Runtime.NODEJS_20_X,
@@ -81,25 +126,6 @@ const api = new NodejsFunction(backend.stack, 'ApiHandler', {
81
126
  timeout: Duration.seconds(cfg.runtimeTimeoutSeconds ?? 15 * 60),
82
127
  });
83
128
 
84
- /**
85
- * Amplify resource augmentations
86
- */
87
- backend.auth.resources.cfnResources.cfnUserPool.policies = {
88
- passwordPolicy: {
89
- minimumLength: 8,
90
- requireLowercase: false,
91
- requireNumbers: false,
92
- requireSymbols: false,
93
- requireUppercase: false,
94
- }
95
- }
96
- const userPoolClient = backend.auth.resources.cfnResources.cfnUserPoolClient;
97
- userPoolClient.explicitAuthFlows = userPoolClient.explicitAuthFlows || [];
98
- userPoolClient.explicitAuthFlows.push(
99
- 'ALLOW_USER_PASSWORD_AUTH',
100
- 'ALLOW_ADMIN_USER_PASSWORD_AUTH'
101
- );
102
-
103
129
  api.role?.addToPrincipalPolicy(new PolicyStatement({
104
130
  actions: ['lambda:InvokeFunction'],
105
131
  resources: [
@@ -282,9 +308,17 @@ for (const resource of generated) {
282
308
  api.addEnvironment(
283
309
  'BUCKET', bucket.bucketName,
284
310
  );
285
- api.addEnvironment(
286
- 'COGNITO_CLIENT_ID', backend.auth.resources.userPoolClient.userPoolClientId
287
- );
311
+ if (cognitoResources.length > 0) {
312
+ const clientIdMap: Record<string, string> = {};
313
+ for (const { absoluteId, userPool, clientId } of cognitoResources) {
314
+ clientIdMap[absoluteId] = clientId;
315
+ userPool.grant(api,
316
+ 'cognito-idp:AdminInitiateAuth',
317
+ 'cognito-idp:AdminRespondToAuthChallenge',
318
+ );
319
+ }
320
+ api.addEnvironment('COGNITO_CLIENT_IDS', Fn.toJsonString(clientIdMap));
321
+ }
288
322
  api.addEnvironment(
289
323
  'TABLE_NAME_PREFIX', TABLE_NAME_PREFIX
290
324
  );
@@ -315,14 +349,28 @@ api.addToRolePolicy(new PolicyStatement({
315
349
  */
316
350
  function isEmailSender(resource: any): resource is {
317
351
  type: 'EmailSender';
318
- options: { absoluteId: string; from: string };
352
+ options: {
353
+ absoluteId: string;
354
+ from?: string;
355
+ fromDomain?: string;
356
+ type: 'email' | 'domain';
357
+ };
319
358
  } {
320
359
  return resource.type === 'EmailSender';
321
360
  }
322
361
 
323
362
  const emailSenderResources = generated.filter(isEmailSender);
324
363
  if (emailSenderResources.length > 0) {
325
- const senderAddresses = emailSenderResources.map(r => r.options.from);
364
+ // Extract both email addresses and domains for verification
365
+ const emailAddresses = emailSenderResources
366
+ .filter(r => r.options.type === 'email' && r.options.from)
367
+ .map(r => r.options.from!);
368
+
369
+ const domains = emailSenderResources
370
+ .filter(r => r.options.type === 'domain' && r.options.fromDomain)
371
+ .map(r => r.options.fromDomain!);
372
+
373
+ // Add SES permissions for all identity types
326
374
  api.addToRolePolicy(new PolicyStatement({
327
375
  actions: [
328
376
  'ses:SendEmail',
@@ -333,13 +381,12 @@ if (emailSenderResources.length > 0) {
333
381
  ],
334
382
  }));
335
383
 
336
- // Initiate SES email verification for each sender address during deployment.
337
- // This sends a verification email to each address automatically so customers
338
- // don't need to manually trigger it from the AWS console.
384
+ // Initiate SES verification for emails and domains during deployment.
339
385
  const sesClient = new SESClient();
340
- const verificationResults: { address: string; status: string }[] = [];
386
+ const verificationResults: { identity: string; type: 'email' | 'domain'; status: string }[] = [];
341
387
 
342
- for (const address of senderAddresses) {
388
+ // Verify individual email addresses
389
+ for (const address of emailAddresses) {
343
390
  try {
344
391
  // Check current verification status first
345
392
  const statusResult = await sesClient.send(
@@ -348,35 +395,64 @@ if (emailSenderResources.length > 0) {
348
395
  const currentStatus = statusResult.VerificationAttributes?.[address]?.VerificationStatus;
349
396
 
350
397
  if (currentStatus === 'Success') {
351
- verificationResults.push({ address, status: 'already verified ✅' });
398
+ verificationResults.push({ identity: address, type: 'email', status: 'already verified ✅' });
352
399
  } else {
353
400
  // Trigger a (new) verification email
354
401
  await sesClient.send(new VerifyEmailIdentityCommand({ EmailAddress: address }));
355
- verificationResults.push({ address, status: 'verification email sent 📧' });
402
+ verificationResults.push({ identity: address, type: 'email', status: 'verification email sent 📧' });
403
+ }
404
+ } catch (err: any) {
405
+ verificationResults.push({ identity: address, type: 'email', status: `could not initiate automatically — ${err?.message ?? err}` });
406
+ }
407
+ }
408
+
409
+ // Verify domains
410
+ for (const domain of domains) {
411
+ try {
412
+ // Check current verification status first
413
+ const statusResult = await sesClient.send(
414
+ new GetIdentityVerificationAttributesCommand({ Identities: [domain] })
415
+ );
416
+ const currentStatus = statusResult.VerificationAttributes?.[domain]?.VerificationStatus;
417
+
418
+ if (currentStatus === 'Success') {
419
+ verificationResults.push({ identity: domain, type: 'domain', status: 'already verified ✅' });
420
+ } else {
421
+ // Trigger domain verification (requires DNS setup)
422
+ await sesClient.send(new VerifyDomainIdentityCommand({ Domain: domain }));
423
+ verificationResults.push({ identity: domain, type: 'domain', status: 'initiated - DNS setup required 🌐' });
356
424
  }
357
425
  } catch (err: any) {
358
- verificationResults.push({ address, status: `could not initiate automatically — ${err?.message ?? err}` });
426
+ verificationResults.push({ identity: domain, type: 'domain', status: `could not initiate automatically — ${err?.message ?? err}` });
359
427
  }
360
428
  }
361
429
 
430
+ // Enhanced console output
362
431
  console.log(`
363
- ⚠️ AWS SES Email Verification
364
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
365
- Your app uses EmailSender with the following sender address(es):
432
+ ⚠️ AWS SES Email and Domain Verification
433
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
434
+ Your app uses EmailSender with the following identities:
435
+
436
+ EMAIL ADDRESSES:
437
+ ${verificationResults.filter(r => r.type === 'email').map(r => ` 📧 ${r.identity} — ${r.status}`).join('\n') || ' (none)'}
366
438
 
367
- ${verificationResults.map(r => ` • ${r.address} — ${r.status}`).join('\n')}
439
+ DOMAINS:
440
+ ${verificationResults.filter(r => r.type === 'domain').map(r => ` 🌐 ${r.identity} — ${r.status}`).join('\n') || ' (none)'}
368
441
 
369
- ${verificationResults.some(r => r.status.includes('sent'))
370
- ? 'Check your inbox and click the verification link in each email from AWS SES.'
371
- : ''}
372
- AWS SES starts in "sandbox" mode you must also verify recipient addresses
373
- or request production access to send to arbitrary recipients.
442
+ NEXT STEPS:
443
+ ${verificationResults.some(r => r.type === 'email' && r.status.includes('sent'))
444
+ ? '📧 Check your inbox and click verification links for email addresses.\n' : ''}${verificationResults.some(r => r.type === 'domain' && r.status.includes('DNS'))
445
+ ? '🌐 Set up DNS records for domain verification (see AWS SES console).\n' : ''}
446
+ ⚠️ AWS SES starts in "sandbox" mode you must also verify recipient addresses
447
+ or request production access to send to arbitrary recipients.
374
448
 
375
- To request production (sandbox removal) access:
376
- https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html
449
+ USEFUL LINKS:
450
+ 📖 Verify identities: https://console.aws.amazon.com/ses/home?region=${backend.stack.region}#/verified-identities
451
+ 🚀 Request production: https://console.aws.amazon.com/ses/home?region=${backend.stack.region}#/account
452
+ 📚 Documentation: https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html
377
453
 
378
454
  See docs/amplify.md in your project for more details.
379
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
455
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
380
456
  `);
381
457
  }
382
458
 
@@ -3,6 +3,6 @@
3
3
  "dependencies": {
4
4
  "jsdom": "^25.0.1",
5
5
  "wirejs-dom": "^1.0.44",
6
- "wirejs-resources": "^0.1.175"
6
+ "wirejs-resources": "^0.1.177"
7
7
  }
8
8
  }
@@ -3,7 +3,6 @@ import * as jose from 'jose';
3
3
  import { CognitoIdentityProviderClient, SignUpCommand, ForgotPasswordCommand, ConfirmForgotPasswordCommand, ConfirmSignUpCommand, ResendConfirmationCodeCommand, InitiateAuthCommand, ChangePasswordCommand, } from '@aws-sdk/client-cognito-identity-provider';
4
4
  import { withContext, SignedCookie, Setting, AuthenticationService as AuthenticationServiceBase, } from 'wirejs-resources';
5
5
  import { addResource } from '../resource-collector.js';
6
- const ClientId = env['COGNITO_CLIENT_ID'];
7
6
  const actions = {
8
7
  changepassword: {
9
8
  name: "Change Password",
@@ -118,6 +117,7 @@ const client = new CognitoIdentityProviderClient();
118
117
  export class AuthenticationService extends AuthenticationServiceBase {
119
118
  #cookie;
120
119
  #keepalive;
120
+ #clientId;
121
121
  constructor(scope, id, options = {}) {
122
122
  super(scope, id, options);
123
123
  const signingSecret = new Setting(this, 'jwt-signing-secret', {
@@ -125,6 +125,8 @@ export class AuthenticationService extends AuthenticationServiceBase {
125
125
  });
126
126
  this.#keepalive = options.keepalive ?? false;
127
127
  this.#cookie = new SignedCookie(this, options.cookie ?? 'identity', signingSecret, { maxAge: ONE_WEEK });
128
+ const cognitoClientIds = JSON.parse(env['COGNITO_CLIENT_IDS'] || '{}');
129
+ this.#clientId = cognitoClientIds[this.absoluteId];
128
130
  addResource('AuthenticationService', { absoluteId: this.absoluteId });
129
131
  }
130
132
  async getMachineState(cookies) {
@@ -206,7 +208,7 @@ export class AuthenticationService extends AuthenticationServiceBase {
206
208
  }
207
209
  try {
208
210
  const command = new SignUpCommand({
209
- ClientId,
211
+ ClientId: this.#clientId,
210
212
  Username: form.inputs.email,
211
213
  Password: form.inputs.password,
212
214
  UserAttributes: [
@@ -249,7 +251,7 @@ export class AuthenticationService extends AuthenticationServiceBase {
249
251
  }
250
252
  try {
251
253
  const command = new ResendConfirmationCodeCommand({
252
- ClientId,
254
+ ClientId: this.#clientId,
253
255
  Username: state.metadata
254
256
  });
255
257
  await client.send(command);
@@ -283,7 +285,7 @@ export class AuthenticationService extends AuthenticationServiceBase {
283
285
  return this.getMachineState(cookies);
284
286
  }
285
287
  const command = new ConfirmSignUpCommand({
286
- ClientId,
288
+ ClientId: this.#clientId,
287
289
  Username: email,
288
290
  ConfirmationCode: form.inputs.code,
289
291
  });
@@ -309,7 +311,7 @@ export class AuthenticationService extends AuthenticationServiceBase {
309
311
  }
310
312
  try {
311
313
  const command = new InitiateAuthCommand({
312
- ClientId,
314
+ ClientId: this.#clientId,
313
315
  AuthFlow: 'USER_PASSWORD_AUTH',
314
316
  AuthParameters: {
315
317
  USERNAME: form.inputs.email,
@@ -333,7 +335,7 @@ export class AuthenticationService extends AuthenticationServiceBase {
333
335
  catch (error) {
334
336
  if (error.message === 'User is not confirmed.') {
335
337
  const command = new ResendConfirmationCodeCommand({
336
- ClientId,
338
+ ClientId: this.#clientId,
337
339
  Username: form.inputs.email
338
340
  });
339
341
  await client.send(command);
@@ -375,7 +377,7 @@ export class AuthenticationService extends AuthenticationServiceBase {
375
377
  // change password requires an access token, which we don't actually store.
376
378
  // so, first step is to actually authenticate.
377
379
  const authCommand = new InitiateAuthCommand({
378
- ClientId,
380
+ ClientId: this.#clientId,
379
381
  AuthFlow: 'USER_PASSWORD_AUTH',
380
382
  AuthParameters: {
381
383
  USERNAME: state.user.username,
@@ -427,7 +429,7 @@ export class AuthenticationService extends AuthenticationServiceBase {
427
429
  }
428
430
  try {
429
431
  const command = new ForgotPasswordCommand({
430
- ClientId,
432
+ ClientId: this.#clientId,
431
433
  Username: form.inputs.email
432
434
  });
433
435
  await client.send(command);
@@ -459,7 +461,7 @@ export class AuthenticationService extends AuthenticationServiceBase {
459
461
  }
460
462
  try {
461
463
  const command = new ConfirmForgotPasswordCommand({
462
- ClientId,
464
+ ClientId: this.#clientId,
463
465
  ConfirmationCode: form.inputs.code,
464
466
  Password: form.inputs.password,
465
467
  Username: state?.metadata
@@ -1,7 +1,5 @@
1
- import { Resource, EmailSender as BaseEmailSender, EmailMessage } from 'wirejs-resources';
1
+ import { Resource, EmailSender as BaseEmailSender, EmailMessage, EmailSenderOptions } from 'wirejs-resources';
2
2
  export declare class EmailSender extends BaseEmailSender {
3
- constructor(scope: Resource | string, id: string, options: {
4
- from: string;
5
- });
3
+ constructor(scope: Resource | string, id: string, options: EmailSenderOptions);
6
4
  send(message: EmailMessage): Promise<void>;
7
5
  }
@@ -2,23 +2,230 @@ import { SESClient, SendEmailCommand, } from '@aws-sdk/client-ses';
2
2
  import { EmailSender as BaseEmailSender, } from 'wirejs-resources';
3
3
  import { addResource } from '../resource-collector.js';
4
4
  const ses = new SESClient();
5
+ const EMAIL_VERIFICATION_ERROR = (senderEmail, recipientAddresses) => `
6
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7
+ ⚠️ AWS SES EMAIL SENDING FAILED - VERIFICATION REQUIRED
8
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
9
+
10
+ Your app attempted to send email but failed because the sender address is not verified.
11
+
12
+ FAILED EMAIL:
13
+ From: ${senderEmail}
14
+ To: ${recipientAddresses.join(', ')}
15
+ Region: ${process.env.AWS_REGION}
16
+
17
+ NEXT STEPS - Choose one of the following options:
18
+
19
+ 📧 OPTION 1: Verify Individual Email Address
20
+ ────────────────────────────────────────────
21
+ 1. Open AWS SES Console: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/verified-identities
22
+ 2. Click "Create identity"
23
+ 3. Select "Email address"
24
+ 4. Enter: ${senderEmail}
25
+ 5. Click "Create identity"
26
+ 6. Check your inbox for verification email and click the link
27
+
28
+ 🌐 OPTION 2: Verify Entire Domain (Recommended for production)
29
+ ───────────────────────────────────────────────────────────────
30
+ 1. Open AWS SES Console: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/verified-identities
31
+ 2. Click "Create identity"
32
+ 3. Select "Domain"
33
+ 4. Enter your domain (e.g., ${senderEmail.split('@')[1]})
34
+ 5. Follow DNS verification steps provided
35
+ 6. Wait for DNS propagation (may take up to 72 hours)
36
+
37
+ 📮 RECIPIENT VERIFICATION (SES Sandbox Mode)
38
+ ─────────────────────────────────────────────────
39
+ Your AWS account is likely in SES sandbox mode. You must ALSO verify recipient addresses:
40
+
41
+ For each recipient (${recipientAddresses.join(', ')}):
42
+ 1. Open: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/verified-identities
43
+ 2. Click "Create identity" → "Email address"
44
+ 3. Enter the recipient email and verify
45
+
46
+ 🚀 PRODUCTION ACCESS (Send to any recipient)
47
+ ──────────────────────────────────────────────
48
+ To send to unverified recipients, request production access:
49
+ 1. Open: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/account
50
+ 2. Click "Request production access"
51
+ 3. Complete the request form
52
+ 4. AWS typically approves within 24 hours
53
+
54
+ 📖 More Information:
55
+ https://docs.aws.amazon.com/ses/latest/dg/verify-addresses-and-domains.html
56
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
57
+ `;
58
+ const DOMAIN_VERIFICATION_ERROR = (domain, senderEmail, recipientAddresses) => `
59
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
60
+ ⚠️ AWS SES DOMAIN SENDING FAILED - DOMAIN VERIFICATION REQUIRED
61
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
62
+
63
+ Your app attempted to send email from a domain but failed because the domain is not verified.
64
+
65
+ FAILED EMAIL:
66
+ From: ${senderEmail}
67
+ Domain: ${domain}
68
+ To: ${recipientAddresses.join(', ')}
69
+ Region: ${process.env.AWS_REGION}
70
+
71
+ NEXT STEPS:
72
+
73
+ 🌐 VERIFY YOUR SENDING DOMAIN
74
+ ────────────────────────────────
75
+ 1. Open AWS SES Console: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/verified-identities
76
+ 2. Click "Create identity"
77
+ 3. Select "Domain"
78
+ 4. Enter domain: ${domain}
79
+ 5. Choose verification method:
80
+ • DNS (recommended): Add TXT records to your DNS
81
+ • Email: Verify via admin email addresses
82
+ 6. Follow the provided instructions
83
+ 7. Wait for verification (DNS may take up to 72 hours)
84
+
85
+ 📮 RECIPIENT VERIFICATION (SES Sandbox Mode)
86
+ ─────────────────────────────────────────────────
87
+ Your AWS account is likely in SES sandbox mode. You must ALSO verify recipient addresses:
88
+
89
+ For each recipient (${recipientAddresses.join(', ')}):
90
+ 1. Open: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/verified-identities
91
+ 2. Click "Create identity" → "Email address"
92
+ 3. Enter the recipient email and verify
93
+
94
+ 🚀 PRODUCTION ACCESS (Send to any recipient)
95
+ ──────────────────────────────────────────────
96
+ To send to unverified recipients, request production access:
97
+ 1. Open: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/account
98
+ 2. Click "Request production access"
99
+ 3. Complete the request form
100
+
101
+ 📖 More Information:
102
+ https://docs.aws.amazon.com/ses/latest/dg/verify-domain-procedure.html
103
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
104
+ `;
105
+ const GENERAL_SES_ERROR = (senderEmail, recipientAddresses, errorMessage) => `
106
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
107
+ ⚠️ AWS SES EMAIL SENDING FAILED
108
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
109
+
110
+ FAILED EMAIL:
111
+ From: ${senderEmail}
112
+ To: ${recipientAddresses.join(', ')}
113
+ Region: ${process.env.AWS_REGION}
114
+ Error: ${errorMessage}
115
+
116
+ TROUBLESHOOTING STEPS:
117
+
118
+ 1️⃣ VERIFY SENDER IDENTITY
119
+ ────────────────────────────
120
+ • Open AWS SES Console: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/verified-identities
121
+ • Ensure your sender address/domain is verified and shows "Verified" status
122
+
123
+ 2️⃣ CHECK RECIPIENT ADDRESSES (Sandbox Mode)
124
+ ─────────────────────────────────────────────
125
+ • If in sandbox mode, verify recipient addresses too
126
+ • Or request production access: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/account
127
+
128
+ 3️⃣ REVIEW SES QUOTAS AND LIMITS
129
+ ──────────────────────────────────
130
+ • Check your sending quota: https://console.aws.amazon.com/ses/home?region=${process.env.AWS_REGION}#/account
131
+ • Verify you haven't exceeded daily/per-second limits
132
+
133
+ 4️⃣ CHECK SES CONFIGURATION
134
+ ────────────────────────────────
135
+ • Ensure SES service is available in region: ${process.env.AWS_REGION}
136
+ • Verify your AWS credentials have SES permissions
137
+
138
+ 📖 AWS SES Documentation:
139
+ https://docs.aws.amazon.com/ses/latest/dg/
140
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
141
+ `;
5
142
  export class EmailSender extends BaseEmailSender {
6
143
  constructor(scope, id, options) {
7
144
  super(scope, id, options);
8
- addResource('EmailSender', { absoluteId: this.absoluteId, from: options.from });
145
+ // Register the resource with appropriate metadata for backend.ts verification
146
+ if ('from' in options) {
147
+ addResource('EmailSender', {
148
+ absoluteId: this.absoluteId,
149
+ from: options.from,
150
+ type: 'email'
151
+ });
152
+ }
153
+ else if ('fromDomain' in options) {
154
+ addResource('EmailSender', {
155
+ absoluteId: this.absoluteId,
156
+ fromDomain: options.fromDomain,
157
+ type: 'domain'
158
+ });
159
+ }
9
160
  }
10
161
  async send(message) {
11
162
  const toAddresses = Array.isArray(message.to) ? message.to : [message.to];
12
- await ses.send(new SendEmailCommand({
13
- Source: this.from,
14
- Destination: { ToAddresses: toAddresses },
15
- Message: {
16
- Subject: { Data: message.subject },
17
- Body: {
18
- Text: { Data: message.body },
19
- ...(message.html !== undefined ? { Html: { Data: message.html } } : {}),
163
+ // Determine sender email address
164
+ let senderEmail;
165
+ if (this.from) {
166
+ // Traditional email-based sender
167
+ senderEmail = this.from;
168
+ }
169
+ else if (this.fromDomain && message.from) {
170
+ // Domain-based sender with message-specific from
171
+ if (!message.from.endsWith(`@${this.fromDomain}`)) {
172
+ throw new Error(`Message from address "${message.from}" must use domain "@${this.fromDomain}"`);
173
+ }
174
+ senderEmail = message.from;
175
+ }
176
+ else if (this.fromDomain) {
177
+ throw new Error('Domain-based EmailSender requires message.from to specify the sender address');
178
+ }
179
+ else {
180
+ throw new Error('EmailSender configuration error: no valid sender address available');
181
+ }
182
+ try {
183
+ await ses.send(new SendEmailCommand({
184
+ Source: senderEmail,
185
+ Destination: { ToAddresses: toAddresses },
186
+ Message: {
187
+ Subject: { Data: message.subject },
188
+ Body: {
189
+ Text: { Data: message.body },
190
+ ...(message.html !== undefined ? { Html: { Data: message.html } } : {}),
191
+ },
20
192
  },
21
- },
22
- }));
193
+ }));
194
+ }
195
+ catch (e) {
196
+ // Enhanced error handling with detailed AWS console instructions
197
+ let errorMessage = '';
198
+ if (e.message.includes('Email address is not verified')) {
199
+ if (this.isDomainSender) {
200
+ errorMessage = DOMAIN_VERIFICATION_ERROR(this.fromDomain, senderEmail, toAddresses);
201
+ }
202
+ else {
203
+ errorMessage = EMAIL_VERIFICATION_ERROR(senderEmail, toAddresses);
204
+ }
205
+ // Send notification email to sender (if possible)
206
+ try {
207
+ await ses.send(new SendEmailCommand({
208
+ Source: senderEmail,
209
+ Destination: { ToAddresses: [senderEmail] },
210
+ Message: {
211
+ Subject: { Data: "⚠️ Email Sending Failed - AWS SES Verification Required" },
212
+ Body: { Text: { Data: errorMessage } }
213
+ }
214
+ }));
215
+ }
216
+ catch (notificationError) {
217
+ // If we can't send notification, that's expected if sender isn't verified
218
+ }
219
+ }
220
+ else {
221
+ // Other SES errors
222
+ errorMessage = GENERAL_SES_ERROR(senderEmail, toAddresses, e.message);
223
+ }
224
+ // Create an enhanced error with our detailed message
225
+ const enhancedError = new Error(errorMessage);
226
+ enhancedError.name = e.name;
227
+ enhancedError.cause = e;
228
+ throw enhancedError;
229
+ }
23
230
  }
24
231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wirejs-deploy-amplify-basic",
3
- "version": "0.1.175",
3
+ "version": "0.1.177",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -45,7 +45,7 @@
45
45
  "recursive-copy": "^2.0.14",
46
46
  "rimraf": "^6.0.1",
47
47
  "wirejs-dom": "^1.0.44",
48
- "wirejs-resources": "^0.1.175"
48
+ "wirejs-resources": "^0.1.177"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@aws-amplify/backend": "^1.14.0",
@@ -1,7 +0,0 @@
1
- import { defineAuth } from '@aws-amplify/backend';
2
-
3
- export const auth = defineAuth({
4
- loginWith: {
5
- email: true,
6
- },
7
- });