alchemy 2.0.0-beta.23 → 2.0.0-beta.25

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 (138) hide show
  1. package/bin/alchemy.js +12574 -2451
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +2 -0
  4. package/bin/cli.js +18 -0
  5. package/bin/commands/bootstrap.ts +88 -4
  6. package/bin/commands/deploy.ts +54 -20
  7. package/bin/commands/dev.ts +45 -0
  8. package/bin/exec.ts +22 -0
  9. package/lib/AdoptPolicy.d.ts +2 -2
  10. package/lib/AdoptPolicy.d.ts.map +1 -1
  11. package/lib/AdoptPolicy.js.map +1 -1
  12. package/lib/AlchemyContext.d.ts +1 -0
  13. package/lib/AlchemyContext.d.ts.map +1 -1
  14. package/lib/AlchemyContext.js +1 -0
  15. package/lib/AlchemyContext.js.map +1 -1
  16. package/lib/Build/Command.d.ts.map +1 -1
  17. package/lib/Build/Command.js +8 -1
  18. package/lib/Build/Command.js.map +1 -1
  19. package/lib/Bundle/Bundle.d.ts +21 -9
  20. package/lib/Bundle/Bundle.d.ts.map +1 -1
  21. package/lib/Bundle/Bundle.js +29 -15
  22. package/lib/Bundle/Bundle.js.map +1 -1
  23. package/lib/Cloudflare/Local/Sidecar.d.ts +27 -0
  24. package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -0
  25. package/lib/Cloudflare/Local/Sidecar.js +24 -0
  26. package/lib/Cloudflare/Local/Sidecar.js.map +1 -0
  27. package/lib/Cloudflare/Local/SidecarHandlers.d.ts +5 -0
  28. package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -0
  29. package/lib/Cloudflare/Local/SidecarHandlers.js +119 -0
  30. package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -0
  31. package/lib/Cloudflare/Local/SidecarServer.d.ts +2 -0
  32. package/lib/Cloudflare/Local/SidecarServer.d.ts.map +1 -0
  33. package/lib/Cloudflare/Local/SidecarServer.js +27 -0
  34. package/lib/Cloudflare/Local/SidecarServer.js.map +1 -0
  35. package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
  36. package/lib/Cloudflare/SecretsStore/Secret.js +55 -7
  37. package/lib/Cloudflare/SecretsStore/Secret.js.map +1 -1
  38. package/lib/Cloudflare/SecretsStore/SecretsStore.d.ts.map +1 -1
  39. package/lib/Cloudflare/SecretsStore/SecretsStore.js +35 -11
  40. package/lib/Cloudflare/SecretsStore/SecretsStore.js.map +1 -1
  41. package/lib/Cloudflare/StateStore/State.d.ts +35 -0
  42. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  43. package/lib/Cloudflare/StateStore/State.js +187 -45
  44. package/lib/Cloudflare/StateStore/State.js.map +1 -1
  45. package/lib/Cloudflare/StateStore/index.d.ts +1 -1
  46. package/lib/Cloudflare/StateStore/index.d.ts.map +1 -1
  47. package/lib/Cloudflare/StateStore/index.js +1 -1
  48. package/lib/Cloudflare/StateStore/index.js.map +1 -1
  49. package/lib/Cloudflare/Workers/Compatibility.d.ts +6 -0
  50. package/lib/Cloudflare/Workers/Compatibility.d.ts.map +1 -0
  51. package/lib/Cloudflare/Workers/Compatibility.js +10 -0
  52. package/lib/Cloudflare/Workers/Compatibility.js.map +1 -0
  53. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +7 -0
  54. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -0
  55. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +81 -0
  56. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -0
  57. package/lib/Cloudflare/Workers/Worker.d.ts +3 -1
  58. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  59. package/lib/Cloudflare/Workers/Worker.js +89 -31
  60. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  61. package/lib/Cloudflare/Workers/WorkerBundle.d.ts +31 -0
  62. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -0
  63. package/lib/Cloudflare/Workers/WorkerBundle.js +174 -0
  64. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -0
  65. package/lib/Cloudflare/Workers/WorkerName.d.ts +3 -0
  66. package/lib/Cloudflare/Workers/WorkerName.d.ts.map +1 -0
  67. package/lib/Cloudflare/Workers/WorkerName.js +9 -0
  68. package/lib/Cloudflare/Workers/WorkerName.js.map +1 -0
  69. package/lib/Cloudflare/Workers/index.d.ts +1 -0
  70. package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
  71. package/lib/Cloudflare/Workers/index.js +1 -0
  72. package/lib/Cloudflare/Workers/index.js.map +1 -1
  73. package/lib/Platform.d.ts +1 -1
  74. package/lib/Platform.d.ts.map +1 -1
  75. package/lib/Sidecar/Lock.d.ts +24 -0
  76. package/lib/Sidecar/Lock.d.ts.map +1 -0
  77. package/lib/Sidecar/Lock.js +102 -0
  78. package/lib/Sidecar/Lock.js.map +1 -0
  79. package/lib/Sidecar/RpcClient.d.ts +15 -0
  80. package/lib/Sidecar/RpcClient.d.ts.map +1 -0
  81. package/lib/Sidecar/RpcClient.js +75 -0
  82. package/lib/Sidecar/RpcClient.js.map +1 -0
  83. package/lib/Sidecar/RpcHandler.d.ts +48 -0
  84. package/lib/Sidecar/RpcHandler.d.ts.map +1 -0
  85. package/lib/Sidecar/RpcHandler.js +64 -0
  86. package/lib/Sidecar/RpcHandler.js.map +1 -0
  87. package/lib/Sidecar/RpcPaths.d.ts +14 -0
  88. package/lib/Sidecar/RpcPaths.d.ts.map +1 -0
  89. package/lib/Sidecar/RpcPaths.js +22 -0
  90. package/lib/Sidecar/RpcPaths.js.map +1 -0
  91. package/lib/Sidecar/RpcServer.d.ts +10 -0
  92. package/lib/Sidecar/RpcServer.d.ts.map +1 -0
  93. package/lib/Sidecar/RpcServer.js +50 -0
  94. package/lib/Sidecar/RpcServer.js.map +1 -0
  95. package/lib/Sidecar/RpcTransport.d.ts +22 -0
  96. package/lib/Sidecar/RpcTransport.d.ts.map +1 -0
  97. package/lib/Sidecar/RpcTransport.js +81 -0
  98. package/lib/Sidecar/RpcTransport.js.map +1 -0
  99. package/lib/Telemetry/Metrics.d.ts +15 -0
  100. package/lib/Telemetry/Metrics.d.ts.map +1 -1
  101. package/lib/Telemetry/Metrics.js +20 -0
  102. package/lib/Telemetry/Metrics.js.map +1 -1
  103. package/lib/Util/PlatformServices.d.ts +3 -0
  104. package/lib/Util/PlatformServices.d.ts.map +1 -1
  105. package/lib/Util/PlatformServices.js +13 -0
  106. package/lib/Util/PlatformServices.js.map +1 -1
  107. package/lib/Util/layer-scoped.d.ts +16 -0
  108. package/lib/Util/layer-scoped.d.ts.map +1 -0
  109. package/lib/Util/layer-scoped.js +14 -0
  110. package/lib/Util/layer-scoped.js.map +1 -0
  111. package/package.json +4 -2
  112. package/src/AdoptPolicy.ts +9 -9
  113. package/src/AlchemyContext.ts +2 -0
  114. package/src/Build/Command.ts +8 -1
  115. package/src/Bundle/Bundle.ts +65 -25
  116. package/src/Cloudflare/Local/Sidecar.ts +44 -0
  117. package/src/Cloudflare/Local/SidecarHandlers.ts +182 -0
  118. package/src/Cloudflare/Local/SidecarServer.ts +57 -0
  119. package/src/Cloudflare/SecretsStore/Secret.ts +78 -7
  120. package/src/Cloudflare/SecretsStore/SecretsStore.ts +47 -12
  121. package/src/Cloudflare/StateStore/State.ts +281 -64
  122. package/src/Cloudflare/StateStore/index.ts +1 -1
  123. package/src/Cloudflare/Workers/Compatibility.ts +12 -0
  124. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +93 -0
  125. package/src/Cloudflare/Workers/Worker.ts +151 -55
  126. package/src/Cloudflare/Workers/WorkerBundle.ts +234 -0
  127. package/src/Cloudflare/Workers/WorkerName.ts +10 -0
  128. package/src/Cloudflare/Workers/index.ts +1 -0
  129. package/src/Platform.ts +1 -1
  130. package/src/Sidecar/Lock.ts +190 -0
  131. package/src/Sidecar/RpcClient.ts +141 -0
  132. package/src/Sidecar/RpcHandler.ts +169 -0
  133. package/src/Sidecar/RpcPaths.ts +29 -0
  134. package/src/Sidecar/RpcServer.ts +80 -0
  135. package/src/Sidecar/RpcTransport.ts +85 -0
  136. package/src/Telemetry/Metrics.ts +38 -0
  137. package/src/Util/PlatformServices.ts +19 -0
  138. package/src/Util/layer-scoped.ts +25 -0
