alchemy 2.0.0-beta.75 → 2.0.0-beta.76

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.
Files changed (69) hide show
  1. package/bin/exec.js +1 -1
  2. package/lib/AWS/Cognito/ManagedLoginBranding.d.ts +108 -0
  3. package/lib/AWS/Cognito/ManagedLoginBranding.d.ts.map +1 -0
  4. package/lib/AWS/Cognito/ManagedLoginBranding.js +200 -0
  5. package/lib/AWS/Cognito/ManagedLoginBranding.js.map +1 -0
  6. package/lib/AWS/Cognito/UserPool.d.ts +80 -0
  7. package/lib/AWS/Cognito/UserPool.d.ts.map +1 -1
  8. package/lib/AWS/Cognito/UserPool.js +72 -4
  9. package/lib/AWS/Cognito/UserPool.js.map +1 -1
  10. package/lib/AWS/Cognito/index.d.ts +1 -0
  11. package/lib/AWS/Cognito/index.d.ts.map +1 -1
  12. package/lib/AWS/Cognito/index.js +1 -0
  13. package/lib/AWS/Cognito/index.js.map +1 -1
  14. package/lib/AWS/Providers.d.ts.map +1 -1
  15. package/lib/AWS/Providers.js +2 -1
  16. package/lib/AWS/Providers.js.map +1 -1
  17. package/lib/AWS/Website/StaticSite.d.ts.map +1 -1
  18. package/lib/AWS/Website/StaticSite.js +8 -4
  19. package/lib/AWS/Website/StaticSite.js.map +1 -1
  20. package/lib/Cli/commands/dev.d.ts +13 -1
  21. package/lib/Cli/commands/dev.d.ts.map +1 -1
  22. package/lib/Cli/commands/dev.js +12 -5
  23. package/lib/Cli/commands/dev.js.map +1 -1
  24. package/lib/Cloudflare/Email/AllowPolicy.d.ts.map +1 -1
  25. package/lib/Cloudflare/Email/AllowPolicy.js +9 -3
  26. package/lib/Cloudflare/Email/AllowPolicy.js.map +1 -1
  27. package/lib/Cloudflare/Email/ImpersonationRegistryEntry.d.ts.map +1 -1
  28. package/lib/Cloudflare/Email/ImpersonationRegistryEntry.js +10 -1
  29. package/lib/Cloudflare/Email/ImpersonationRegistryEntry.js.map +1 -1
  30. package/lib/Cloudflare/Email/TrustedDomain.d.ts.map +1 -1
  31. package/lib/Cloudflare/Email/TrustedDomain.js +10 -1
  32. package/lib/Cloudflare/Email/TrustedDomain.js.map +1 -1
  33. package/lib/Cloudflare/Workers/Assets.d.ts +1 -1
  34. package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
  35. package/lib/Cloudflare/Workers/Assets.js +14 -7
  36. package/lib/Cloudflare/Workers/Assets.js.map +1 -1
  37. package/lib/Cloudflare/Workers/WorkerProvider.d.ts.map +1 -1
  38. package/lib/Cloudflare/Workers/WorkerProvider.js +1 -1
  39. package/lib/Cloudflare/Workers/WorkerProvider.js.map +1 -1
  40. package/lib/Command/Dev.d.ts.map +1 -1
  41. package/lib/Command/Dev.js +6 -1
  42. package/lib/Command/Dev.js.map +1 -1
  43. package/lib/Planetscale/Postgres/PostgresDefaultRole.d.ts +8 -0
  44. package/lib/Planetscale/Postgres/PostgresDefaultRole.d.ts.map +1 -1
  45. package/lib/Planetscale/Postgres/PostgresDefaultRole.js +17 -5
  46. package/lib/Planetscale/Postgres/PostgresDefaultRole.js.map +1 -1
  47. package/lib/Planetscale/Postgres/PostgresRole.d.ts +8 -0
  48. package/lib/Planetscale/Postgres/PostgresRole.d.ts.map +1 -1
  49. package/lib/Planetscale/Postgres/PostgresRole.js +2 -0
  50. package/lib/Planetscale/Postgres/PostgresRole.js.map +1 -1
  51. package/lib/Website/Server.d.ts.map +1 -1
  52. package/lib/Website/Server.js +11 -1
  53. package/lib/Website/Server.js.map +1 -1
  54. package/package.json +6 -6
  55. package/src/AWS/Cognito/ManagedLoginBranding.ts +339 -0
  56. package/src/AWS/Cognito/UserPool.ts +144 -6
  57. package/src/AWS/Cognito/index.ts +1 -0
  58. package/src/AWS/Providers.ts +4 -0
  59. package/src/AWS/Website/StaticSite.ts +8 -4
  60. package/src/Cli/commands/dev.ts +13 -6
  61. package/src/Cloudflare/Email/AllowPolicy.ts +9 -3
  62. package/src/Cloudflare/Email/ImpersonationRegistryEntry.ts +9 -1
  63. package/src/Cloudflare/Email/TrustedDomain.ts +9 -1
  64. package/src/Cloudflare/Workers/Assets.ts +14 -6
  65. package/src/Cloudflare/Workers/WorkerProvider.ts +1 -0
  66. package/src/Command/Dev.ts +9 -1
  67. package/src/Planetscale/Postgres/PostgresDefaultRole.ts +27 -5
  68. package/src/Planetscale/Postgres/PostgresRole.ts +12 -0
  69. package/src/Website/Server.ts +16 -1
