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.
- package/bin/alchemy.js +12574 -2451
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +2 -0
- package/bin/cli.js +18 -0
- package/bin/commands/bootstrap.ts +88 -4
- package/bin/commands/deploy.ts +54 -20
- package/bin/commands/dev.ts +45 -0
- package/bin/exec.ts +22 -0
- package/lib/AdoptPolicy.d.ts +2 -2
- package/lib/AdoptPolicy.d.ts.map +1 -1
- package/lib/AdoptPolicy.js.map +1 -1
- package/lib/AlchemyContext.d.ts +1 -0
- package/lib/AlchemyContext.d.ts.map +1 -1
- package/lib/AlchemyContext.js +1 -0
- package/lib/AlchemyContext.js.map +1 -1
- package/lib/Build/Command.d.ts.map +1 -1
- package/lib/Build/Command.js +8 -1
- package/lib/Build/Command.js.map +1 -1
- package/lib/Bundle/Bundle.d.ts +21 -9
- package/lib/Bundle/Bundle.d.ts.map +1 -1
- package/lib/Bundle/Bundle.js +29 -15
- package/lib/Bundle/Bundle.js.map +1 -1
- package/lib/Cloudflare/Local/Sidecar.d.ts +27 -0
- package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -0
- package/lib/Cloudflare/Local/Sidecar.js +24 -0
- package/lib/Cloudflare/Local/Sidecar.js.map +1 -0
- package/lib/Cloudflare/Local/SidecarHandlers.d.ts +5 -0
- package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -0
- package/lib/Cloudflare/Local/SidecarHandlers.js +119 -0
- package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -0
- package/lib/Cloudflare/Local/SidecarServer.d.ts +2 -0
- package/lib/Cloudflare/Local/SidecarServer.d.ts.map +1 -0
- package/lib/Cloudflare/Local/SidecarServer.js +27 -0
- package/lib/Cloudflare/Local/SidecarServer.js.map +1 -0
- package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/Secret.js +55 -7
- package/lib/Cloudflare/SecretsStore/Secret.js.map +1 -1
- package/lib/Cloudflare/SecretsStore/SecretsStore.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/SecretsStore.js +35 -11
- package/lib/Cloudflare/SecretsStore/SecretsStore.js.map +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +35 -0
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/State.js +187 -45
- package/lib/Cloudflare/StateStore/State.js.map +1 -1
- package/lib/Cloudflare/StateStore/index.d.ts +1 -1
- package/lib/Cloudflare/StateStore/index.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/index.js +1 -1
- package/lib/Cloudflare/StateStore/index.js.map +1 -1
- package/lib/Cloudflare/Workers/Compatibility.d.ts +6 -0
- package/lib/Cloudflare/Workers/Compatibility.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/Compatibility.js +10 -0
- package/lib/Cloudflare/Workers/Compatibility.js.map +1 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +7 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +81 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -0
- package/lib/Cloudflare/Workers/Worker.d.ts +3 -1
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +89 -31
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts +31 -0
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/WorkerBundle.js +174 -0
- package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -0
- package/lib/Cloudflare/Workers/WorkerName.d.ts +3 -0
- package/lib/Cloudflare/Workers/WorkerName.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/WorkerName.js +9 -0
- package/lib/Cloudflare/Workers/WorkerName.js.map +1 -0
- package/lib/Cloudflare/Workers/index.d.ts +1 -0
- package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/index.js +1 -0
- package/lib/Cloudflare/Workers/index.js.map +1 -1
- package/lib/Platform.d.ts +1 -1
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Sidecar/Lock.d.ts +24 -0
- package/lib/Sidecar/Lock.d.ts.map +1 -0
- package/lib/Sidecar/Lock.js +102 -0
- package/lib/Sidecar/Lock.js.map +1 -0
- package/lib/Sidecar/RpcClient.d.ts +15 -0
- package/lib/Sidecar/RpcClient.d.ts.map +1 -0
- package/lib/Sidecar/RpcClient.js +75 -0
- package/lib/Sidecar/RpcClient.js.map +1 -0
- package/lib/Sidecar/RpcHandler.d.ts +48 -0
- package/lib/Sidecar/RpcHandler.d.ts.map +1 -0
- package/lib/Sidecar/RpcHandler.js +64 -0
- package/lib/Sidecar/RpcHandler.js.map +1 -0
- package/lib/Sidecar/RpcPaths.d.ts +14 -0
- package/lib/Sidecar/RpcPaths.d.ts.map +1 -0
- package/lib/Sidecar/RpcPaths.js +22 -0
- package/lib/Sidecar/RpcPaths.js.map +1 -0
- package/lib/Sidecar/RpcServer.d.ts +10 -0
- package/lib/Sidecar/RpcServer.d.ts.map +1 -0
- package/lib/Sidecar/RpcServer.js +50 -0
- package/lib/Sidecar/RpcServer.js.map +1 -0
- package/lib/Sidecar/RpcTransport.d.ts +22 -0
- package/lib/Sidecar/RpcTransport.d.ts.map +1 -0
- package/lib/Sidecar/RpcTransport.js +81 -0
- package/lib/Sidecar/RpcTransport.js.map +1 -0
- package/lib/Telemetry/Metrics.d.ts +15 -0
- package/lib/Telemetry/Metrics.d.ts.map +1 -1
- package/lib/Telemetry/Metrics.js +20 -0
- package/lib/Telemetry/Metrics.js.map +1 -1
- package/lib/Util/PlatformServices.d.ts +3 -0
- package/lib/Util/PlatformServices.d.ts.map +1 -1
- package/lib/Util/PlatformServices.js +13 -0
- package/lib/Util/PlatformServices.js.map +1 -1
- package/lib/Util/layer-scoped.d.ts +16 -0
- package/lib/Util/layer-scoped.d.ts.map +1 -0
- package/lib/Util/layer-scoped.js +14 -0
- package/lib/Util/layer-scoped.js.map +1 -0
- package/package.json +4 -2
- package/src/AdoptPolicy.ts +9 -9
- package/src/AlchemyContext.ts +2 -0
- package/src/Build/Command.ts +8 -1
- package/src/Bundle/Bundle.ts +65 -25
- package/src/Cloudflare/Local/Sidecar.ts +44 -0
- package/src/Cloudflare/Local/SidecarHandlers.ts +182 -0
- package/src/Cloudflare/Local/SidecarServer.ts +57 -0
- package/src/Cloudflare/SecretsStore/Secret.ts +78 -7
- package/src/Cloudflare/SecretsStore/SecretsStore.ts +47 -12
- package/src/Cloudflare/StateStore/State.ts +281 -64
- package/src/Cloudflare/StateStore/index.ts +1 -1
- package/src/Cloudflare/Workers/Compatibility.ts +12 -0
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +93 -0
- package/src/Cloudflare/Workers/Worker.ts +151 -55
- package/src/Cloudflare/Workers/WorkerBundle.ts +234 -0
- package/src/Cloudflare/Workers/WorkerName.ts +10 -0
- package/src/Cloudflare/Workers/index.ts +1 -0
- package/src/Platform.ts +1 -1
- package/src/Sidecar/Lock.ts +190 -0
- package/src/Sidecar/RpcClient.ts +141 -0
- package/src/Sidecar/RpcHandler.ts +169 -0
- package/src/Sidecar/RpcPaths.ts +29 -0
- package/src/Sidecar/RpcServer.ts +80 -0
- package/src/Sidecar/RpcTransport.ts +85 -0
- package/src/Telemetry/Metrics.ts +38 -0
- package/src/Util/PlatformServices.ts +19 -0
- 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
|
|
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
|
-
|
|
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:
|
|
140
|
-
secretName:
|
|
141
|
-
storeId:
|
|
209
|
+
secretId: existing.id,
|
|
210
|
+
secretName: existing.name,
|
|
211
|
+
storeId: existing.storeId,
|
|
142
212
|
accountId: news.store.accountId,
|
|
143
|
-
status:
|
|
213
|
+
status: patched?.status ?? existing.status,
|
|
144
214
|
scopes,
|
|
145
|
-
comment:
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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;
|