alchemy 2.0.0-beta.3 → 2.0.0-beta.4
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/{alchemy-effect.js → alchemy.js} +170 -170
- package/bin/alchemy.js.map +1 -0
- package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +167 -167
- package/lib/cli/index.js.map +1 -1
- package/package.json +38 -38
- package/src/AWS/AGENTS.md +10 -10
- package/src/AWS/Assets.ts +1 -1
- package/src/AWS/AuthProvider.ts +392 -0
- package/src/AWS/Credentials.ts +0 -1
- package/src/AWS/DynamoDB/Table.ts +63 -10
- package/src/AWS/EC2/VpcEndpoint.ts +4 -4
- package/src/AWS/EC2/hosted.ts +1 -1
- package/src/AWS/ECS/Task.ts +1 -1
- package/src/AWS/Kinesis/Stream.ts +44 -8
- package/src/AWS/Lambda/Function.ts +218 -10
- package/src/AWS/S3/Bucket.ts +26 -19
- package/src/AWS/S3/BucketNotifications.ts +1 -1
- package/src/AWS/SNS/Topic.ts +47 -10
- package/src/AWS/SQS/Queue.ts +66 -0
- package/src/Auth/AuthProvider.ts +33 -0
- package/src/Auth/Credentials.ts +56 -0
- package/src/Auth/Env.ts +45 -0
- package/src/Auth/Profile.ts +72 -0
- package/src/Auth/index.ts +2 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
- package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
- package/src/Cloudflare/Container/ContainerApplication.ts +3 -3
- package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/Providers.ts +1 -6
- package/src/Cloudflare/R2/R2Bucket.ts +2 -2
- package/src/Cloudflare/Website/StaticSite.ts +44 -15
- package/src/Cloudflare/Website/Vite.ts +34 -12
- package/src/Cloudflare/Workers/Assets.ts +170 -207
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +230 -370
- package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
- package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
- package/src/Cloudflare/Workers/InferEnv.ts +11 -7
- package/src/Cloudflare/Workers/Rpc.ts +13 -2
- package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
- package/src/Cloudflare/Workers/WebSocket.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +261 -61
- package/src/Cloudflare/Workers/index.ts +3 -0
- package/src/Construct.ts +2 -2
- package/src/GitHub/Secret.ts +52 -7
- package/src/GitHub/Variable.ts +45 -2
- package/src/Kubernetes/client.ts +2 -2
- package/src/Output.ts +1 -1
- package/src/Platform.ts +10 -5
- package/src/Resource.ts +4 -4
- package/src/Test/Vitest.ts +4 -3
- package/src/Util/Clank.ts +77 -0
- package/src/Util/PlatformServices.ts +21 -0
- package/src/Util/dedent.ts +4 -1
- package/bin/alchemy-effect.js.map +0 -1
- package/src/Daemon/Client.ts +0 -116
- package/src/Daemon/Config.ts +0 -28
- package/src/Daemon/Errors.ts +0 -48
- package/src/Daemon/Lock.ts +0 -162
- package/src/Daemon/ProcessRegistry.ts +0 -284
- package/src/Daemon/RpcSchema.ts +0 -43
- package/src/Daemon/RpcServer.ts +0 -231
- package/src/Daemon/index.ts +0 -27
- package/src/Spawn.ts +0 -23
- /package/bin/{alchemy-effect.ts → alchemy.ts} +0 -0
|
@@ -2,6 +2,7 @@ import type * as cf from "@cloudflare/workers-types";
|
|
|
2
2
|
import cloudflareRolldown from "@distilled.cloud/cloudflare-rolldown-plugin";
|
|
3
3
|
import cloudflareVite from "@distilled.cloud/cloudflare-vite-plugin";
|
|
4
4
|
import * as workers from "@distilled.cloud/cloudflare/workers";
|
|
5
|
+
import * as zones from "@distilled.cloud/cloudflare/zones";
|
|
5
6
|
import type * as Cause from "effect/Cause";
|
|
6
7
|
import * as Context from "effect/Context";
|
|
7
8
|
import * as Data from "effect/Data";
|
|
@@ -26,7 +27,7 @@ import { findCwdForBundle } from "../../Bundle/TempRoot.ts";
|
|
|
26
27
|
import type { ScopedPlanStatusSession } from "../../Cli/Cli.ts";
|
|
27
28
|
import { isResolved } from "../../Diff.ts";
|
|
28
29
|
import type { HttpEffect } from "../../Http.ts";
|
|
29
|
-
import type {
|
|
30
|
+
import type { InputProps } from "../../Input.ts";
|
|
30
31
|
import * as Output from "../../Output.ts";
|
|
31
32
|
import { createPhysicalName } from "../../PhysicalName.ts";
|
|
32
33
|
import {
|
|
@@ -47,10 +48,20 @@ import { fromCloudflareFetcher } from "../Fetcher.ts";
|
|
|
47
48
|
import { CloudflareLogs } from "../Logs.ts";
|
|
48
49
|
import type { Providers } from "../Providers.ts";
|
|
49
50
|
import type { R2Bucket } from "../R2/R2Bucket.ts";
|
|
50
|
-
import
|
|
51
|
-
|
|
51
|
+
import {
|
|
52
|
+
isAssets,
|
|
53
|
+
readAssets,
|
|
54
|
+
uploadAssets,
|
|
55
|
+
type Assets,
|
|
56
|
+
type AssetsConfig,
|
|
57
|
+
type AssetsProps,
|
|
58
|
+
} from "./Assets.ts";
|
|
52
59
|
import cloudflare_workers from "./cloudflare_workers.ts";
|
|
53
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
isDurableObjectExport,
|
|
62
|
+
isDurableObjectNamespaceLike,
|
|
63
|
+
type DurableObjectNamespaceLike,
|
|
64
|
+
} from "./DurableObjectNamespace.ts";
|
|
54
65
|
import { workersHttpHandler } from "./HttpServer.ts";
|
|
55
66
|
import { Request } from "./Request.ts";
|
|
56
67
|
import { makeRpcStub } from "./Rpc.ts";
|
|
@@ -105,12 +116,12 @@ export interface AssetsWithHash {
|
|
|
105
116
|
/**
|
|
106
117
|
* Path to the assets directory.
|
|
107
118
|
*/
|
|
108
|
-
path:
|
|
119
|
+
path: string;
|
|
109
120
|
/**
|
|
110
121
|
* Pre-computed hash of the assets. When provided, this hash is used for diffing
|
|
111
122
|
* to determine if the worker needs to be redeployed.
|
|
112
123
|
*/
|
|
113
|
-
hash:
|
|
124
|
+
hash: string;
|
|
114
125
|
/**
|
|
115
126
|
* Optional assets configuration.
|
|
116
127
|
*/
|
|
@@ -161,7 +172,11 @@ export type WorkerServices = Worker | WorkerEnvironment | Request;
|
|
|
161
172
|
|
|
162
173
|
export type WorkerShape = Main<WorkerServices>;
|
|
163
174
|
|
|
164
|
-
export type WorkerBindingResource =
|
|
175
|
+
export type WorkerBindingResource =
|
|
176
|
+
| Assets
|
|
177
|
+
| R2Bucket
|
|
178
|
+
| D1Database
|
|
179
|
+
| DurableObjectNamespaceLike<any>;
|
|
165
180
|
|
|
166
181
|
export type WorkerBindings = {
|
|
167
182
|
[bindingName in string]: WorkerBindingResource;
|
|
@@ -173,8 +188,26 @@ export type WorkerBindingProps = {
|
|
|
173
188
|
| Effect.Effect<WorkerBindingResource, any, any>;
|
|
174
189
|
};
|
|
175
190
|
|
|
191
|
+
type NormalizedBindings<
|
|
192
|
+
Bindings extends WorkerBindingProps = {},
|
|
193
|
+
AssetsConfig extends WorkerAssetsConfig | undefined = undefined,
|
|
194
|
+
> = {
|
|
195
|
+
[B in keyof Bindings]: Bindings[B] extends Effect.Effect<
|
|
196
|
+
infer T extends WorkerBindingResource,
|
|
197
|
+
any,
|
|
198
|
+
any
|
|
199
|
+
>
|
|
200
|
+
? T
|
|
201
|
+
: Extract<Bindings[B], WorkerBindingResource>;
|
|
202
|
+
} & (undefined extends AssetsConfig ? {} : { ASSETS: Assets });
|
|
203
|
+
|
|
204
|
+
export type WorkerAssetsConfig = string | AssetsProps | AssetsWithHash;
|
|
205
|
+
|
|
176
206
|
export interface WorkerProps<
|
|
177
207
|
Bindings extends WorkerBindingProps = any,
|
|
208
|
+
Assets extends WorkerAssetsConfig | undefined =
|
|
209
|
+
| WorkerAssetsConfig
|
|
210
|
+
| undefined,
|
|
178
211
|
> extends PlatformProps {
|
|
179
212
|
/**
|
|
180
213
|
* Worker name override. If omitted, Alchemy derives a deterministic physical
|
|
@@ -192,11 +225,7 @@ export interface WorkerProps<
|
|
|
192
225
|
* - An AssetsProps object with directory and config
|
|
193
226
|
* - An object with path and hash (e.g., from a Build resource)
|
|
194
227
|
*/
|
|
195
|
-
assets?:
|
|
196
|
-
| string
|
|
197
|
-
| AssetsProps
|
|
198
|
-
| AssetsWithHash
|
|
199
|
-
| (AssetsWithHash & { [K: string]: any });
|
|
228
|
+
assets?: Assets;
|
|
200
229
|
subdomain?: {
|
|
201
230
|
enabled?: boolean;
|
|
202
231
|
previewsEnabled?: boolean;
|
|
@@ -219,6 +248,12 @@ export interface WorkerProps<
|
|
|
219
248
|
env?: Record<string, string | Redacted.Redacted<string>>;
|
|
220
249
|
exports?: string[];
|
|
221
250
|
bindings?: Bindings;
|
|
251
|
+
/**
|
|
252
|
+
* One or more custom hostnames (e.g. `"app.example.com"`) to bind to this
|
|
253
|
+
* Worker. The Cloudflare Zone is inferred from the hostname — the zone must
|
|
254
|
+
* already exist in the account.
|
|
255
|
+
*/
|
|
256
|
+
domain?: string | string[];
|
|
222
257
|
build?: {
|
|
223
258
|
/**
|
|
224
259
|
* Whether to generate a metafile for the worker bundle.
|
|
@@ -239,6 +274,7 @@ export type Worker<Bindings extends WorkerBindings = any> = Resource<
|
|
|
239
274
|
tags: string[] | undefined;
|
|
240
275
|
durableObjectNamespaces: Record<string, string>;
|
|
241
276
|
accountId: string;
|
|
277
|
+
domains: { hostname: string; id: string; zoneId: string }[];
|
|
242
278
|
hash?: {
|
|
243
279
|
assets: string | undefined;
|
|
244
280
|
bundle: string | undefined;
|
|
@@ -295,6 +331,10 @@ export type Worker<Bindings extends WorkerBindings = any> = Resource<
|
|
|
295
331
|
* at runtime, and `Cloudflare.InferEnv` to extract a fully typed
|
|
296
332
|
* `env` object from those bindings.
|
|
297
333
|
*
|
|
334
|
+
* See the {@link https://alchemy.run/guides/async-worker | Async Workers Guide}
|
|
335
|
+
* for a comprehensive walkthrough of all binding types (R2, D1,
|
|
336
|
+
* Durable Objects, Assets, and more).
|
|
337
|
+
*
|
|
298
338
|
* @example Defining an async Worker in your stack
|
|
299
339
|
* ```typescript
|
|
300
340
|
* // alchemy.run.ts
|
|
@@ -579,19 +619,24 @@ export const Worker: Platform<
|
|
|
579
619
|
WorkerShape,
|
|
580
620
|
WorkerExecutionContext
|
|
581
621
|
> & {
|
|
582
|
-
<
|
|
622
|
+
<
|
|
623
|
+
const Bindings extends WorkerBindingProps,
|
|
624
|
+
const Assets extends WorkerAssetsConfig | undefined = undefined,
|
|
625
|
+
Req = never,
|
|
626
|
+
>(
|
|
583
627
|
id: string,
|
|
584
|
-
props:
|
|
628
|
+
props:
|
|
629
|
+
| InputProps<WorkerProps<Bindings, Assets>>
|
|
630
|
+
| Effect.Effect<InputProps<WorkerProps<Bindings, Assets>>, never, Req>,
|
|
585
631
|
): Effect.Effect<
|
|
586
632
|
Worker<{
|
|
587
|
-
[
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}>
|
|
633
|
+
[binding in keyof NormalizedBindings<
|
|
634
|
+
Bindings,
|
|
635
|
+
Assets
|
|
636
|
+
>]: NormalizedBindings<Bindings, Assets>[binding];
|
|
637
|
+
}>,
|
|
638
|
+
never,
|
|
639
|
+
Req | Providers
|
|
595
640
|
>;
|
|
596
641
|
} = Platform(WorkerTypeId, {
|
|
597
642
|
onCreate: Effect.fnUntraced(function* (
|
|
@@ -608,31 +653,45 @@ export const Worker: Platform<
|
|
|
608
653
|
? yield* bindingEff
|
|
609
654
|
: bindingEff;
|
|
610
655
|
|
|
611
|
-
const bindingMeta: InputProps<WorkerBinding> | undefined =
|
|
612
|
-
binding
|
|
656
|
+
const bindingMeta: InputProps<WorkerBinding> | undefined = isAssets(
|
|
657
|
+
binding,
|
|
658
|
+
)
|
|
659
|
+
? {
|
|
660
|
+
type: "assets",
|
|
661
|
+
name: bindingName,
|
|
662
|
+
}
|
|
663
|
+
: isDurableObjectNamespaceLike(binding)
|
|
613
664
|
? {
|
|
614
|
-
type: "
|
|
615
|
-
id: binding.databaseId,
|
|
665
|
+
type: "durable_object_namespace",
|
|
616
666
|
name: bindingName,
|
|
667
|
+
className: binding.className ?? binding.name,
|
|
617
668
|
}
|
|
618
|
-
: binding.Type === "Cloudflare.
|
|
669
|
+
: binding.Type === "Cloudflare.D1Database"
|
|
619
670
|
? {
|
|
620
|
-
type: "
|
|
671
|
+
type: "d1",
|
|
672
|
+
id: binding.databaseId,
|
|
621
673
|
name: bindingName,
|
|
622
|
-
bucketName: binding.bucketName,
|
|
623
|
-
jurisdiction: binding.jurisdiction.pipe(
|
|
624
|
-
Output.map((jurisdiction) =>
|
|
625
|
-
jurisdiction === "default" ? undefined : jurisdiction,
|
|
626
|
-
),
|
|
627
|
-
),
|
|
628
674
|
}
|
|
629
|
-
:
|
|
630
|
-
|
|
675
|
+
: binding.Type === "Cloudflare.R2Bucket"
|
|
676
|
+
? {
|
|
677
|
+
type: "r2_bucket",
|
|
678
|
+
name: bindingName,
|
|
679
|
+
bucketName: binding.bucketName,
|
|
680
|
+
jurisdiction: binding.jurisdiction.pipe(
|
|
681
|
+
Output.map((jurisdiction) =>
|
|
682
|
+
jurisdiction === "default" ? undefined : jurisdiction,
|
|
683
|
+
),
|
|
684
|
+
),
|
|
685
|
+
}
|
|
686
|
+
: // TODO(sam): handle others
|
|
687
|
+
undefined;
|
|
631
688
|
|
|
632
689
|
if (bindingMeta) {
|
|
633
690
|
yield* resource.bind`${bindingName}`({
|
|
634
691
|
bindings: [bindingMeta],
|
|
635
692
|
});
|
|
693
|
+
} else {
|
|
694
|
+
return yield* Effect.die(`Unknown binding type: ${bindingName}`);
|
|
636
695
|
}
|
|
637
696
|
}
|
|
638
697
|
}
|
|
@@ -857,7 +916,6 @@ export const WorkerProvider = () =>
|
|
|
857
916
|
const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
|
|
858
917
|
const stack = yield* Stack;
|
|
859
918
|
|
|
860
|
-
const { read, upload } = yield* Assets.Assets;
|
|
861
919
|
const createScriptSubdomain = yield* workers.createScriptSubdomain;
|
|
862
920
|
const createScriptTail = yield* workers.createScriptTail;
|
|
863
921
|
const deleteScript = yield* workers.deleteScript;
|
|
@@ -867,6 +925,10 @@ export const WorkerProvider = () =>
|
|
|
867
925
|
const getSubdomain = yield* workers.getSubdomain;
|
|
868
926
|
const listScripts = yield* workers.listScripts;
|
|
869
927
|
const putScript = yield* workers.putScript;
|
|
928
|
+
const putDomain = yield* workers.putDomain;
|
|
929
|
+
const listDomains = yield* workers.listDomains;
|
|
930
|
+
const deleteDomain = yield* workers.deleteDomain;
|
|
931
|
+
const listZones = yield* zones.listZones;
|
|
870
932
|
const telemetry = yield* CloudflareLogs;
|
|
871
933
|
const defaultCompatibilityDate = yield* Effect.promise(() =>
|
|
872
934
|
// @ts-expect-error no types for workerd
|
|
@@ -893,6 +955,95 @@ export const WorkerProvider = () =>
|
|
|
893
955
|
maxLength: 54,
|
|
894
956
|
}).pipe(Effect.map((name) => name.toLowerCase()));
|
|
895
957
|
|
|
958
|
+
const normalizeDomains = (
|
|
959
|
+
domain: string | string[] | undefined,
|
|
960
|
+
): string[] =>
|
|
961
|
+
domain === undefined
|
|
962
|
+
? []
|
|
963
|
+
: Array.isArray(domain)
|
|
964
|
+
? Array.from(new Set(domain))
|
|
965
|
+
: [domain];
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Infer the Cloudflare Zone ID for a given hostname by listing the
|
|
969
|
+
* account's zones and matching the hostname against each zone's name —
|
|
970
|
+
* walking up the DNS label hierarchy until a match is found.
|
|
971
|
+
*/
|
|
972
|
+
const inferZoneIdForHostname = (
|
|
973
|
+
hostname: string,
|
|
974
|
+
zoneCache: Map<string, string>,
|
|
975
|
+
) =>
|
|
976
|
+
Effect.gen(function* () {
|
|
977
|
+
const cached = zoneCache.get(hostname);
|
|
978
|
+
if (cached) return cached;
|
|
979
|
+
|
|
980
|
+
const zoneList = yield* listZones({}).pipe(
|
|
981
|
+
Effect.map((response) => response.result ?? []),
|
|
982
|
+
);
|
|
983
|
+
for (const zone of zoneList) {
|
|
984
|
+
zoneCache.set(zone.name, zone.id);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const parts = hostname.split(".");
|
|
988
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
989
|
+
const candidate = parts.slice(i).join(".");
|
|
990
|
+
const match = zoneList.find((z) => z.name === candidate);
|
|
991
|
+
if (match) {
|
|
992
|
+
zoneCache.set(hostname, match.id);
|
|
993
|
+
return match.id;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return yield* Effect.die(
|
|
997
|
+
`Could not infer Cloudflare Zone for hostname "${hostname}". ` +
|
|
998
|
+
"Ensure the parent zone exists in this account.",
|
|
999
|
+
);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const reconcileDomains = (
|
|
1003
|
+
scriptName: string,
|
|
1004
|
+
desired: string[],
|
|
1005
|
+
previous: Worker["Attributes"]["domains"],
|
|
1006
|
+
) =>
|
|
1007
|
+
Effect.gen(function* () {
|
|
1008
|
+
const desiredSet = new Set(desired);
|
|
1009
|
+
const toRemove = previous.filter((p) => !desiredSet.has(p.hostname));
|
|
1010
|
+
yield* Effect.all(
|
|
1011
|
+
toRemove.map((d) =>
|
|
1012
|
+
deleteDomain({ accountId, domainId: d.id }).pipe(
|
|
1013
|
+
Effect.catchTag("DomainNotFound", () => Effect.void),
|
|
1014
|
+
),
|
|
1015
|
+
),
|
|
1016
|
+
{ concurrency: "unbounded" },
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
if (desired.length === 0) return [];
|
|
1020
|
+
|
|
1021
|
+
const zoneCache = new Map<string, string>();
|
|
1022
|
+
const applied = yield* Effect.all(
|
|
1023
|
+
desired.map((hostname) =>
|
|
1024
|
+
Effect.gen(function* () {
|
|
1025
|
+
const zoneId = yield* inferZoneIdForHostname(
|
|
1026
|
+
hostname,
|
|
1027
|
+
zoneCache,
|
|
1028
|
+
);
|
|
1029
|
+
const res = yield* putDomain({
|
|
1030
|
+
accountId,
|
|
1031
|
+
hostname,
|
|
1032
|
+
service: scriptName,
|
|
1033
|
+
zoneId,
|
|
1034
|
+
});
|
|
1035
|
+
return {
|
|
1036
|
+
hostname,
|
|
1037
|
+
id: res.id ?? "",
|
|
1038
|
+
zoneId: res.zoneId ?? zoneId,
|
|
1039
|
+
};
|
|
1040
|
+
}),
|
|
1041
|
+
),
|
|
1042
|
+
{ concurrency: "unbounded" },
|
|
1043
|
+
);
|
|
1044
|
+
return applied;
|
|
1045
|
+
});
|
|
1046
|
+
|
|
896
1047
|
const createAlchemyWorkerTags = (id: string) => [
|
|
897
1048
|
`alchemy:stack:${stack.name}`,
|
|
898
1049
|
`alchemy:stage:${stack.stage}`,
|
|
@@ -974,7 +1125,9 @@ export const WorkerProvider = () =>
|
|
|
974
1125
|
const prepareAssets = Effect.fnUntraced(function* (
|
|
975
1126
|
assets: WorkerProps["assets"],
|
|
976
1127
|
) {
|
|
977
|
-
if (!assets)
|
|
1128
|
+
if (!assets) {
|
|
1129
|
+
return undefined;
|
|
1130
|
+
}
|
|
978
1131
|
|
|
979
1132
|
// Handle AssetsWithHash (from Build resource)
|
|
980
1133
|
// Props are resolved by Plan, so Input<string> values are already strings at runtime
|
|
@@ -983,7 +1136,7 @@ export const WorkerProvider = () =>
|
|
|
983
1136
|
"path" in assets &&
|
|
984
1137
|
"hash" in assets
|
|
985
1138
|
) {
|
|
986
|
-
const result = yield*
|
|
1139
|
+
const result = yield* readAssets({
|
|
987
1140
|
directory: assets.path as string,
|
|
988
1141
|
config: assets.config,
|
|
989
1142
|
});
|
|
@@ -994,7 +1147,7 @@ export const WorkerProvider = () =>
|
|
|
994
1147
|
}
|
|
995
1148
|
|
|
996
1149
|
// Handle string path or AssetsProps
|
|
997
|
-
return yield*
|
|
1150
|
+
return yield* readAssets(
|
|
998
1151
|
typeof assets === "string" ? { directory: assets } : assets,
|
|
999
1152
|
);
|
|
1000
1153
|
});
|
|
@@ -1065,8 +1218,8 @@ import * as Stream from "effect/Stream";
|
|
|
1065
1218
|
import { env, DurableObject${hasWfClasses ? ", WorkflowEntrypoint" : ""} } from "cloudflare:workers";
|
|
1066
1219
|
import { MinimumLogLevel } from "effect/References";
|
|
1067
1220
|
import { NodeServices } from "@effect/platform-node";
|
|
1068
|
-
import { Stack } from "alchemy
|
|
1069
|
-
import { WorkerEnvironment, makeDurableObjectBridge${hasWfClasses ? ", makeWorkflowBridge" : ""}, ExportedHandlerMethods } from "alchemy
|
|
1221
|
+
import { Stack } from "alchemy/Stack";
|
|
1222
|
+
import { WorkerEnvironment, makeDurableObjectBridge${hasWfClasses ? ", makeWorkflowBridge" : ""}, ExportedHandlerMethods } from "alchemy/Cloudflare";
|
|
1070
1223
|
|
|
1071
1224
|
import entry from "${importPath}";
|
|
1072
1225
|
|
|
@@ -1232,7 +1385,7 @@ ${[
|
|
|
1232
1385
|
const [assets, bundle] = yield* Effect.all(
|
|
1233
1386
|
[
|
|
1234
1387
|
assetsDirectory
|
|
1235
|
-
?
|
|
1388
|
+
? readAssets({
|
|
1236
1389
|
directory: assetsDirectory,
|
|
1237
1390
|
config:
|
|
1238
1391
|
typeof props.assets === "object" && "config" in props.assets
|
|
@@ -1312,7 +1465,12 @@ ${[
|
|
|
1312
1465
|
yield* Effect.logInfo(
|
|
1313
1466
|
`Cloudflare Worker ${olds ? "update" : "create"}: uploading assets for ${name}`,
|
|
1314
1467
|
);
|
|
1315
|
-
const { jwt } = yield*
|
|
1468
|
+
const { jwt } = yield* uploadAssets(
|
|
1469
|
+
accountId,
|
|
1470
|
+
name,
|
|
1471
|
+
assets,
|
|
1472
|
+
session,
|
|
1473
|
+
);
|
|
1316
1474
|
metadataAssets = {
|
|
1317
1475
|
jwt,
|
|
1318
1476
|
config: assets.config,
|
|
@@ -1586,6 +1744,18 @@ ${[
|
|
|
1586
1744
|
);
|
|
1587
1745
|
yield* setWorkerSubdomain(name, enable);
|
|
1588
1746
|
}
|
|
1747
|
+
const desiredDomains = normalizeDomains(news.domain);
|
|
1748
|
+
const previousDomains = output?.domains ?? [];
|
|
1749
|
+
if (desiredDomains.length > 0 || previousDomains.length > 0) {
|
|
1750
|
+
yield* session.note(
|
|
1751
|
+
`Reconciling custom domains (${desiredDomains.length}) ...`,
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
const domains = yield* reconcileDomains(
|
|
1755
|
+
name,
|
|
1756
|
+
desiredDomains,
|
|
1757
|
+
previousDomains,
|
|
1758
|
+
);
|
|
1589
1759
|
return {
|
|
1590
1760
|
workerId: worker.id ?? name,
|
|
1591
1761
|
workerName: name,
|
|
@@ -1597,6 +1767,7 @@ ${[
|
|
|
1597
1767
|
tags: settings.tags ?? metadata.tags,
|
|
1598
1768
|
durableObjectNamespaces,
|
|
1599
1769
|
accountId,
|
|
1770
|
+
domains,
|
|
1600
1771
|
hash,
|
|
1601
1772
|
} satisfies Worker["Attributes"];
|
|
1602
1773
|
});
|
|
@@ -1642,7 +1813,14 @@ ${[
|
|
|
1642
1813
|
if (!output) {
|
|
1643
1814
|
return;
|
|
1644
1815
|
}
|
|
1645
|
-
|
|
1816
|
+
const newDomains = normalizeDomains(news.domain).sort();
|
|
1817
|
+
const oldDomains = (output?.domains ?? [])
|
|
1818
|
+
.map((d) => d.hostname)
|
|
1819
|
+
.sort();
|
|
1820
|
+
const domainsChanged =
|
|
1821
|
+
newDomains.length !== oldDomains.length ||
|
|
1822
|
+
newDomains.some((d, i) => d !== oldDomains[i]);
|
|
1823
|
+
if (domainsChanged || (yield* hasChanged(id, news, output))) {
|
|
1646
1824
|
return {
|
|
1647
1825
|
action: "update",
|
|
1648
1826
|
stables:
|
|
@@ -1771,6 +1949,7 @@ ${[
|
|
|
1771
1949
|
tags: existingSettings?.tags ?? tags,
|
|
1772
1950
|
durableObjectNamespaces,
|
|
1773
1951
|
accountId,
|
|
1952
|
+
domains: [],
|
|
1774
1953
|
} satisfies Worker["Attributes"];
|
|
1775
1954
|
}),
|
|
1776
1955
|
read: Effect.fnUntraced(
|
|
@@ -1780,23 +1959,28 @@ ${[
|
|
|
1780
1959
|
yield* Effect.logInfo(
|
|
1781
1960
|
`Cloudflare Worker read: checking ${workerName}`,
|
|
1782
1961
|
);
|
|
1783
|
-
const [worker, subdomain, settings] =
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1962
|
+
const [worker, subdomain, settings, domainsList] =
|
|
1963
|
+
yield* Effect.all([
|
|
1964
|
+
listScripts({
|
|
1965
|
+
accountId,
|
|
1966
|
+
}).pipe(
|
|
1967
|
+
Effect.map((workers) =>
|
|
1968
|
+
workers.result.find((worker) => worker.id === workerName),
|
|
1969
|
+
),
|
|
1789
1970
|
),
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1971
|
+
getScriptSubdomain({
|
|
1972
|
+
accountId,
|
|
1973
|
+
scriptName: workerName,
|
|
1974
|
+
}),
|
|
1975
|
+
getScriptSettings({
|
|
1976
|
+
accountId,
|
|
1977
|
+
scriptName: workerName,
|
|
1978
|
+
}),
|
|
1979
|
+
listDomains({
|
|
1980
|
+
accountId,
|
|
1981
|
+
service: workerName,
|
|
1982
|
+
}).pipe(Effect.map((r) => r.result ?? [])),
|
|
1983
|
+
]);
|
|
1800
1984
|
if (!worker) {
|
|
1801
1985
|
yield* Effect.logInfo(
|
|
1802
1986
|
`Cloudflare Worker read: ${workerName} not found in script list`,
|
|
@@ -1818,6 +2002,11 @@ ${[
|
|
|
1818
2002
|
durableObjectNamespaces: getDurableObjectNamespaces(
|
|
1819
2003
|
settings.bindings,
|
|
1820
2004
|
),
|
|
2005
|
+
domains: domainsList.flatMap((d) =>
|
|
2006
|
+
d.id && d.hostname && d.zoneId
|
|
2007
|
+
? [{ id: d.id, hostname: d.hostname, zoneId: d.zoneId }]
|
|
2008
|
+
: [],
|
|
2009
|
+
),
|
|
1821
2010
|
} satisfies Worker["Attributes"];
|
|
1822
2011
|
},
|
|
1823
2012
|
(effect) =>
|
|
@@ -1919,6 +2108,17 @@ ${[
|
|
|
1919
2108
|
yield* Effect.logInfo(
|
|
1920
2109
|
`Cloudflare Worker delete: deleting ${output.workerName}`,
|
|
1921
2110
|
);
|
|
2111
|
+
if (output.domains?.length) {
|
|
2112
|
+
yield* Effect.all(
|
|
2113
|
+
output.domains.map((d) =>
|
|
2114
|
+
deleteDomain({
|
|
2115
|
+
accountId: output.accountId,
|
|
2116
|
+
domainId: d.id,
|
|
2117
|
+
}).pipe(Effect.catchTag("DomainNotFound", () => Effect.void)),
|
|
2118
|
+
),
|
|
2119
|
+
{ concurrency: "unbounded" },
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
1922
2122
|
yield* deleteScript({
|
|
1923
2123
|
accountId: output.accountId,
|
|
1924
2124
|
scriptName: output.workerName,
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export * from "./Assets.ts";
|
|
2
2
|
export * from "./ConfigProvider.ts";
|
|
3
3
|
export * from "./DurableObjectNamespace.ts";
|
|
4
|
+
export * from "./DurableObjectState.ts";
|
|
5
|
+
export * from "./DurableObjectStorage.ts";
|
|
4
6
|
export * from "./DynamicWorkerLoader.ts";
|
|
5
7
|
export * from "./Fetch.ts";
|
|
6
8
|
export * from "./HttpServer.ts";
|
|
7
9
|
export * from "./InferEnv.ts";
|
|
8
10
|
export * from "./Request.ts";
|
|
9
11
|
export * from "./Rpc.ts";
|
|
12
|
+
export * from "./ScheduledEvents.ts";
|
|
10
13
|
export * from "./WebSocket.ts";
|
|
11
14
|
export * from "./Worker.ts";
|
|
12
15
|
export * from "./Workflow.ts";
|
package/src/Construct.ts
CHANGED
|
@@ -12,8 +12,8 @@ import * as Namespace from "./Namespace.ts";
|
|
|
12
12
|
* @section Creating Constructs
|
|
13
13
|
* @example Simple Reusable Construct
|
|
14
14
|
* ```typescript
|
|
15
|
-
* import * as Construct from "alchemy
|
|
16
|
-
* import { Bucket } from "alchemy
|
|
15
|
+
* import * as Construct from "alchemy/Construct";
|
|
16
|
+
* import { Bucket } from "alchemy/AWS/S3";
|
|
17
17
|
*
|
|
18
18
|
* export const Logs = Construct.fn(function* (
|
|
19
19
|
* id: string,
|
package/src/GitHub/Secret.ts
CHANGED
|
@@ -53,11 +53,19 @@ export interface Secret extends Resource<
|
|
|
53
53
|
/**
|
|
54
54
|
* A GitHub Actions repository or environment secret.
|
|
55
55
|
*
|
|
56
|
+
* `Secret` manages the lifecycle of an encrypted secret in GitHub Actions.
|
|
56
57
|
* Secrets are encrypted using the repository's (or environment's) public
|
|
57
|
-
* key before being stored. The resource is idempotent —
|
|
58
|
-
* the same name will update the secret value in place.
|
|
58
|
+
* key via `libsodium` before being stored. The resource is idempotent —
|
|
59
|
+
* calling it with the same name will update the secret value in place.
|
|
60
|
+
*
|
|
61
|
+
* Authentication is resolved in order: explicit `token` prop,
|
|
62
|
+
* `GITHUB_ACCESS_TOKEN` env var, `GITHUB_TOKEN` env var. The token needs
|
|
63
|
+
* `repo` scope for private repositories or `public_repo` for public ones.
|
|
59
64
|
*
|
|
60
65
|
* @section Repository Secrets
|
|
66
|
+
* Store secrets accessible to all GitHub Actions workflows in the
|
|
67
|
+
* repository.
|
|
68
|
+
*
|
|
61
69
|
* @example Create a Repository Secret
|
|
62
70
|
* ```typescript
|
|
63
71
|
* yield* GitHub.Secret("aws-role", {
|
|
@@ -69,6 +77,10 @@ export interface Secret extends Resource<
|
|
|
69
77
|
* ```
|
|
70
78
|
*
|
|
71
79
|
* @section Environment Secrets
|
|
80
|
+
* Scope a secret to a specific GitHub Actions environment (e.g.
|
|
81
|
+
* `production`, `staging`). Environment secrets require environment
|
|
82
|
+
* protection rules to be satisfied before workflows can access them.
|
|
83
|
+
*
|
|
72
84
|
* @example Create an Environment Secret
|
|
73
85
|
* ```typescript
|
|
74
86
|
* yield* GitHub.Secret("deploy-key", {
|
|
@@ -79,6 +91,42 @@ export interface Secret extends Resource<
|
|
|
79
91
|
* value: Redacted.make("my-secret-value"),
|
|
80
92
|
* });
|
|
81
93
|
* ```
|
|
94
|
+
*
|
|
95
|
+
* @section Wiring with Other Resources
|
|
96
|
+
* A common pattern is wiring the output of another resource — like an
|
|
97
|
+
* IAM role ARN or a database URL — directly into a GitHub secret so
|
|
98
|
+
* that CI workflows can use it.
|
|
99
|
+
*
|
|
100
|
+
* @example Store an IAM Role ARN for CI
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const role = yield* AWS.IAM.Role("ci-role", { ... });
|
|
103
|
+
*
|
|
104
|
+
* yield* GitHub.Secret("ci-role-arn", {
|
|
105
|
+
* owner: "my-org",
|
|
106
|
+
* repository: "my-repo",
|
|
107
|
+
* name: "AWS_ROLE_ARN",
|
|
108
|
+
* value: Redacted.make(role.roleArn),
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @example Store Multiple Secrets
|
|
113
|
+
* ```typescript
|
|
114
|
+
* yield* GitHub.Secret("db-url", {
|
|
115
|
+
* owner: "my-org",
|
|
116
|
+
* repository: "my-repo",
|
|
117
|
+
* environment: "production",
|
|
118
|
+
* name: "DATABASE_URL",
|
|
119
|
+
* value: Redacted.make(database.connectionString),
|
|
120
|
+
* });
|
|
121
|
+
*
|
|
122
|
+
* yield* GitHub.Secret("api-key", {
|
|
123
|
+
* owner: "my-org",
|
|
124
|
+
* repository: "my-repo",
|
|
125
|
+
* environment: "production",
|
|
126
|
+
* name: "API_KEY",
|
|
127
|
+
* value: Redacted.make(apiKey),
|
|
128
|
+
* });
|
|
129
|
+
* ```
|
|
82
130
|
*/
|
|
83
131
|
export const Secret = Resource<Secret>("GitHub.Secret");
|
|
84
132
|
|
|
@@ -98,10 +146,7 @@ async function encryptValue(
|
|
|
98
146
|
): Promise<string> {
|
|
99
147
|
const sodium = await import("libsodium-wrappers");
|
|
100
148
|
await sodium.ready;
|
|
101
|
-
const binKey = sodium.from_base64(
|
|
102
|
-
publicKey,
|
|
103
|
-
sodium.base64_variants.ORIGINAL,
|
|
104
|
-
);
|
|
149
|
+
const binKey = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
|
|
105
150
|
const binMessage = sodium.from_string(plaintext);
|
|
106
151
|
const encrypted = sodium.crypto_box_seal(binMessage, binKey);
|
|
107
152
|
return sodium.to_base64(encrypted, sodium.base64_variants.ORIGINAL);
|
|
@@ -115,7 +160,7 @@ export const SecretProvider = () =>
|
|
|
115
160
|
return { updatedAt: new Date().toISOString() };
|
|
116
161
|
}),
|
|
117
162
|
|
|
118
|
-
update: Effect.fn(function* ({ news, olds
|
|
163
|
+
update: Effect.fn(function* ({ news, olds }) {
|
|
119
164
|
const octokit = createClient(news);
|
|
120
165
|
|
|
121
166
|
const wasEnv = !!olds.environment;
|