@@ -0,0 +1,339 @@
1
+ import * as cip from "@distilled.cloud/aws/cognito-identity-provider";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Schedule from "effect/Schedule";
4
+ import { isResolved } from "../../Diff.ts";
5
+ import * as Provider from "../../Provider.ts";
6
+ import { Resource } from "../../Resource.ts";
7
+ import type { Providers } from "../Providers.ts";
8
+
9
+ /**
10
+ * An image asset (logo, favicon, background, …) attached to a managed login
11
+ * branding style, per color mode.
12
+ */
13
+ export interface ManagedLoginBrandingAsset {
14
+ /** Which slot of the managed login pages the asset fills. */
15
+ category:
16
+ | "FAVICON_ICO"
17
+ | "FAVICON_SVG"
18
+ | "EMAIL_GRAPHIC"
19
+ | "SMS_GRAPHIC"
20
+ | "AUTH_APP_GRAPHIC"
21
+ | "PASSWORD_GRAPHIC"
22
+ | "PASSKEY_GRAPHIC"
23
+ | "PAGE_HEADER_LOGO"
24
+ | "PAGE_HEADER_BACKGROUND"
25
+ | "PAGE_FOOTER_LOGO"
26
+ | "PAGE_FOOTER_BACKGROUND"
27
+ | "PAGE_BACKGROUND"
28
+ | "FORM_BACKGROUND"
29
+ | "FORM_LOGO"
30
+ | "IDP_BUTTON_ICON";
31
+ /** The color scheme the asset applies to. */
32
+ colorMode: "LIGHT" | "DARK" | "DYNAMIC";
33
+ /** The file type of the asset. */
34
+ extension: "ICO" | "JPEG" | "PNG" | "SVG" | "WEBP";
35
+ /** The image file, as bytes (max 2 MB). */
36
+ bytes?: Uint8Array;
37
+ /** For `IDP_BUTTON_ICON` assets, the identity provider the icon is for. */
38
+ resourceId?: string;
39
+ }
40
+
41
+ export interface ManagedLoginBrandingProps {
42
+ /**
43
+ * The ID of the user pool the branding style belongs to. Changing this
44
+ * triggers a replacement.
45
+ */
46
+ userPoolId: string;
47
+ /**
48
+ * The ID of the app client the branding style is assigned to. Each app
49
+ * client can have exactly one style. Changing this triggers a
50
+ * replacement.
51
+ */
52
+ clientId: string;
53
+ /**
54
+ * Apply Cognito's default branding instead of custom `settings` /
55
+ * `assets`. This is what makes a fresh pool's managed login pages render
56
+ * without a one-time console step.
57
+ * @default true when neither `settings` nor `assets` is provided
58
+ */
59
+ useCognitoProvidedValues?: boolean;
60
+ /**
61
+ * The branding settings JSON document (colors, component styles, …) in
62
+ * the shape produced by the managed login branding designer / returned by
63
+ * `DescribeManagedLoginBranding`.
64
+ */
65
+ settings?: Record<string, unknown>;
66
+ /**
67
+ * Image assets for the branding style (logos, favicons, backgrounds),
68
+ * one per category + color mode.
69
+ */
70
+ assets?: ManagedLoginBrandingAsset[];
71
+ }
72
+
73
+ export interface ManagedLoginBranding extends Resource<
74
+ "AWS.Cognito.ManagedLoginBranding",
75
+ ManagedLoginBrandingProps,
76
+ {
77
+ /** The generated ID of the branding style. */
78
+ managedLoginBrandingId: string;
79
+ /** The ID of the user pool the style belongs to. */
80
+ userPoolId: string;
81
+ /** The ID of the app client the style is assigned to. */
82
+ clientId: string;
83
+ },
84
+ never,
85
+ Providers
86
+ > {}
87
+
88
+ /**
89
+ * A managed login branding style for an Amazon Cognito user pool app
90
+ * client. Assigning a style (even just Cognito's provided defaults) is what
91
+ * activates the hosted managed login pages — a `UserPoolDomain` with
92
+ * `managedLoginVersion: 2` serves them end-to-end without any console step.
93
+ * ### Activating Managed Login
94
+ * **Example:** Default Branding for a Hosted Login Domain
95
+ * ```typescript
96
+ * import * as Cognito from "alchemy/AWS/Cognito";
97
+ *
98
+ * const pool = yield* Cognito.UserPool("Users", {});
99
+ * const client = yield* Cognito.UserPoolClient("Web", {
100
+ * userPoolId: pool.userPoolId,
101
+ * callbackUrls: ["https://example.com/callback"],
102
+ * allowedOAuthFlowsUserPoolClient: true,
103
+ * allowedOAuthFlows: ["code"],
104
+ * allowedOAuthScopes: ["openid", "email"],
105
+ * });
106
+ * const domain = yield* Cognito.UserPoolDomain("AuthDomain", {
107
+ * userPoolId: pool.userPoolId,
108
+ * managedLoginVersion: 2,
109
+ * });
110
+ * yield* Cognito.ManagedLoginBranding("Branding", {
111
+ * userPoolId: pool.userPoolId,
112
+ * clientId: client.clientId,
113
+ * });
114
+ * ```
115
+ *
116
+ * ### Custom Branding
117
+ * **Example:** Custom Settings and a Logo Asset
118
+ * ```typescript
119
+ * yield* Cognito.ManagedLoginBranding("Branding", {
120
+ * userPoolId: pool.userPoolId,
121
+ * clientId: client.clientId,
122
+ * settings: brandingSettings, // designer-exported JSON document
123
+ * assets: [{
124
+ * category: "FORM_LOGO",
125
+ * colorMode: "LIGHT",
126
+ * extension: "PNG",
127
+ * bytes: logoBytes,
128
+ * }],
129
+ * });
130
+ * ```
131
+ *
132
+ * @resource
133
+ */
134
+ export const ManagedLoginBranding = Resource<ManagedLoginBranding>(
135
+ "AWS.Cognito.ManagedLoginBranding",
136
+ );
137
+
138
+ const toWireAssets = (
139
+ assets: ManagedLoginBrandingAsset[] | undefined,
140
+ ): cip.AssetType[] | undefined =>
141
+ assets?.map((asset) => ({
142
+ Category: asset.category,
143
+ ColorMode: asset.colorMode,
144
+ Extension: asset.extension,
145
+ Bytes: asset.bytes,
146
+ ResourceId: asset.resourceId,
147
+ }));
148
+
149
+ /** Canonicalize an asset list for order-insensitive comparison; bytes are
150
+ * folded to base64. */
151
+ const canonicalAssets = (assets: cip.AssetType[] | undefined) =>
152
+ (assets ?? [])
153
+ .map((asset) =>
154
+ [
155
+ asset.Category,
156
+ asset.ColorMode,
157
+ asset.Extension,
158
+ asset.ResourceId ?? "",
159
+ asset.Bytes === undefined
160
+ ? ""
161
+ : Buffer.from(asset.Bytes).toString("base64"),
162
+ ].join(":"),
163
+ )
164
+ .sort()
165
+ .join(",");
166
+
167
+ /**
168
+ * Whether the style should carry Cognito's provided default values: an
169
+ * explicit prop wins, otherwise defaults to `true` exactly when no custom
170
+ * settings/assets are declared.
171
+ */
172
+ const desiredUseProvidedValues = (news: ManagedLoginBrandingProps) =>
173
+ news.useCognitoProvidedValues ??
174
+ (news.settings === undefined && news.assets === undefined);
175
+
176
+ /**
177
+ * Bounded retry over the concurrent-modification window (branding updates
178
+ * can race domain/pool operations). Explicitly typed so the conditional
179
+ * `Retry.Return` type never leaks into declaration emit.
180
+ */
181
+ const retryWhileConcurrent = <A, E extends { _tag: string }, R>(
182
+ self: Effect.Effect<A, E, R>,
183
+ ): Effect.Effect<A, E, R> =>
184
+ Effect.retry(self, {
185
+ while: (e) => e._tag === "ConcurrentModificationException",
186
+ schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(10)]),
187
+ });
188
+
189
+ export const ManagedLoginBrandingProvider = () =>
190
+ Provider.effect(
191
+ ManagedLoginBranding,
192
+ Effect.gen(function* () {
193
+ const describeById = Effect.fn(function* (
194
+ userPoolId: string,
195
+ managedLoginBrandingId: string,
196
+ ) {
197
+ return yield* cip
198
+ .describeManagedLoginBranding({
199
+ UserPoolId: userPoolId,
200
+ ManagedLoginBrandingId: managedLoginBrandingId,
201
+ })
202
+ .pipe(
203
+ Effect.map((r) => r.ManagedLoginBranding),
204
+ Effect.catchTag("ResourceNotFoundException", () =>
205
+ Effect.succeed(undefined),
206
+ ),
207
+ );
208
+ });
209
+
210
+ const describeByClient = Effect.fn(function* (
211
+ userPoolId: string,
212
+ clientId: string,
213
+ ) {
214
+ return yield* cip
215
+ .describeManagedLoginBrandingByClient({
216
+ UserPoolId: userPoolId,
217
+ ClientId: clientId,
218
+ })
219
+ .pipe(
220
+ Effect.map((r) => r.ManagedLoginBranding),
221
+ Effect.catchTag("ResourceNotFoundException", () =>
222
+ Effect.succeed(undefined),
223
+ ),
224
+ );
225
+ });
226
+
227
+ return ManagedLoginBranding.Provider.of({
228
+ stables: ["managedLoginBrandingId", "userPoolId", "clientId"],
229
+
230
+ // Sub-resource keyed entirely by its user pool (userPoolId) with no
231
+ // global enumeration API of its own — nuke reaches it through the
232
+ // parent's deletion, so enumeration returns empty per the
233
+ // ProviderService doctrine.
234
+ list: () => Effect.succeed([]),
235
+
236
+ read: Effect.fn(function* ({ olds, output }) {
237
+ const userPoolId = output?.userPoolId ?? olds?.userPoolId;
238
+ const clientId = output?.clientId ?? olds?.clientId;
239
+ if (userPoolId === undefined) return undefined;
240
+ const observed =
241
+ output?.managedLoginBrandingId !== undefined
242
+ ? yield* describeById(userPoolId, output.managedLoginBrandingId)
243
+ : clientId !== undefined
244
+ ? yield* describeByClient(userPoolId, clientId)
245
+ : undefined;
246
+ if (observed?.ManagedLoginBrandingId === undefined) return undefined;
247
+ return {
248
+ managedLoginBrandingId: observed.ManagedLoginBrandingId,
249
+ userPoolId,
250
+ clientId: clientId!,
251
+ };
252
+ }),
253
+
254
+ diff: Effect.fn(function* ({ news, olds }) {
255
+ if (!isResolved(news)) return undefined;
256
+ if (
257
+ olds?.userPoolId !== news?.userPoolId ||
258
+ olds?.clientId !== news?.clientId
259
+ ) {
260
+ return { action: "replace" } as const;
261
+ }
262
+ }),
263
+
264
+ reconcile: Effect.fn(function* ({ news, output, session }) {
265
+ const { userPoolId, clientId } = news;
266
+ const useProvided = desiredUseProvidedValues(news);
267
+
268
+ // 1. OBSERVE — output.managedLoginBrandingId is only a cache;
269
+ // fall back to the by-client lookup so state loss converges.
270
+ let observed =
271
+ output?.managedLoginBrandingId !== undefined
272
+ ? yield* describeById(userPoolId, output.managedLoginBrandingId)
273
+ : undefined;
274
+ if (observed === undefined) {
275
+ observed = yield* describeByClient(userPoolId, clientId);
276
+ }
277
+
278
+ // 2. ENSURE — create when missing; a branding created out-of-band
279
+ // (or by a concurrent run) for the same client surfaces as
280
+ // ManagedLoginBrandingExistsException and is taken over.
281
+ if (observed === undefined) {
282
+ observed = yield* cip
283
+ .createManagedLoginBranding({
284
+ UserPoolId: userPoolId,
285
+ ClientId: clientId,
286
+ UseCognitoProvidedValues: useProvided,
287
+ Settings: news.settings,
288
+ Assets: toWireAssets(news.assets),
289
+ })
290
+ .pipe(
291
+ Effect.map((r) => r.ManagedLoginBranding),
292
+ Effect.catchTag("ManagedLoginBrandingExistsException", () =>
293
+ describeByClient(userPoolId, clientId),
294
+ ),
295
+ );
296
+ }
297
+
298
+ const managedLoginBrandingId = observed?.ManagedLoginBrandingId!;
299
+
300
+ // 3. SYNC — diff observed style against desired; skip the update
301
+ // entirely on no-op.
302
+ const drift =
303
+ (observed?.UseCognitoProvidedValues ?? false) !== useProvided ||
304
+ (news.settings !== undefined &&
305
+ JSON.stringify(news.settings) !==
306
+ JSON.stringify(observed?.Settings)) ||
307
+ (news.assets !== undefined &&
308
+ canonicalAssets(toWireAssets(news.assets)) !==
309
+ canonicalAssets(observed?.Assets));
310
+ if (drift) {
311
+ yield* cip
312
+ .updateManagedLoginBranding({
313
+ UserPoolId: userPoolId,
314
+ ManagedLoginBrandingId: managedLoginBrandingId,
315
+ UseCognitoProvidedValues: useProvided,
316
+ Settings: news.settings,
317
+ Assets: toWireAssets(news.assets),
318
+ })
319
+ .pipe(retryWhileConcurrent);
320
+ }
321
+
322
+ yield* session.note(managedLoginBrandingId);
323
+ return { managedLoginBrandingId, userPoolId, clientId };
324
+ }),
325
+
326
+ delete: Effect.fn(function* ({ output }) {
327
+ yield* cip
328
+ .deleteManagedLoginBranding({
329
+ UserPoolId: output.userPoolId,
330
+ ManagedLoginBrandingId: output.managedLoginBrandingId,
331
+ })
332
+ .pipe(
333
+ Effect.catchTag("ResourceNotFoundException", () => Effect.void),
334
+ retryWhileConcurrent,
335
+ );
336
+ }),
337
+ });
338
+ }),
339
+ );
@@ -218,6 +218,47 @@ export interface UserPoolCustomEmailSender {
218
218
  lambdaVersion?: "V1_0";
219
219
  }
