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.
- package/bin/exec.js +1 -1
- package/lib/AWS/Cognito/ManagedLoginBranding.d.ts +108 -0
- package/lib/AWS/Cognito/ManagedLoginBranding.d.ts.map +1 -0
- package/lib/AWS/Cognito/ManagedLoginBranding.js +200 -0
- package/lib/AWS/Cognito/ManagedLoginBranding.js.map +1 -0
- package/lib/AWS/Cognito/UserPool.d.ts +80 -0
- package/lib/AWS/Cognito/UserPool.d.ts.map +1 -1
- package/lib/AWS/Cognito/UserPool.js +72 -4
- package/lib/AWS/Cognito/UserPool.js.map +1 -1
- package/lib/AWS/Cognito/index.d.ts +1 -0
- package/lib/AWS/Cognito/index.d.ts.map +1 -1
- package/lib/AWS/Cognito/index.js +1 -0
- package/lib/AWS/Cognito/index.js.map +1 -1
- package/lib/AWS/Providers.d.ts.map +1 -1
- package/lib/AWS/Providers.js +2 -1
- package/lib/AWS/Providers.js.map +1 -1
- package/lib/AWS/Website/StaticSite.d.ts.map +1 -1
- package/lib/AWS/Website/StaticSite.js +8 -4
- package/lib/AWS/Website/StaticSite.js.map +1 -1
- package/lib/Cli/commands/dev.d.ts +13 -1
- package/lib/Cli/commands/dev.d.ts.map +1 -1
- package/lib/Cli/commands/dev.js +12 -5
- package/lib/Cli/commands/dev.js.map +1 -1
- package/lib/Cloudflare/Email/AllowPolicy.d.ts.map +1 -1
- package/lib/Cloudflare/Email/AllowPolicy.js +9 -3
- package/lib/Cloudflare/Email/AllowPolicy.js.map +1 -1
- package/lib/Cloudflare/Email/ImpersonationRegistryEntry.d.ts.map +1 -1
- package/lib/Cloudflare/Email/ImpersonationRegistryEntry.js +10 -1
- package/lib/Cloudflare/Email/ImpersonationRegistryEntry.js.map +1 -1
- package/lib/Cloudflare/Email/TrustedDomain.d.ts.map +1 -1
- package/lib/Cloudflare/Email/TrustedDomain.js +10 -1
- package/lib/Cloudflare/Email/TrustedDomain.js.map +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Assets.js +14 -7
- package/lib/Cloudflare/Workers/Assets.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerProvider.js +1 -1
- package/lib/Cloudflare/Workers/WorkerProvider.js.map +1 -1
- package/lib/Command/Dev.d.ts.map +1 -1
- package/lib/Command/Dev.js +6 -1
- package/lib/Command/Dev.js.map +1 -1
- package/lib/Planetscale/Postgres/PostgresDefaultRole.d.ts +8 -0
- package/lib/Planetscale/Postgres/PostgresDefaultRole.d.ts.map +1 -1
- package/lib/Planetscale/Postgres/PostgresDefaultRole.js +17 -5
- package/lib/Planetscale/Postgres/PostgresDefaultRole.js.map +1 -1
- package/lib/Planetscale/Postgres/PostgresRole.d.ts +8 -0
- package/lib/Planetscale/Postgres/PostgresRole.d.ts.map +1 -1
- package/lib/Planetscale/Postgres/PostgresRole.js +2 -0
- package/lib/Planetscale/Postgres/PostgresRole.js.map +1 -1
- package/lib/Website/Server.d.ts.map +1 -1
- package/lib/Website/Server.js +11 -1
- package/lib/Website/Server.js.map +1 -1
- package/package.json +6 -6
- package/src/AWS/Cognito/ManagedLoginBranding.ts +339 -0
- package/src/AWS/Cognito/UserPool.ts +144 -6
- package/src/AWS/Cognito/index.ts +1 -0
- package/src/AWS/Providers.ts +4 -0
- package/src/AWS/Website/StaticSite.ts +8 -4
- package/src/Cli/commands/dev.ts +13 -6
- package/src/Cloudflare/Email/AllowPolicy.ts +9 -3
- package/src/Cloudflare/Email/ImpersonationRegistryEntry.ts +9 -1
- package/src/Cloudflare/Email/TrustedDomain.ts +9 -1
- package/src/Cloudflare/Workers/Assets.ts +14 -6
- package/src/Cloudflare/Workers/WorkerProvider.ts +1 -0
- package/src/Command/Dev.ts +9 -1
- package/src/Planetscale/Postgres/PostgresDefaultRole.ts +27 -5
- package/src/Planetscale/Postgres/PostgresRole.ts +12 -0
- 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.
|
|
517
|
-
|
|
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
|
-
"
|
|
628
|
+
"emailConfiguration with emailSendingAccount DEVELOPER requires sourceArn — the ARN of the verified SES identity Cognito sends from",
|
|
524
629
|
}),
|
|
525
630
|
)
|
|
526
|
-
:
|
|
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,
|
package/src/AWS/Cognito/index.ts
CHANGED
|
@@ -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";
|
package/src/AWS/Providers.ts
CHANGED
|
@@ -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
|
-
: [
|
|
775
|
+
: [dist.url];
|
|
772
776
|
}
|
|
773
777
|
|
|
774
778
|
// The edge router signs S3 origin requests with OAC (sigv4, see
|
package/src/Cli/commands/dev.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
176
|
-
// entitlement
|
|
177
|
-
|
|
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
|
);
|