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.
Files changed (67) hide show
  1. package/bin/{alchemy-effect.js → alchemy.js} +170 -170
  2. package/bin/alchemy.js.map +1 -0
  3. package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
  4. package/lib/cli/index.d.ts.map +1 -1
  5. package/lib/cli/index.js +167 -167
  6. package/lib/cli/index.js.map +1 -1
  7. package/package.json +38 -38
  8. package/src/AWS/AGENTS.md +10 -10
  9. package/src/AWS/Assets.ts +1 -1
  10. package/src/AWS/AuthProvider.ts +392 -0
  11. package/src/AWS/Credentials.ts +0 -1
  12. package/src/AWS/DynamoDB/Table.ts +63 -10
  13. package/src/AWS/EC2/VpcEndpoint.ts +4 -4
  14. package/src/AWS/EC2/hosted.ts +1 -1
  15. package/src/AWS/ECS/Task.ts +1 -1
  16. package/src/AWS/Kinesis/Stream.ts +44 -8
  17. package/src/AWS/Lambda/Function.ts +218 -10
  18. package/src/AWS/S3/Bucket.ts +26 -19
  19. package/src/AWS/S3/BucketNotifications.ts +1 -1
  20. package/src/AWS/SNS/Topic.ts +47 -10
  21. package/src/AWS/SQS/Queue.ts +66 -0
  22. package/src/Auth/AuthProvider.ts +33 -0
  23. package/src/Auth/Credentials.ts +56 -0
  24. package/src/Auth/Env.ts +45 -0
  25. package/src/Auth/Profile.ts +72 -0
  26. package/src/Auth/index.ts +2 -0
  27. package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
  28. package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
  29. package/src/Cloudflare/Container/ContainerApplication.ts +3 -3
  30. package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
  31. package/src/Cloudflare/Container/StartContainer.ts +1 -1
  32. package/src/Cloudflare/Providers.ts +1 -6
  33. package/src/Cloudflare/R2/R2Bucket.ts +2 -2
  34. package/src/Cloudflare/Website/StaticSite.ts +44 -15
  35. package/src/Cloudflare/Website/Vite.ts +34 -12
  36. package/src/Cloudflare/Workers/Assets.ts +170 -207
  37. package/src/Cloudflare/Workers/DurableObjectNamespace.ts +230 -370
  38. package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
  39. package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
  40. package/src/Cloudflare/Workers/InferEnv.ts +11 -7
  41. package/src/Cloudflare/Workers/Rpc.ts +13 -2
  42. package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
  43. package/src/Cloudflare/Workers/WebSocket.ts +1 -1
  44. package/src/Cloudflare/Workers/Worker.ts +261 -61
  45. package/src/Cloudflare/Workers/index.ts +3 -0
  46. package/src/Construct.ts +2 -2
  47. package/src/GitHub/Secret.ts +52 -7
  48. package/src/GitHub/Variable.ts +45 -2
  49. package/src/Kubernetes/client.ts +2 -2
  50. package/src/Output.ts +1 -1
  51. package/src/Platform.ts +10 -5
  52. package/src/Resource.ts +4 -4
  53. package/src/Test/Vitest.ts +4 -3
  54. package/src/Util/Clank.ts +77 -0
  55. package/src/Util/PlatformServices.ts +21 -0
  56. package/src/Util/dedent.ts +4 -1
  57. package/bin/alchemy-effect.js.map +0 -1
  58. package/src/Daemon/Client.ts +0 -116
  59. package/src/Daemon/Config.ts +0 -28
  60. package/src/Daemon/Errors.ts +0 -48
  61. package/src/Daemon/Lock.ts +0 -162
  62. package/src/Daemon/ProcessRegistry.ts +0 -284
  63. package/src/Daemon/RpcSchema.ts +0 -43
  64. package/src/Daemon/RpcServer.ts +0 -231
  65. package/src/Daemon/index.ts +0 -27
  66. package/src/Spawn.ts +0 -23
  67. /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 { Input, InputProps } from "../../Input.ts";
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 type { AssetsConfig, AssetsProps } from "./Assets.ts";
51
- import * as Assets from "./Assets.ts";
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 { isDurableObjectExport } from "./DurableObjectNamespace.ts";
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: Input<string>;
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: Input<string>;
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 = R2Bucket | D1Database;
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
- <const Bindings extends WorkerBindingProps>(
622
+ <
623
+ const Bindings extends WorkerBindingProps,
624
+ const Assets extends WorkerAssetsConfig | undefined = undefined,
625
+ Req = never,
626
+ >(
583
627
  id: string,
584
- props: InputProps<WorkerProps<Bindings>>,
628
+ props:
629
+ | InputProps<WorkerProps<Bindings, Assets>>
630
+ | Effect.Effect<InputProps<WorkerProps<Bindings, Assets>>, never, Req>,
585
631
  ): Effect.Effect<
586
632
  Worker<{
587
- [B in keyof Bindings]: Bindings[B] extends Effect.Effect<
588
- infer T extends WorkerBindingResource,
589
- any,
590
- any
591
- >
592
- ? T
593
- : Extract<Bindings[B], WorkerBindingResource>;
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.Type === "Cloudflare.D1Database"
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: "d1",
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.R2Bucket"
669
+ : binding.Type === "Cloudflare.D1Database"
619
670
  ? {
620
- type: "r2_bucket",
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
- : // TODO(sam): handle others
630
- undefined;
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) return undefined;
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* read({
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* read(
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-effect/Stack";
1069
- import { WorkerEnvironment, makeDurableObjectBridge${hasWfClasses ? ", makeWorkflowBridge" : ""}, ExportedHandlerMethods } from "alchemy-effect/Cloudflare";
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
- ? read({
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* upload(accountId, name, assets, session);
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
- if (yield* hasChanged(id, news, output)) {
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] = yield* Effect.all([
1784
- listScripts({
1785
- accountId,
1786
- }).pipe(
1787
- Effect.map((workers) =>
1788
- workers.result.find((worker) => worker.id === workerName),
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
- getScriptSubdomain({
1792
- accountId,
1793
- scriptName: workerName,
1794
- }),
1795
- getScriptSettings({
1796
- accountId,
1797
- scriptName: workerName,
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-effect/Construct";
16
- * import { Bucket } from "alchemy-effect/AWS/S3";
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,
@@ -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 — calling it with
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, output }) {
163
+ update: Effect.fn(function* ({ news, olds }) {
119
164
  const octokit = createClient(news);
120
165
 
121
166
  const wasEnv = !!olds.environment;