220
220
 
221
+ /**
222
+ * How the user pool delivers its email messages (verification codes, OTPs,
223
+ * invitations): either Cognito's built-in, rate-limited delivery
224
+ * (`COGNITO_DEFAULT`) or your own verified Amazon SES identity
225
+ * (`DEVELOPER`). With `DEVELOPER`, the SES identity's policy must grant
226
+ * `cognito-idp.amazonaws.com` permission to send
227
+ * (`SES.EmailIdentityPolicy`).
228
+ */
229
+ export interface UserPoolEmailConfiguration {
230
+ /**
231
+ * Which sending account delivers the pool's email. `COGNITO_DEFAULT`
232
+ * uses Cognito's built-in delivery (limited daily volume);
233
+ * `DEVELOPER` sends through your own SES identity (`sourceArn`
234
+ * required) at your SES quota.
235
+ * @default "COGNITO_DEFAULT"
236
+ */
237
+ emailSendingAccount?: "COGNITO_DEFAULT" | "DEVELOPER";
238
+ /**
239
+ * ARN of a verified SES email identity (address or domain) in a
240
+ * supported region. Required when `emailSendingAccount` is `DEVELOPER`;
241
+ * with `COGNITO_DEFAULT` it customizes the FROM address while Cognito
242
+ * still handles delivery.
243
+ */
244
+ sourceArn?: string;
245
+ /**
246
+ * The FROM address, either bare (`no-reply@example.com`) or with a
247
+ * friendly display name (`"My App <no-reply@example.com>"`). Must be
248
+ * covered by the `sourceArn` identity.
249
+ */
250
+ from?: string;
251
+ /**
252
+ * The destination for replies to the pool's messages.
253
+ */
254
+ replyToEmailAddress?: string;
255
+ /**
256
+ * Name of an SES configuration set applied to email sent by the pool
257
+ * (event publishing, IP pool selection). `DEVELOPER` only.
258
+ */
259
+ configurationSet?: string;
260
+ }
261
+
221
262
  /**
222
263
  * An account recovery mechanism with its priority (1 is highest).
223
264
  */