@@ -0,0 +1,182 @@
1
+ import * as Server from "@distilled.cloud/cloudflare-runtime/Server";
2
+ import type { WorkerModule } from "@distilled.cloud/cloudflare-runtime/WorkerModule";
3
+ import * as Cause from "effect/Cause";
4
+ import * as Deferred from "effect/Deferred";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Exit from "effect/Exit";
7
+ import * as Fiber from "effect/Fiber";
8
+ import * as Hash from "effect/Hash";
9
+ import * as Layer from "effect/Layer";
10
+ import * as Result from "effect/Result";
11
+ import * as Scope from "effect/Scope";
12
+ import * as Stream from "effect/Stream";
13
+ import * as Bundle from "../../Bundle/Bundle.ts";
14
+ import { WorkerBundle } from "../Workers/WorkerBundle.ts";
15
+ import { Sidecar, type ServeOptions } from "./Sidecar.ts";
16
+
17
+ export const SidecarHandlers = Layer.effect(
18
+ Sidecar,
19
+ Effect.gen(function* () {
20
+ const bundle = yield* WorkerBundle;
21
+ const server = yield* Server.Server;
22
+
23
+ const rootScope = yield* Effect.scope;
24
+ const serverScopes = new Map<string, Scope.Closeable>();
25
+
26
+ const serveScoped = Effect.fnUntraced(function* (
27
+ worker: ServeOptions,
28
+ modules: WorkerModule[],
29
+ ) {
30
+ const scope = yield* Effect.flatMap(Effect.scope, Scope.fork);
31
+ const result = yield* server
32
+ .serve({
33
+ name: worker.id.toLowerCase(),
34
+ compatibilityDate: worker.compatibility.date,
35
+ compatibilityFlags: worker.compatibility.flags,
36
+ bindings: worker.bindings,
37
+ durableObjectNamespaces: worker.durableObjectNamespaces,
38
+ modules,
39
+ })
40
+ .pipe(Scope.provide(scope));
41
+ const previous = serverScopes.get(worker.name);
42
+ if (previous) {
43
+ yield* Effect.forkDetach(Scope.close(previous, Exit.void));
44
+ }
45
+ serverScopes.set(worker.name, scope);
46
+ return result;
47
+ });
48
+
49
+ const watchers = new Map<
50
+ string,
51
+ {
52
+ hash: number;
53
+ fiber: Fiber.Fiber<
54
+ Server.ServeResult,
55
+ Server.ServeError | Bundle.BundleError
56
+ >;
57
+ scope: Scope.Closeable;
58
+ }
59
+ >();
60
+
61
+ const serveFiber = Effect.fnUntraced(function* (worker: ServeOptions) {
62
+ const result = yield* Deferred.make<
63
+ Server.ServeResult,
64
+ Server.ServeError | Bundle.BundleError
65
+ >();
66
+ let start = Date.now();
67
+ yield* bundle.watch(worker).pipe(
68
+ Stream.mapEffect((event) =>
69
+ event._tag === "Error" && !Deferred.isDoneUnsafe(result)
70
+ ? Effect.fail(event.error)
71
+ : Effect.succeed(event),
72
+ ),
73
+ Stream.tap((event) => {
74
+ if (event._tag === "Start") {
75
+ start = Date.now();
76
+ if (Deferred.isDoneUnsafe(result)) {
77
+ return Effect.log(`[${worker.id}] Rebuilding`);
78
+ }
79
+ } else if (event._tag === "Error") {
80
+ return Effect.logError(`[${worker.id}] Bundle error`, event.error);
81
+ }
82
+ return Effect.void;
83
+ }),
84
+ Stream.filterMap((event) =>
85
+ event._tag === "Success"
86
+ ? Result.succeed(event.output)
87
+ : Result.failVoid,
88
+ ),
89
+ Stream.map(bundleOutputToWorkerModules),
90
+ Stream.mapEffect((modules) =>
91
+ serveScoped(worker, modules).pipe(
92
+ Effect.exit,
93
+ Effect.tap((exit) => {
94
+ const isDone = Deferred.isDoneUnsafe(result);
95
+ if (exit._tag === "Success") {
96
+ return Effect.log(
97
+ `[${worker.id}] ${isDone ? "Updated" : "Started"} in ${Math.round(Date.now() - start)}ms`,
98
+ );
99
+ } else if (isDone) {
100
+ return Effect.logError(
101
+ `[${worker.id}] Error`,
102
+ Cause.squash(exit.cause),
103
+ );
104
+ }
105
+ return Effect.void;
106
+ }),
107
+ Effect.tap((exit) => Deferred.complete(result, exit)),
108
+ ),
109
+ ),
110
+ Stream.onExit((exit) =>
111
+ exit._tag === "Failure" && !Deferred.isDoneUnsafe(result)
112
+ ? Deferred.failCause(result, exit.cause)
113
+ : Effect.void,
114
+ ),
115
+ Stream.runDrain,
116
+ Effect.forkScoped,
117
+ );
118
+ return yield* Deferred.await(result);
119
+ });
120
+
121
+ return Sidecar.of({
122
+ serve: Effect.fn(function* (worker: ServeOptions) {
123
+ const hash = Hash.structure(worker);
124
+ const existing = watchers.get(worker.name);
125
+ if (existing) {
126
+ if (existing.hash === hash) {
127
+ yield* Effect.log(
128
+ `[${worker.id}] No changes, using existing watcher`,
129
+ );
130
+ return yield* Fiber.join(existing.fiber);
131
+ }
132
+ yield* Effect.log(
133
+ `[${worker.id}] Changes detected, interrupting existing watcher`,
134
+ );
135
+ yield* Fiber.interrupt(existing.fiber);
136
+ yield* Scope.close(existing.scope, Exit.void);
137
+ watchers.delete(worker.name);
138
+ }
139
+ const scope = yield* Scope.fork(rootScope);
140
+ const fiber = yield* serveFiber(worker).pipe(
141
+ Effect.forkDetach,
142
+ Scope.provide(scope),
143
+ );
144
+ watchers.set(worker.name, { hash, fiber, scope });
145
+ return yield* Fiber.join(fiber).pipe(
146
+ Effect.onExit((exit) =>
147
+ Effect.sync(() => {
148
+ if (exit._tag === "Failure") {
149
+ watchers.delete(worker.name);
150
+ }
151
+ }),
152
+ ),
153
+ );
154
+ }),
155
+ stop: Effect.fn(function* (name: string) {
156
+ const watcher = watchers.get(name);
157
+ if (watcher) {
158
+ yield* Fiber.interrupt(watcher.fiber);
159
+ yield* Scope.close(watcher.scope, Exit.void);
160
+ watchers.delete(name);
161
+ }
162
+ }),
163
+ });
164
+ }),
165
+ );
166
+
167
+ function bundleOutputToWorkerModules(
168
+ bundle: Bundle.BundleOutput,
169
+ ): WorkerModule[] {
170
+ const modules: WorkerModule[] = [];
171
+ for (const file of bundle.files) {
172
+ if (file.path.endsWith(".map") || file.content instanceof Uint8Array) {
173
+ continue;
174
+ }
175
+ modules.push({
176
+ name: file.path,
177
+ type: "ESModule",
178
+ content: file.content,
179
+ });
180
+ }
181
+ return modules;
182
+ }
@@ -0,0 +1,57 @@
1
+ import * as RuntimeServices from "@distilled.cloud/cloudflare-runtime/RuntimeServices";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import * as Path from "effect/Path";
5
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
6
+ import * as AlchemyContext from "../../AlchemyContext.ts";
7
+ import { AuthProviders } from "../../Auth/AuthProvider.ts";
8
+ import * as RpcServer from "../../Sidecar/RpcServer.ts";
9
+ import {
10
+ httpServer,
11
+ PlatformServices,
12
+ runMain,
13
+ } from "../../Util/PlatformServices.ts";
14
+ import { CloudflareAuth } from "../Auth/AuthProvider.ts";
15
+ import * as CloudflareEnvironment from "../CloudflareEnvironment.ts";
16
+ import * as Credentials from "../Credentials.ts";
17
+ import { Sidecar, SidecarSchema } from "./Sidecar.ts";
18
+ import { SidecarHandlers } from "./SidecarHandlers.ts";
19
+
20
+ const apiServices = Layer.merge(
21
+ Credentials.fromAuthProvider(),
22
+ CloudflareEnvironment.fromProfile(),
23
+ ).pipe(
24
+ Layer.provide(CloudflareAuth),
25
+ Layer.provide(Layer.succeed(AuthProviders, {})),
26
+ );
27
+
28
+ const runtimeServices = SidecarHandlers.pipe(
29
+ Layer.provide(
30
+ Layer.unwrap(
31
+ Effect.gen(function* () {
32
+ const { accountId } =
33
+ yield* CloudflareEnvironment.CloudflareEnvironment;
34
+ const { dotAlchemy } = yield* AlchemyContext.AlchemyContext;
35
+ const path = yield* Path.Path;
36
+ return RuntimeServices.layer({
37
+ accountId,
38
+ storage: path.join(dotAlchemy, "local"),
39
+ });
40
+ }),
41
+ ),
42
+ ),
43
+ Layer.provide(
44
+ Layer.mergeAll(apiServices, FetchHttpClient.layer, httpServer()),
45
+ ),
46
+ );
47
+
48
+ const services = Layer.provideMerge(
49
+ Layer.provideMerge(runtimeServices, RpcServer.layerServices(import.meta.url)),
50
+ PlatformServices,
51
+ );
52
+
53
+ RpcServer.makeRpcServer(Sidecar.asEffect(), SidecarSchema).pipe(
54
+ Effect.provide(services),
55
+ Effect.scoped,
56
+ runMain,
57
+ );
@@ -3,6 +3,7 @@ import * as Effect from "effect/Effect";
3
3
  import * as Option from "effect/Option";
