wirejs-deploy-amplify-basic 0.1.176 → 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.
- package/amplify-backend-assets/backend.ts +124 -48
- package/amplify-hosting-assets/compute/default/package.json +1 -1
- package/dist/services/authentication.js +11 -9
- package/dist/services/email-sender.d.ts +2 -4
- package/dist/services/email-sender.js +197 -22
- package/package.json +2 -2
- package/amplify-backend-assets/auth/resource.ts +0 -7
|
@@ -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({
|
|
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
|
-
|
|
286
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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: {
|
|
386
|
+
const verificationResults: { identity: string; type: 'email' | 'domain'; status: string }[] = [];
|
|
341
387
|
|
|
342
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
439
|
+
DOMAINS:
|
|
440
|
+
${verificationResults.filter(r => r.type === 'domain').map(r => ` 🌐 ${r.identity} — ${r.status}`).join('\n') || ' (none)'}
|
|
368
441
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
: ''}
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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,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,32 +2,186 @@ 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
|
|
6
|
-
|
|
5
|
+
const EMAIL_VERIFICATION_ERROR = (senderEmail, recipientAddresses) => `
|
|
6
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
7
|
+
⚠️ AWS SES EMAIL SENDING FAILED - VERIFICATION REQUIRED
|
|
8
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
To : ${addresses.join(', ')}
|
|
10
|
+
Your app attempted to send email but failed because the sender address is not verified.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
FAILED EMAIL:
|
|
13
|
+
From: ${senderEmail}
|
|
14
|
+
To: ${recipientAddresses.join(', ')}
|
|
15
|
+
Region: ${process.env.AWS_REGION}
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
NEXT STEPS - Choose one of the following options:
|
|
14
18
|
|
|
15
|
-
|
|
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
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
20
141
|
`;
|
|
21
142
|
export class EmailSender extends BaseEmailSender {
|
|
22
143
|
constructor(scope, id, options) {
|
|
23
144
|
super(scope, id, options);
|
|
24
|
-
|
|
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
|
+
}
|
|
25
160
|
}
|
|
26
161
|
async send(message) {
|
|
27
162
|
const toAddresses = Array.isArray(message.to) ? message.to : [message.to];
|
|
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
|
+
}
|
|
28
182
|
try {
|
|
29
183
|
await ses.send(new SendEmailCommand({
|
|
30
|
-
Source:
|
|
184
|
+
Source: senderEmail,
|
|
31
185
|
Destination: { ToAddresses: toAddresses },
|
|
32
186
|
Message: {
|
|
33
187
|
Subject: { Data: message.subject },
|
|
@@ -39,18 +193,39 @@ export class EmailSender extends BaseEmailSender {
|
|
|
39
193
|
}));
|
|
40
194
|
}
|
|
41
195
|
catch (e) {
|
|
196
|
+
// Enhanced error handling with detailed AWS console instructions
|
|
197
|
+
let errorMessage = '';
|
|
42
198
|
if (e.message.includes('Email address is not verified')) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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);
|
|
52
223
|
}
|
|
53
|
-
|
|
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;
|
|
54
229
|
}
|
|
55
230
|
}
|
|
56
231
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wirejs-deploy-amplify-basic",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
48
|
+
"wirejs-resources": "^0.1.177"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@aws-amplify/backend": "^1.14.0",
|