@@ -322,6 +363,14 @@ export interface UserPoolProps {
322
363
  * never affected by adding or removing it.
323
364
  */
324
365
  customEmailSender?: UserPoolCustomEmailSender;
366
+ /**
367
+ * How the pool delivers its email messages: Cognito's built-in delivery
368
+ * (`COGNITO_DEFAULT`, the service default) or a verified SES identity
369
+ * (`DEVELOPER`) — no custom sender Lambda required. When omitted, an
370
+ * observed email configuration is preserved rather than reset to the
371
+ * service default.
372
+ */
373
+ emailConfiguration?: UserPoolEmailConfiguration;
325
374
  /**
326
375
  * ARN of the symmetric KMS key Cognito uses to encrypt the codes it
327
376
  * passes to the custom sender function(s) (`LambdaConfig.KMSKeyID`).
@@ -394,6 +443,38 @@ export interface UserPool extends Resource<
394
443
  * });
395
444
  * ```
396
445
  *
446
+ * ### Sending Email Through SES
447
+ * **Example:** OTP and Verification Email from a Verified SES Identity
448
+ * ```typescript
449
+ * const identity = yield* SES.EmailIdentity("Sender", {
450
+ * emailIdentity: "mail.example.com",
451
+ * });
452
+ * // allow Cognito to send through the identity
453
+ * yield* SES.EmailIdentityPolicy("CognitoSend", {
454
+ * emailIdentity: identity.emailIdentity,
455
+ * policyName: "cognito",
456
+ * policy: {
457
+ * Version: "2012-10-17",
458
+ * Statement: [{
459
+ * Effect: "Allow",
460
+ * Principal: { Service: "cognito-idp.amazonaws.com" },
461
+ * Action: ["ses:SendEmail", "ses:SendRawEmail"],
462
+ * Resource: identity.identityArn,
463
+ * }],
464
+ * },
465
+ * });
466
+ * const pool = yield* Cognito.UserPool("Users", {
467
+ * usernameAttributes: ["email"],
468
+ * autoVerifiedAttributes: ["email"],
469
+ * emailConfiguration: {
470
+ * emailSendingAccount: "DEVELOPER",
471
+ * sourceArn: identity.identityArn,
472
+ * from: "My App <no-reply@mail.example.com>",
473
+ * replyToEmailAddress: "support@example.com",
474
+ * },
475
+ * });
476
+ * ```
477
+ *
397
478
  * ### Email OTP with a Custom Email Sender
398
479
  * **Example:** Passwordless Email OTP Delivered by Your Own Lambda
399
480
  * ```typescript
@@ -485,6 +566,32 @@ const toWireSignInPolicy = (policy: UserPoolSignInPolicy | undefined) =>
485
566
  ? undefined
486
567
  : { AllowedFirstAuthFactors: policy.allowedFirstAuthFactors };
487
568
 
569
+ const toWireEmailConfiguration = (
570
+ config: UserPoolEmailConfiguration | undefined,
571
+ ): cip.EmailConfigurationType | undefined =>
572
+ config === undefined
573
+ ? undefined
574
+ : {
575
+ EmailSendingAccount: config.emailSendingAccount ?? "COGNITO_DEFAULT",
576
+ SourceArn: config.sourceArn,
577
+ From: config.from,
578
+ ReplyToEmailAddress: config.replyToEmailAddress,
579
+ ConfigurationSet: config.configurationSet,
580
+ };
581
+
582
+ /** Observed/desired email configuration with the service default filled in,
583
+ * for order- and absence-insensitive comparison (`JSON.stringify` drops the
584
+ * `undefined` members on both sides). */
585
+ const normalizedEmailConfiguration = (
586
+ config: cip.EmailConfigurationType | undefined,
587
+ ) => ({
588
+ emailSendingAccount: config?.EmailSendingAccount ?? "COGNITO_DEFAULT",
589
+ sourceArn: config?.SourceArn,
590
+ from: config?.From,
591
+ replyToEmailAddress: config?.ReplyToEmailAddress,
592
+ configurationSet: config?.ConfigurationSet,
593
+ });
594
+
488
595
  const toWireCustomEmailSender = (
489
596
  sender: UserPoolCustomEmailSender | undefined,
490
597
  ): cip.CustomEmailLambdaVersionConfigType | undefined =>
@@ -513,17 +620,25 @@ const validateProps = (props: UserPoolProps) =>
513
620
  "customEmailSender requires kmsKeyId — Cognito encrypts the codes it passes to the custom sender with that KMS key",
514
621
  }),