4
4
  import * as Redacted from "effect/Redacted";
5
5
  import * as Stream from "effect/Stream";
6
+ import { AdoptPolicy } from "../../AdoptPolicy.ts";
6
7
  import { isResolved } from "../../Diff.ts";
7
8
  import * as Provider from "../../Provider.ts";
8
9
  import { Resource } from "../../Resource.ts";
@@ -122,7 +123,11 @@ export const StoreSecretProvider = () =>
122
123
  create: Effect.fn(function* ({ id, news }) {
123
124
  const name = resolveName(id, news.name);
124
125
  const scopes = resolveScopes(news.scopes);
125
- const response = yield* createStoreSecret({
126
+ const adoptEnabled = yield* Effect.serviceOption(AdoptPolicy).pipe(
127
+ Effect.map(Option.getOrElse(() => false)),
128
+ );
129
+
130
+ const create = createStoreSecret({
126
131
  accountId: news.store.accountId,
127
132
  storeId: news.store.storeId,
128
133
  body: [
@@ -134,15 +139,81 @@ export const StoreSecretProvider = () =>
134
139
  },
135
140
  ],
136
141
  });
137
- const secret = response.result[0]!;
142
+ // Only swallow the conflict when adoption is opted-in via
143
+ // `AdoptPolicy`; otherwise surface `SecretNameAlreadyExists`
144
+ // so the caller learns the secret already exists rather than
145
+ // silently taking ownership of someone else's secret.
146
+ const created = adoptEnabled
147
+ ? yield* create.pipe(
148
+ Effect.catchTag("SecretNameAlreadyExists", () =>
149
+ Effect.succeed(undefined),
150
+ ),
151
+ )
152
+ : yield* create;
153
+
154
+ if (created) {
155
+ const secret = created.result[0]!;
156
+ return {
157
+ secretId: secret.id,
158
+ secretName: secret.name,
159
+ storeId: secret.storeId,
160
+ accountId: news.store.accountId,
161
+ status: secret.status,
162
+ scopes,
163
+ comment: secret.comment ?? undefined,
164
+ };
165
+ }
166
+
167
+ // Adopt-on-conflict path. A secret with this name already
168
+ // exists in Cloudflare — typically because a previous deploy
169
+ // partially failed (e.g. the secret was created server-side
170
+ // but the response was lost so local state never recorded
171
+ // it). We re-fetch the existing secret and reconcile
172
+ // scopes/comment. The value itself cannot be read back from
173
+ // the API and we trust that an identically-named secret in
174
+ // the same store reflects the same intent (this matches how
175
+ // SecretsStore itself adopts the account-wide store).
176
+ const existing = yield* listStoreSecrets
177
+ .items({
178
+ accountId: news.store.accountId,
179
+ storeId: news.store.storeId,
180
+ })
181
+ .pipe(
182
+ Stream.filter((s) => s.name === name),
183
+ Stream.runHead,
184
+ Effect.map(Option.getOrUndefined),
185
+ );
186
+
187
+ if (!existing) {
188
+ return yield* Effect.die(
189
+ new Error(
190
+ `Secret '${name}' reported as already existing in store ${news.store.storeId} but could not be found on lookup.`,
191
+ ),
192
+ );
193
+ }
194
+
195
+ // listStoreSecrets does not surface scopes, so we have no way
196
+ // to detect drift; reconcile to the requested scopes/comment
197
+ // unconditionally. PATCH is cheap and idempotent.
198
+ const patched = yield* patchStoreSecret({
199
+ accountId: news.store.accountId,
200
+ storeId: news.store.storeId,
201
+ secretId: existing.id,
202
+ scopes,
203
+ comment: news.comment,
204
+ }).pipe(
205
+ Effect.catchTag("SecretNotFound", () => Effect.succeed(undefined)),
206
+ );
207
+
138
208
  return {
139
- secretId: secret.id,
140
- secretName: secret.name,
141
- storeId: secret.storeId,
209
+ secretId: existing.id,
210
+ secretName: existing.name,
211
+ storeId: existing.storeId,
142
212
  accountId: news.store.accountId,
143
- status: secret.status,
213
+ status: patched?.status ?? existing.status,
144
214
  scopes,
145
- comment: secret.comment ?? undefined,
215
+ comment:
216
+ patched?.comment ?? news.comment ?? existing.comment ?? undefined,
146
217
  };
147
218
  }),
148
219
  update: Effect.fn(function* ({ news, olds = {} as any, output }) {
@@ -1,5 +1,7 @@
1
1
  import * as secretsStore from "@distilled.cloud/cloudflare/secrets-store";
2
2
  import * as Effect from "effect/Effect";
3
+ import * as Option from "effect/Option";
4
+ import { AdoptPolicy } from "../../AdoptPolicy.ts";
3
5
  import * as Provider from "../../Provider.ts";
4
6
  import { Resource } from "../../Resource.ts";
5
7
  import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
@@ -55,29 +57,62 @@ export const SecretsStoreProvider = () =>
55
57
  return {
56
58
  stables: ["storeId", "storeName", "accountId"],
57
59
  create: Effect.fn(function* () {
58
- const stores = yield* listStores({ accountId });
59
- if (stores.result.length > 0) {
60
- // No name specified — adopt the first (and likely only) store.
61
- const first = stores.result[0]!;
60
+ const adoptEnabled = yield* Effect.serviceOption(AdoptPolicy).pipe(
61
+ Effect.map(Option.getOrElse(() => false)),
62
+ );
63
+
64
+ const adoptExisting = Effect.gen(function* () {
65
+ const stores = yield* listStores({ accountId });
66
+ const first = stores.result[0];
67
+ if (!first) return undefined;
62
68
  return {
63
69
  storeId: first.id,
64
70
  storeName: first.name,
65
71
  accountId,
66
72
  };
73
+ });
74
+
75
+ // Cloudflare allows exactly one Secrets Store per account, so
76
+ // any account that's been touched before may already have one.
77
+ // Only adopt it if the caller opted in via `AdoptPolicy`,
78
+ // otherwise let `createStore` surface MaximumStoresExceeded.
79
+ if (adoptEnabled) {
80
+ const adopted = yield* adoptExisting;
81
+ if (adopted) return adopted;
67
82
  }
68
83
 
69
- // No store exists yet — create one.
70
- const response = yield* createStore({
84
+ const create = createStore({
71
85
  accountId,
72
86
  //`default_secrets_store` is the name cloudflare uses to create a secret store
73
87
  body: [{ name: "default_secrets_store" }],
74
88
  });
75
- const store = response.result[0]!;
76
- return {
77
- storeId: store.id,
78
- storeName: store.name,
79
- accountId,
80
- };
89
+ const response = adoptEnabled
90
+ ? yield* create.pipe(
91
+ // A concurrent process (or a previous partially-failed
92
+ // deploy) may have raced us between list and create.
93
+ Effect.catchTag("MaximumStoresExceeded", () =>
94
+ Effect.succeed(undefined),
95
+ ),
96
+ )
97
+ : yield* create;
98
+
99
+ if (response) {
100
+ const store = response.result[0]!;
101
+ return {
102
+ storeId: store.id,
103
+ storeName: store.name,
104
+ accountId,
105
+ };
106
+ }
107
+
108
+ const recovered = yield* adoptExisting;
109
+ if (recovered) return recovered;
110
+
111
+ return yield* Effect.die(
112
+ new Error(
113
+ `Cloudflare reported MaximumStoresExceeded for account ${accountId} but no store could be listed.`,
114
+ ),
115
+ );
81
116
  }),
82
117
  update: Effect.fn(function* ({ output }) {
83
118
  return output;