515
622
  )
516
- : props.tier === "LITE" &&
517
- (props.signInPolicy?.allowedFirstAuthFactors ?? []).some((factor) =>
518
- PASSWORDLESS_AUTH_FACTORS.includes(factor),
519
- )
623
+ : props.emailConfiguration?.emailSendingAccount === "DEVELOPER" &&
624
+ props.emailConfiguration.sourceArn === undefined
520
625
  ? Effect.fail(
521
626
  new InvalidUserPoolConfiguration({
522
627
  reason:
523
- "passwordless first-auth factors (EMAIL_OTP / SMS_OTP / WEB_AUTHN) require the ESSENTIALS or PLUS tier, not LITE",
628
+ "emailConfiguration with emailSendingAccount DEVELOPER requires sourceArn the ARN of the verified SES identity Cognito sends from",
524
629
  }),
525
630
  )
526
- : Effect.void;
631
+ : props.tier === "LITE" &&
632
+ (props.signInPolicy?.allowedFirstAuthFactors ?? []).some((factor) =>
633
+ PASSWORDLESS_AUTH_FACTORS.includes(factor),
634
+ )
635
+ ? Effect.fail(
636
+ new InvalidUserPoolConfiguration({
637
+ reason:
638
+ "passwordless first-auth factors (EMAIL_OTP / SMS_OTP / WEB_AUTHN) require the ESSENTIALS or PLUS tier, not LITE",
639
+ }),
640
+ )
641
+ : Effect.void;
527
642
 
528
643
  const tagRecordOf = (
529
644
  tags: { [key: string]: string | undefined } | undefined,
@@ -709,6 +824,13 @@ export const UserPoolProvider = () =>
709
824
  : toWireSignInPolicy(news.signInPolicy);
710
825
  return {
711
826
  LambdaConfig: lambdaConfig,
827
+ // updateUserPool resets an omitted EmailConfiguration to the
828
+ // service default — echo the OBSERVED configuration back when the
829
+ // prop is undefined so unrelated updates never clear it.
830
+ EmailConfiguration:
831
+ news.emailConfiguration === undefined
832
+ ? observed.EmailConfiguration
833
+ : toWireEmailConfiguration(news.emailConfiguration),
712
834
  Policies:
713
835
  PasswordPolicy === undefined && SignInPolicy === undefined
714
836
  ? undefined
@@ -844,6 +966,19 @@ export const UserPoolProvider = () =>
844
966
  ) {
845
967
  return true;
846
968
  }
969
+ if (
970
+ news.emailConfiguration !== undefined &&
971
+ JSON.stringify(
972
+ normalizedEmailConfiguration(
973
+ toWireEmailConfiguration(news.emailConfiguration),
974
+ ),
975
+ ) !==
976
+ JSON.stringify(
977
+ normalizedEmailConfiguration(observed.EmailConfiguration),
978
+ )
979
+ ) {
980
+ return true;
981
+ }
847
982
  if (
848
983
  news.adminCreateUserOnly !== undefined &&
849
984
  news.adminCreateUserOnly !==
@@ -988,6 +1123,9 @@ export const UserPoolProvider = () =>
988
1123
  DeletionProtection: news.deletionProtection
989
1124
  ? "ACTIVE"
990
1125
  : "INACTIVE",
1126
+ EmailConfiguration: toWireEmailConfiguration(
1127
+ news.emailConfiguration,
1128
+ ),
991
1129
  UsernameAttributes: news.usernameAttributes,
992
1130
  AliasAttributes: news.aliasAttributes,
993
1131
  AutoVerifiedAttributes: news.autoVerifiedAttributes,
@@ -6,6 +6,7 @@ export * from "./IdentityPoolAuth.ts";
6
6
  export * from "./IdentityPoolAuthHttp.ts";
7
7
  export * from "./IdentityPoolRoleAttachment.ts";
8
8
  export * from "./IdentityProvider.ts";
9
+ export * from "./ManagedLoginBranding.ts";
9
10
  export * from "./ResourceServer.ts";
10
11
  export * from "./User.ts";
11
12
  export * from "./UserPool.ts";
@@ -423,6 +423,7 @@ export const providers = () =>
423
423
  Cognito.IdentityPool,
424
424
  Cognito.IdentityPoolRoleAttachment,
425
425
  Cognito.IdentityProvider,
426
+ Cognito.ManagedLoginBranding,
426
427
  Cognito.ResourceServer,
427
428
  Cognito.User,
428
429
  Cognito.UserPool,
@@ -1203,6 +1204,9 @@ export const providers = () =>
1203
1204
  flociDual(Cognito.IdentityProvider, () =>
1204
1205
  Cognito.IdentityProviderProvider(),
1205
1206
  ),
1207
+ flociDual(Cognito.ManagedLoginBranding, () =>
1208
+ Cognito.ManagedLoginBrandingProvider(),
1209
+ ),
1206
1210
  flociDual(Cognito.ResourceServer, () =>
1207
1211
  Cognito.ResourceServerProvider(),
1208
1212
  ),
@@ -760,15 +760,19 @@ export const makeKvSite = Effect.fn("AWS.Website.KvSite")(function* (
760
760
  // Precedence: the canonical domain, then aliases in declaration
761
761
  // order, then the CloudFront default domain (only while
762
762
  // `cloudfrontUrl` is enabled). Redirect hostnames never appear.
763
+ //
764
+ // `dist.url` rather than an interpolated
765
+ // `https://${dist.domainName}` (same as Router): the distribution
766
+ // knows its own address, which is `https://{id}.cloudfront.net` on
767
+ // AWS and `http://localhost:{port}` under `alchemy dev`, where that
768
+ // AWS hostname resolves to nothing.
763
769
  urls = domain
764
770
  ? [
765
771
  Output.interpolate`https://${domain.name}`,
766
772
  ...(domain.aliases ?? []).map((alias) => `https://${alias}`),
767
- ...(props.cloudfrontUrl !== false
768
- ? [Output.interpolate`https://${dist.domainName}`]
769
- : []),
773
+ ...(props.cloudfrontUrl !== false ? [dist.url] : []),
770
774
  ]
771
- : [Output.interpolate`https://${dist.domainName}`];
775
+ : [dist.url];
772
776
  }
773
777
 
774
778
  // The edge router signs S3 origin requests with OAC (sigv4, see
@@ -1,13 +1,14 @@
1
1
  import * as Floci from "@alchemy.run/floci";
2
2
  import * as Effect from "effect/Effect";
3
+ import * as FileSystem from "effect/FileSystem";
3
4
  import * as Option from "effect/Option";
4
5
  import * as Schema from "effect/Schema";
5
6
  import * as Command from "effect/unstable/cli/Command";
6
7
  import * as ChildProcess from "effect/unstable/process/ChildProcess";
7
8
  import { fileURLToPath } from "node:url";
8
- import { transformTypesFlags } from "../../Util/Node.ts";
9
9
  import { SPAWNER_URL_ENV_KEY } from "../../Local/RpcProviderProxy.ts";
10
10
  import * as RpcSpawner from "../../Local/RpcSpawner.ts";
11
+ import { transformTypesFlags } from "../../Util/Node.ts";
11
12
  import { envFile, force, profile, script, stage } from "./_shared.ts";
12
13
  import { ExecStackOptions } from "./deploy.ts";
13
14
 
@@ -18,10 +19,10 @@ import { ExecStackOptions } from "./deploy.ts";
18
19
  * reads its trusted certificates from `NODE_EXTRA_CA_CERTS` at runtime init, so
19
20
  * this must be present in the env of every spawned child (the exec worker, the
20
21
  * RPC sidecar, and the workerd instances they start). `ensureFloci` refreshes
21
- * the bundle at this stable path; never clobber a value the caller set.
22
+ * the bundle at this stable path (and sets the variable itself once written);
23
+ * never clobber a value the caller set, and don't point at a missing file —
24
+ * Node/Bun warn about it on every startup.
22
25
  */
23
- const NODE_EXTRA_CA_CERTS =
24
- process.env.NODE_EXTRA_CA_CERTS ?? Floci.FLOCI_CA_PATH;
25
26
 
26
27
  export const devCommand = Command.make(
27
28
  "dev",
@@ -39,10 +40,14 @@ export const devCommand = Command.make(
39
40
  yes: true,
40
41
  dev: true,
41
42
  });
43
+
44
+ const fs = yield* FileSystem.FileSystem;
42
45
  // Set on THIS process too, so the RPC spawner's sidecars (and the workerd
43
46
  // they launch) inherit it — they are forked from here, not from the exec
44
47
  // child below.
45
- process.env.NODE_EXTRA_CA_CERTS = NODE_EXTRA_CA_CERTS;
48
+ if (yield* fs.exists(Floci.FLOCI_CA_PATH)) {
49
+ process.env.NODE_EXTRA_CA_CERTS ??= Floci.FLOCI_CA_PATH;
50
+ }
46
51
  const spawner = yield* RpcSpawner.RpcSpawner;
47
52
  // We no longer force Bun in development because this prevents us from testing in Node.
48
53
  const command =
@@ -70,7 +75,9 @@ export const devCommand = Command.make(
70
75
  env: {
71
76
  ALCHEMY_EXEC_OPTIONS: JSON.stringify(options),
72
77
  ALCHEMY_DEV: "true",
73
- NODE_EXTRA_CA_CERTS,
78
+ ...(process.env.NODE_EXTRA_CA_CERTS
79
+ ? { NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS }
80
+ : {}),
74
81
  [SPAWNER_URL_ENV_KEY]: spawner.url,
75
82
  },
76
83
  extendEnv: true,
@@ -172,9 +172,15 @@ export const AllowPolicyProvider = () =>
172
172
  ),
173
173
  ),
174
174
  ),
175
- // Email Security is a paid add-on; accounts without the
176
- // entitlement can't enumerate policies treat as empty.
177
- Effect.catchTag("EmailSecurityNotEntitled", () =>
175
+ // Email Security is a paid add-on gated by both account
176
+ // entitlement and token scope: an unentitled account answers
177
+ // `EmailSecurityNotEntitled`, while a credential lacking the
178
+ // Email Security scope (e.g. Cloudflare OAuth) answers a bare
179
+ // `Forbidden`. Neither can enumerate, so both mean "none
180
+ // visible" — matching `Domain.list()`. Returning `[]` is the
181
+ // safe direction for the callers of `list` (orphan detection
182
+ // never deletes what it cannot see).
183
+ Effect.catchTag(["EmailSecurityNotEntitled", "Forbidden"], () =>
178
184
  Effect.succeed([] as AllowPolicyAttributes[]),
179
185
  ),
180
186
  );