alchemy 2.0.0-beta.13 → 2.0.0-beta.14
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 +8386 -8205
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +4 -2
- package/bin/cli.js +61 -0
- package/bin/commands/deploy.ts +6 -3
- package/bin/commands/login.ts +1 -1
- package/bin/commands/logs.ts +1 -1
- package/bin/commands/state.ts +230 -0
- package/bin/commands/tail.ts +1 -1
- package/lib/AWS/AutoScaling/LaunchTemplate.d.ts +1 -1
- package/lib/AWS/EC2/Instance.d.ts +1 -1
- package/lib/AWS/EC2/hosted.d.ts +2 -2
- package/lib/AWS/ECS/Task.d.ts +4 -4
- package/lib/AWS/ECS/Task.d.ts.map +1 -1
- package/lib/AWS/ECS/Task.js +2 -2
- package/lib/AWS/ECS/Task.js.map +1 -1
- package/lib/AWS/Environment.d.ts +2 -2
- package/lib/AWS/Lambda/BucketEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.d.ts +2 -2
- package/lib/AWS/Lambda/Function.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.js +1 -2
- package/lib/AWS/Lambda/Function.js.map +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts.map +1 -1
- package/lib/AWS/SQS/QueueSink.d.ts.map +1 -1
- package/lib/AWS/SQS/QueueSink.js +1 -2
- package/lib/AWS/SQS/QueueSink.js.map +1 -1
- package/lib/Agent/coding/bash.d.ts +2 -6
- package/lib/Agent/coding/bash.d.ts.map +1 -1
- package/lib/Agent/coding/bash.js +2 -3
- package/lib/Agent/coding/bash.js.map +1 -1
- package/lib/Agent/coding/edit.d.ts +1 -1
- package/lib/Agent/coding/glob.d.ts +2 -4
- package/lib/Agent/coding/glob.d.ts.map +1 -1
- package/lib/Agent/coding/glob.js +1 -2
- package/lib/Agent/coding/glob.js.map +1 -1
- package/lib/Agent/coding/grep.d.ts +2 -4
- package/lib/Agent/coding/grep.d.ts.map +1 -1
- package/lib/Agent/coding/grep.js +1 -2
- package/lib/Agent/coding/grep.js.map +1 -1
- package/lib/Agent/coding/read.d.ts +1 -1
- package/lib/Agent/coding/write.d.ts +1 -1
- package/lib/Agent/util/ripgrep.d.ts +2 -2
- package/lib/AlchemyContext.d.ts +13 -0
- package/lib/AlchemyContext.d.ts.map +1 -0
- package/lib/AlchemyContext.js +18 -0
- package/lib/AlchemyContext.js.map +1 -0
- package/lib/Auth/AuthProvider.js +1 -1
- package/lib/Auth/AuthProvider.js.map +1 -1
- package/lib/Auth/Profile.d.ts +1 -1
- package/lib/Auth/index.d.ts +3 -1
- package/lib/Auth/index.d.ts.map +1 -1
- package/lib/Auth/index.js +3 -0
- package/lib/Auth/index.js.map +1 -1
- package/lib/Build/Command.d.ts +1 -1
- package/lib/Build/Memo.d.ts +1 -1
- package/lib/Bundle/Docker.d.ts +3 -3
- package/lib/Bundle/TempRoot.d.ts +3 -3
- package/lib/Cli/InkCLI.d.ts +1 -0
- package/lib/Cli/InkCLI.d.ts.map +1 -1
- package/lib/Cli/InkCLI.js +5 -0
- package/lib/Cli/InkCLI.js.map +1 -1
- package/lib/Cli/LoggingCli.d.ts +4 -0
- package/lib/Cli/LoggingCli.d.ts.map +1 -0
- package/lib/{Test/TestCli.js → Cli/LoggingCli.js} +3 -3
- package/lib/Cli/LoggingCli.js.map +1 -0
- package/lib/Cloudflare/CloudflareEnvironment.d.ts +1 -1
- package/lib/Cloudflare/Container/ContainerApplication.d.ts +2 -2
- package/lib/Cloudflare/Container/ContainerApplication.d.ts.map +1 -1
- package/lib/Cloudflare/Container/ContainerApplication.js +7 -2
- package/lib/Cloudflare/Container/ContainerApplication.js.map +1 -1
- package/lib/Cloudflare/EdgeSession.d.ts +53 -0
- package/lib/Cloudflare/EdgeSession.d.ts.map +1 -0
- package/lib/Cloudflare/EdgeSession.js +78 -0
- package/lib/Cloudflare/EdgeSession.js.map +1 -0
- package/lib/Cloudflare/Providers.d.ts +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +62 -0
- package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/Api.js +101 -0
- package/lib/Cloudflare/StateStore/Api.js.map +1 -0
- package/lib/Cloudflare/StateStore/State.d.ts +39 -0
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/State.js +217 -0
- package/lib/Cloudflare/StateStore/State.js.map +1 -0
- package/lib/Cloudflare/StateStore/Store.d.ts +82 -0
- package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/Store.js +189 -0
- package/lib/Cloudflare/StateStore/Store.js.map +1 -0
- package/lib/Cloudflare/StateStore/Token.d.ts +41 -0
- package/lib/Cloudflare/StateStore/Token.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/Token.js +57 -0
- package/lib/Cloudflare/StateStore/Token.js.map +1 -0
- package/lib/Cloudflare/StateStore/index.d.ts +2 -0
- package/lib/Cloudflare/StateStore/index.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/index.js +2 -0
- package/lib/Cloudflare/StateStore/index.js.map +1 -0
- package/lib/Cloudflare/Workers/Assets.d.ts +2 -2
- package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts +2 -2
- package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/DurableObjectNamespace.js +0 -1
- package/lib/Cloudflare/Workers/DurableObjectNamespace.js.map +1 -1
- package/lib/Cloudflare/Workers/HttpServer.d.ts +1 -1
- package/lib/Cloudflare/Workers/HttpServer.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/HttpServer.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +5 -5
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +0 -1
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/index.d.ts +3 -1
- package/lib/Cloudflare/index.d.ts.map +1 -1
- package/lib/Cloudflare/index.js +3 -1
- package/lib/Cloudflare/index.js.map +1 -1
- package/lib/Deploy.d.ts +10 -0
- package/lib/Deploy.d.ts.map +1 -0
- package/lib/Deploy.js +12 -0
- package/lib/Deploy.js.map +1 -0
- package/lib/Destroy.d.ts +8 -3
- package/lib/Destroy.d.ts.map +1 -1
- package/lib/Destroy.js +8 -8
- package/lib/Destroy.js.map +1 -1
- package/lib/Http.d.ts +1 -1
- package/lib/Http.d.ts.map +1 -1
- package/lib/Http.js +7 -3
- package/lib/Http.js.map +1 -1
- package/lib/Kubernetes/client.d.ts +10 -10
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js.map +1 -1
- package/lib/Platform.d.ts +2 -2
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Platform.js.map +1 -1
- package/lib/Provider.d.ts.map +1 -1
- package/lib/Provider.js +0 -1
- package/lib/Provider.js.map +1 -1
- package/lib/Stack.d.ts +23 -12
- package/lib/Stack.d.ts.map +1 -1
- package/lib/Stack.js +32 -14
- package/lib/Stack.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +96 -0
- package/lib/State/HttpStateApi.d.ts.map +1 -0
- package/lib/State/HttpStateApi.js +96 -0
- package/lib/State/HttpStateApi.js.map +1 -0
- package/lib/State/HttpStateStore.d.ts +10 -0
- package/lib/State/HttpStateStore.d.ts.map +1 -0
- package/lib/State/HttpStateStore.js +102 -0
- package/lib/State/HttpStateStore.js.map +1 -0
- package/lib/State/InMemoryState.d.ts +2 -30
- package/lib/State/InMemoryState.d.ts.map +1 -1
- package/lib/State/InMemoryState.js +23 -18
- package/lib/State/InMemoryState.js.map +1 -1
- package/lib/State/LocalState.d.ts +4 -2
- package/lib/State/LocalState.d.ts.map +1 -1
- package/lib/State/LocalState.js +9 -41
- package/lib/State/LocalState.js.map +1 -1
- package/lib/State/State.d.ts +11 -4
- package/lib/State/State.d.ts.map +1 -1
- package/lib/State/State.js +1 -1
- package/lib/State/State.js.map +1 -1
- package/lib/State/StateEncoding.d.ts +31 -0
- package/lib/State/StateEncoding.d.ts.map +1 -0
- package/lib/State/StateEncoding.js +82 -0
- package/lib/State/StateEncoding.js.map +1 -0
- package/lib/State/Sync.d.ts +23 -0
- package/lib/State/Sync.d.ts.map +1 -0
- package/lib/State/Sync.js +49 -0
- package/lib/State/Sync.js.map +1 -0
- package/lib/State/index.d.ts +4 -0
- package/lib/State/index.d.ts.map +1 -1
- package/lib/State/index.js +4 -0
- package/lib/State/index.js.map +1 -1
- package/lib/Test/Bun.d.ts +13 -17
- package/lib/Test/Bun.d.ts.map +1 -1
- package/lib/Test/Bun.js +16 -33
- package/lib/Test/Bun.js.map +1 -1
- package/lib/Test/TestState.d.ts +1 -2
- package/lib/Test/TestState.d.ts.map +1 -1
- package/lib/Test/TestState.js +5 -9
- package/lib/Test/TestState.js.map +1 -1
- package/lib/Test/Vitest.d.ts +3 -2
- package/lib/Test/Vitest.d.ts.map +1 -1
- package/lib/Test/Vitest.js +20 -15
- package/lib/Test/Vitest.js.map +1 -1
- package/lib/Test/index.d.ts +0 -1
- package/lib/Test/index.d.ts.map +1 -1
- package/lib/Test/index.js +0 -1
- package/lib/Test/index.js.map +1 -1
- package/lib/Util/FileLogger.d.ts +2 -2
- package/lib/Util/FileLogger.d.ts.map +1 -1
- package/lib/Util/FileLogger.js +2 -2
- package/lib/Util/FileLogger.js.map +1 -1
- package/lib/cli/index.d.ts +10 -1
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +9 -4
- package/lib/cli/index.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/package.json +26 -11
- package/src/AWS/ECS/Task.ts +4 -4
- package/src/AWS/Lambda/Function.ts +2 -3
- package/src/AWS/SQS/QueueSink.ts +1 -2
- package/src/Agent/coding/bash.ts +2 -3
- package/src/Agent/coding/glob.ts +1 -2
- package/src/Agent/coding/grep.ts +1 -2
- package/src/AlchemyContext.ts +27 -0
- package/src/Auth/AuthProvider.ts +1 -1
- package/src/Auth/index.ts +3 -1
- package/src/Cli/InkCLI.tsx +10 -0
- package/src/{Test/TestCli.ts → Cli/LoggingCli.ts} +2 -2
- package/src/Cloudflare/Container/ContainerApplication.ts +8 -2
- package/src/Cloudflare/EdgeSession.ts +144 -0
- package/src/Cloudflare/StateStore/Api.ts +157 -0
- package/src/Cloudflare/StateStore/State.ts +318 -0
- package/src/Cloudflare/StateStore/Store.ts +288 -0
- package/src/Cloudflare/StateStore/Token.ts +62 -0
- package/src/Cloudflare/StateStore/index.ts +1 -0
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +1 -6
- package/src/Cloudflare/Workers/HttpServer.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +7 -7
- package/src/Cloudflare/index.ts +3 -1
- package/src/Deploy.ts +25 -0
- package/src/Destroy.ts +25 -13
- package/src/Http.ts +31 -16
- package/src/Output.ts +0 -2
- package/src/Platform.ts +8 -2
- package/src/Provider.ts +0 -2
- package/src/Stack.ts +122 -47
- package/src/State/HttpStateApi.ts +155 -0
- package/src/State/HttpStateStore.ts +155 -0
- package/src/State/InMemoryState.ts +15 -17
- package/src/State/LocalState.ts +13 -45
- package/src/State/State.ts +13 -4
- package/src/State/StateEncoding.ts +83 -0
- package/src/State/Sync.ts +89 -0
- package/src/State/index.ts +4 -0
- package/src/Test/Bun.ts +29 -87
- package/src/Test/TestState.ts +6 -8
- package/src/Test/Vitest.ts +32 -23
- package/src/Test/index.ts +0 -1
- package/src/Util/FileLogger.ts +2 -2
- package/src/index.ts +2 -1
- package/lib/Config.d.ts +0 -20
- package/lib/Config.d.ts.map +0 -1
- package/lib/Config.js +0 -22
- package/lib/Config.js.map +0 -1
- package/lib/Observability.d.ts +0 -2
- package/lib/Observability.d.ts.map +0 -1
- package/lib/Observability.js +0 -1
- package/lib/Observability.js.map +0 -1
- package/lib/Test/TestCli.d.ts +0 -4
- package/lib/Test/TestCli.d.ts.map +0 -1
- package/lib/Test/TestCli.js.map +0 -1
- package/src/Config.ts +0 -31
- package/src/Observability.ts +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import * as Cloudflare from "alchemy/Cloudflare";
|
|
2
|
+
import type { ReplacedResourceState, ResourceState } from "alchemy/State";
|
|
3
|
+
import { encodeState } from "alchemy/State";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import { pipe } from "effect/Function";
|
|
6
|
+
import { EncryptionKey } from "./Token.ts";
|
|
7
|
+
|
|
8
|
+
export default class Store extends Cloudflare.DurableObjectNamespace<Store>()(
|
|
9
|
+
"Store",
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
// Outer (class-level) phase — resolve the binding factory once.
|
|
12
|
+
// The actual secret read happens inside each DO instance below,
|
|
13
|
+
// since `SecretClient.get()` needs the per-instance worker env.
|
|
14
|
+
const encryptionSecret = yield* Cloudflare.Secret.bind(EncryptionKey);
|
|
15
|
+
|
|
16
|
+
return Effect.gen(function* () {
|
|
17
|
+
// Inner (per-instance) phase — set up storage and the AES key.
|
|
18
|
+
// The key is imported once per DO boot and reused thereafter.
|
|
19
|
+
const doState = yield* Cloudflare.DurableObjectState;
|
|
20
|
+
const storage = doState.storage;
|
|
21
|
+
|
|
22
|
+
const keyHex = yield* encryptionSecret.get().pipe(Effect.orDie);
|
|
23
|
+
const cryptoKey = yield* Effect.tryPromise(() =>
|
|
24
|
+
crypto.subtle.importKey(
|
|
25
|
+
"raw",
|
|
26
|
+
hexToBytes(keyHex),
|
|
27
|
+
{ name: "AES-CTR" },
|
|
28
|
+
false,
|
|
29
|
+
["encrypt", "decrypt"],
|
|
30
|
+
),
|
|
31
|
+
).pipe(Effect.orDie);
|
|
32
|
+
|
|
33
|
+
const encryptValue = (value: ResourceState) =>
|
|
34
|
+
Effect.tryPromise(async () => {
|
|
35
|
+
const plaintext = new TextEncoder().encode(
|
|
36
|
+
JSON.stringify(encodeState(value)),
|
|
37
|
+
);
|
|
38
|
+
const counter = crypto.getRandomValues(allocBytes(NONCE_BYTES));
|
|
39
|
+
const ct = new Uint8Array(
|
|
40
|
+
await crypto.subtle.encrypt(
|
|
41
|
+
{ name: "AES-CTR", counter, length: 64 },
|
|
42
|
+
cryptoKey,
|
|
43
|
+
plaintext,
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
// Frame as a single base64 string: nonce || ciphertext.
|
|
47
|
+
const framed = allocBytes(counter.byteLength + ct.byteLength);
|
|
48
|
+
framed.set(counter, 0);
|
|
49
|
+
framed.set(ct, counter.byteLength);
|
|
50
|
+
return toB64(framed);
|
|
51
|
+
}).pipe(Effect.orDie);
|
|
52
|
+
|
|
53
|
+
const decryptEntry = (entry: string) =>
|
|
54
|
+
Effect.tryPromise(async () => {
|
|
55
|
+
const framed = fromB64(entry);
|
|
56
|
+
const counter = framed.slice(0, NONCE_BYTES);
|
|
57
|
+
const ciphertext = framed.slice(NONCE_BYTES);
|
|
58
|
+
const pt = await crypto.subtle.decrypt(
|
|
59
|
+
{ name: "AES-CTR", counter, length: 64 },
|
|
60
|
+
cryptoKey,
|
|
61
|
+
ciphertext,
|
|
62
|
+
);
|
|
63
|
+
return JSON.parse(new TextDecoder().decode(pt)) as ResourceState;
|
|
64
|
+
}).pipe(Effect.orDie);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
// -- Root DO methods -----------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* (Root DO only) List every stack name ever registered.
|
|
71
|
+
*/
|
|
72
|
+
listStacks: Effect.fnUntraced(function* () {
|
|
73
|
+
const entries = yield* storage.list<number>({
|
|
74
|
+
prefix: STACK_INDEX_PREFIX,
|
|
75
|
+
});
|
|
76
|
+
const stacks: string[] = [];
|
|
77
|
+
for (const key of entries.keys()) {
|
|
78
|
+
stacks.push(key.slice(STACK_INDEX_PREFIX.length));
|
|
79
|
+
}
|
|
80
|
+
return stacks;
|
|
81
|
+
}),
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* (Root DO only) Register a stack name. Idempotent — safe to
|
|
85
|
+
* call on every `set` to the corresponding stack DO.
|
|
86
|
+
*/
|
|
87
|
+
registerStack: ({ stack }: { stack: string }) =>
|
|
88
|
+
storage.put(`${STACK_INDEX_PREFIX}${stack}`, 1),
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* (Root DO only) Remove a stack name from the global index.
|
|
92
|
+
*/
|
|
93
|
+
unregisterStack: ({ stack }: { stack: string }) =>
|
|
94
|
+
storage.delete(`${STACK_INDEX_PREFIX}${stack}`),
|
|
95
|
+
|
|
96
|
+
// -- Stack DO methods ----------------------------------------
|
|
97
|
+
|
|
98
|
+
/** (Stack DO only) List stages with at least one resource. */
|
|
99
|
+
listStages: () =>
|
|
100
|
+
storage
|
|
101
|
+
.list<string>({
|
|
102
|
+
prefix: RESOURCE_PREFIX,
|
|
103
|
+
})
|
|
104
|
+
.pipe(
|
|
105
|
+
Effect.map((entries) => {
|
|
106
|
+
const stages = new Set<string>();
|
|
107
|
+
for (const key of entries.keys()) {
|
|
108
|
+
const parsed = parseResourceKey(key);
|
|
109
|
+
if (parsed) stages.add(parsed.stage);
|
|
110
|
+
}
|
|
111
|
+
return [...stages];
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
|
|
115
|
+
/** (Stack DO only) List every resource FQN in a stage. */
|
|
116
|
+
listResources: ({ stage }: { stage: string }) =>
|
|
117
|
+
storage
|
|
118
|
+
.list<string>({
|
|
119
|
+
prefix: stagePrefix(stage),
|
|
120
|
+
})
|
|
121
|
+
.pipe(
|
|
122
|
+
Effect.map((entries) => {
|
|
123
|
+
const fqns: string[] = [];
|
|
124
|
+
for (const key of entries.keys()) {
|
|
125
|
+
const parsed = parseResourceKey(key);
|
|
126
|
+
if (parsed) fqns.push(parsed.fqn);
|
|
127
|
+
}
|
|
128
|
+
return fqns;
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* (Stack DO only) Get a resource by (stage, fqn). Returns
|
|
134
|
+
* null if missing.
|
|
135
|
+
*/
|
|
136
|
+
get: ({ stage, fqn }: { stage: string; fqn: string }) =>
|
|
137
|
+
storage
|
|
138
|
+
.get<string>(resourceKey(stage, fqn))
|
|
139
|
+
.pipe(
|
|
140
|
+
Effect.flatMap((entry) =>
|
|
141
|
+
entry == null ? Effect.succeed(undefined) : decryptEntry(entry),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* (Stack DO only) Persist a resource. Returns the stored
|
|
147
|
+
* value unchanged.
|
|
148
|
+
*/
|
|
149
|
+
set: ({
|
|
150
|
+
stage,
|
|
151
|
+
fqn,
|
|
152
|
+
value,
|
|
153
|
+
}: {
|
|
154
|
+
stage: string;
|
|
155
|
+
fqn: string;
|
|
156
|
+
value: ResourceState;
|
|
157
|
+
}) =>
|
|
158
|
+
encryptValue(value).pipe(
|
|
159
|
+
Effect.flatMap((encrypted) =>
|
|
160
|
+
storage
|
|
161
|
+
.put<string>(resourceKey(stage, fqn), encrypted)
|
|
162
|
+
.pipe(Effect.asVoid),
|
|
163
|
+
),
|
|
164
|
+
Effect.map(() => value),
|
|
165
|
+
),
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* (Stack DO only) Delete a resource. Idempotent.
|
|
169
|
+
*
|
|
170
|
+
* Exposed as `remove` (not `delete`) because Cloudflare's
|
|
171
|
+
* Durable Object RPC stub reserves `delete` and refuses to
|
|
172
|
+
* proxy the call, surfacing as "RPC receiver does not
|
|
173
|
+
* implement the method 'delete'".
|
|
174
|
+
*/
|
|
175
|
+
remove: ({ stage, fqn }: { stage: string; fqn: string }) =>
|
|
176
|
+
storage.delete(resourceKey(stage, fqn)),
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* (Stack DO only) Delete every resource in this stack, or every
|
|
180
|
+
* resource in a single stage when specified.
|
|
181
|
+
*/
|
|
182
|
+
deleteStack: ({ stage }: { stage?: string } = {}) =>
|
|
183
|
+
stage === undefined
|
|
184
|
+
? storage.deleteAll()
|
|
185
|
+
: storage
|
|
186
|
+
.list<string>({ prefix: stagePrefix(stage) })
|
|
187
|
+
.pipe(
|
|
188
|
+
Effect.flatMap((entries) =>
|
|
189
|
+
entries.size === 0
|
|
190
|
+
? Effect.void
|
|
191
|
+
: storage.delete([...entries.keys()]).pipe(Effect.asVoid),
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* (Stack DO only) Return every resource in a stage whose
|
|
197
|
+
* `status === "replaced"`. Each entry is decrypted so the
|
|
198
|
+
* `status` field can be inspected.
|
|
199
|
+
*/
|
|
200
|
+
getReplacedResources: ({ stage }: { stage: string }) =>
|
|
201
|
+
pipe(
|
|
202
|
+
storage.list<string>({ prefix: stagePrefix(stage) }),
|
|
203
|
+
Effect.map((entries) =>
|
|
204
|
+
[...entries.values()].filter((e): e is string => !!e),
|
|
205
|
+
),
|
|
206
|
+
Effect.flatMap(
|
|
207
|
+
Effect.forEach(decryptEntry, { concurrency: "unbounded" }),
|
|
208
|
+
),
|
|
209
|
+
Effect.map((decoded) =>
|
|
210
|
+
decoded.filter(
|
|
211
|
+
(d): d is ReplacedResourceState => d?.status === "replaced",
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
),
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}),
|
|
218
|
+
) {
|
|
219
|
+
/**
|
|
220
|
+
* Well-known DO name whose sole job is to track the set of stacks
|
|
221
|
+
* that have ever had resources written. `listStacks` queries it;
|
|
222
|
+
* every `set` asks it to register the stack (idempotent).
|
|
223
|
+
*/
|
|
224
|
+
static readonly ROOT_DO_NAME = "__root__" as const;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** NUL byte separator for composite keys. */
|
|
228
|
+
const SEP = "\x00";
|
|
229
|
+
|
|
230
|
+
/** Key prefix for resource entries in a stack DO. */
|
|
231
|
+
const RESOURCE_PREFIX = `r${SEP}`;
|
|
232
|
+
|
|
233
|
+
/** Key prefix for stack-index entries in the root DO. */
|
|
234
|
+
const STACK_INDEX_PREFIX = "s:";
|
|
235
|
+
|
|
236
|
+
/** AES-CTR counter block length. */
|
|
237
|
+
const NONCE_BYTES = 16;
|
|
238
|
+
|
|
239
|
+
/** Build the resource key inside a *stack DO*. */
|
|
240
|
+
const resourceKey = (stage: string, fqn: string) =>
|
|
241
|
+
`${RESOURCE_PREFIX}${stage}${SEP}${fqn}`;
|
|
242
|
+
|
|
243
|
+
/** Prefix matching every resource key inside a specific stage. */
|
|
244
|
+
const stagePrefix = (stage: string) => `${RESOURCE_PREFIX}${stage}${SEP}`;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Parse a resource key back into its (stage, fqn) tuple. Returns
|
|
248
|
+
* undefined for keys that do not match the expected shape.
|
|
249
|
+
*/
|
|
250
|
+
const parseResourceKey = (
|
|
251
|
+
key: string,
|
|
252
|
+
): { stage: string; fqn: string } | undefined => {
|
|
253
|
+
if (!key.startsWith(RESOURCE_PREFIX)) return undefined;
|
|
254
|
+
const rest = key.slice(RESOURCE_PREFIX.length);
|
|
255
|
+
const sep = rest.indexOf(SEP);
|
|
256
|
+
if (sep < 0) return undefined;
|
|
257
|
+
return { stage: rest.slice(0, sep), fqn: rest.slice(sep + 1) };
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Allocate a `Uint8Array` over a fresh `ArrayBuffer` (not shared) so
|
|
262
|
+
* the resulting buffer satisfies Web Crypto's `BufferSource` type
|
|
263
|
+
* constraint under strict DOM typings.
|
|
264
|
+
*/
|
|
265
|
+
const allocBytes = (size: number): Uint8Array<ArrayBuffer> =>
|
|
266
|
+
new Uint8Array(new ArrayBuffer(size));
|
|
267
|
+
|
|
268
|
+
const toB64 = (bytes: Uint8Array): string => {
|
|
269
|
+
let s = "";
|
|
270
|
+
for (let i = 0; i < bytes.byteLength; i++)
|
|
271
|
+
s += String.fromCharCode(bytes[i]!);
|
|
272
|
+
return btoa(s);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const fromB64 = (s: string): Uint8Array<ArrayBuffer> => {
|
|
276
|
+
const bin = atob(s);
|
|
277
|
+
const bytes = allocBytes(bin.length);
|
|
278
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
279
|
+
return bytes;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const hexToBytes = (hex: string): Uint8Array<ArrayBuffer> => {
|
|
283
|
+
const bytes = allocBytes(hex.length / 2);
|
|
284
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
285
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
286
|
+
}
|
|
287
|
+
return bytes;
|
|
288
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Random } from "alchemy";
|
|
2
|
+
import * as Cloudflare from "alchemy/Cloudflare";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The account-wide Secrets Store that backs every secret used by the
|
|
7
|
+
* state store worker. `SecretsStore` adopts the single store that
|
|
8
|
+
* already exists on the account, or creates one if none exists.
|
|
9
|
+
*/
|
|
10
|
+
export const Store = Cloudflare.SecretsStore("StateStoreSecrets");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The randomly generated bearer token value. Generated once on create
|
|
14
|
+
* and persisted in alchemy state, so subsequent deploys keep the same
|
|
15
|
+
* value unless the resource is replaced.
|
|
16
|
+
*/
|
|
17
|
+
export const TokenValue = Random("StateStoreAuthTokenValue");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The name of the secret in the Cloudflare Secrets Store that contains the bearer token.
|
|
21
|
+
*/
|
|
22
|
+
export const AuthTokenSecretName = "AlchemyStateStoreToken" as const;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The bearer token used to authenticate every request to the state
|
|
26
|
+
* store worker. The value comes from {@link TokenValue} and lives in
|
|
27
|
+
* the account-wide Cloudflare Secrets Store so it can be bound into
|
|
28
|
+
* the worker without bundling the raw string.
|
|
29
|
+
*/
|
|
30
|
+
export const AuthToken = Effect.gen(function* () {
|
|
31
|
+
const store = yield* Store;
|
|
32
|
+
const random = yield* TokenValue;
|
|
33
|
+
return yield* Cloudflare.Secret(AuthTokenSecretName, {
|
|
34
|
+
store,
|
|
35
|
+
value: random.text,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A 32-byte (256-bit) random value, hex-encoded, that seeds the
|
|
41
|
+
* AES-CTR key used to encrypt resource state at rest. Generated once
|
|
42
|
+
* and persisted, so the ciphertext stored by the Durable Object can
|
|
43
|
+
* always be decrypted by subsequent worker boots.
|
|
44
|
+
*/
|
|
45
|
+
export const EncryptionKeyValue = Random("StateStoreEncryptionKeyValue", {
|
|
46
|
+
bytes: 32,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The encryption key secret. The raw hex-encoded bytes live inside
|
|
51
|
+
* Cloudflare's Secrets Store; the Durable Object binds to it at
|
|
52
|
+
* runtime to derive an AES-CTR `CryptoKey` via Web Crypto's
|
|
53
|
+
* `subtle.importKey`.
|
|
54
|
+
*/
|
|
55
|
+
export const EncryptionKey = Effect.gen(function* () {
|
|
56
|
+
const store = yield* Store;
|
|
57
|
+
const random = yield* EncryptionKeyValue;
|
|
58
|
+
return yield* Cloudflare.Secret("StateStoreEncryptionKey", {
|
|
59
|
+
store,
|
|
60
|
+
value: random.text,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { state } from "./State.ts";
|
|
@@ -100,6 +100,7 @@ export type DurableObjectServices =
|
|
|
100
100
|
| DurableObjectNamespace
|
|
101
101
|
| DurableObjectState
|
|
102
102
|
| WorkerServices
|
|
103
|
+
| WorkerEnvironment
|
|
103
104
|
| PlatformServices;
|
|
104
105
|
|
|
105
106
|
export interface DurableObjectNamespaceProps {
|
|
@@ -514,12 +515,6 @@ export const DurableObjectNamespace: DurableObjectNamespaceClass =
|
|
|
514
515
|
Effect.provideService(DurableObjectState, doState),
|
|
515
516
|
Effect.provideService(WorkerEnvironment, env),
|
|
516
517
|
Effect.map((methods: any) => {
|
|
517
|
-
console.log(
|
|
518
|
-
"[DurableObject] wrapping methods:",
|
|
519
|
-
Object.keys(methods),
|
|
520
|
-
"own:",
|
|
521
|
-
Object.getOwnPropertyNames(methods),
|
|
522
|
-
);
|
|
523
518
|
const wrapped: Record<string, unknown> = {};
|
|
524
519
|
for (const key of Object.getOwnPropertyNames(methods)) {
|
|
525
520
|
const value = methods[key];
|
|
@@ -14,7 +14,7 @@ import { isWorkerEvent, type WorkerServices } from "./Worker.ts";
|
|
|
14
14
|
export type HttpEffect = Http.HttpEffect<WorkerServices>;
|
|
15
15
|
|
|
16
16
|
export const workersHttpHandler = <Req = never>(
|
|
17
|
-
handler: Http.HttpEffect<Req
|
|
17
|
+
handler: Http.HttpEffect<Req> | Effect.Effect<Http.HttpEffect<Req>>,
|
|
18
18
|
) => {
|
|
19
19
|
const safeHandler = Http.safeHttpEffect(handler);
|
|
20
20
|
return (event: any) => {
|
|
@@ -48,11 +48,11 @@ import { Stack } from "../../Stack.ts";
|
|
|
48
48
|
import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
|
|
49
49
|
import { D1Database } from "../D1/D1Database.ts";
|
|
50
50
|
import { fromCloudflareFetcher } from "../Fetcher.ts";
|
|
51
|
+
import type { KVNamespace } from "../KV/KVNamespace.ts";
|
|
51
52
|
import { CloudflareLogs } from "../Logs.ts";
|
|
52
53
|
import type { Providers } from "../Providers.ts";
|
|
53
|
-
import type { R2Bucket } from "../R2/R2Bucket.ts";
|
|
54
|
-
import type { KVNamespace } from "../KV/KVNamespace.ts";
|
|
55
54
|
import type { Queue as CloudflareQueue } from "../Queue/Queue.ts";
|
|
55
|
+
import type { R2Bucket } from "../R2/R2Bucket.ts";
|
|
56
56
|
import {
|
|
57
57
|
isAssets,
|
|
58
58
|
readAssets,
|
|
@@ -173,9 +173,9 @@ export interface WorkerExecutionContext extends Serverless.FunctionContext {
|
|
|
173
173
|
export(name: string, value: any): Effect.Effect<void>;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
export type WorkerServices = Worker |
|
|
176
|
+
export type WorkerServices = Worker | Request;
|
|
177
177
|
|
|
178
|
-
export type WorkerShape = Main<WorkerServices>;
|
|
178
|
+
export type WorkerShape = Main<WorkerServices | WorkerEnvironment>;
|
|
179
179
|
|
|
180
180
|
export type WorkerBindingResource =
|
|
181
181
|
| Assets
|
|
@@ -764,8 +764,9 @@ export const Worker: Platform<
|
|
|
764
764
|
);
|
|
765
765
|
return key;
|
|
766
766
|
}),
|
|
767
|
-
serve: <Req = never>(
|
|
768
|
-
|
|
767
|
+
serve: <Req = never>(
|
|
768
|
+
handler: HttpEffect<Req> | Effect.Effect<HttpEffect<Req>>,
|
|
769
|
+
) => ctx.listen(workersHttpHandler(handler)),
|
|
769
770
|
listen: ((
|
|
770
771
|
handler:
|
|
771
772
|
| Serverless.FunctionListener
|
|
@@ -1280,7 +1281,6 @@ const stack = Layer.succeed(
|
|
|
1280
1281
|
|
|
1281
1282
|
const exportsEffect = tag.asEffect().pipe(
|
|
1282
1283
|
Effect.flatMap(func => func.ExecutionContext.exports),
|
|
1283
|
-
Effect.map(exports => exports),
|
|
1284
1284
|
Effect.provide(
|
|
1285
1285
|
layer.pipe(
|
|
1286
1286
|
Layer.provideMerge(stack),
|
package/src/Cloudflare/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from "./CloudflareEnvironment.ts";
|
|
2
2
|
export * from "./Container/index.ts";
|
|
3
3
|
export * from "./D1/index.ts";
|
|
4
|
+
export * from "./EdgeSession.ts";
|
|
4
5
|
export * from "./KV/index.ts";
|
|
5
|
-
export * from "./Queue/index.ts";
|
|
6
6
|
export * from "./Providers.ts";
|
|
7
|
+
export * from "./Queue/index.ts";
|
|
7
8
|
export * from "./R2/index.ts";
|
|
8
9
|
export * from "./SecretsStore/index.ts";
|
|
10
|
+
export * from "./StateStore/index.ts";
|
|
9
11
|
export * from "./Website/index.ts";
|
|
10
12
|
export * from "./Workers/index.ts";
|
package/src/Deploy.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { AlchemyContext } from "./AlchemyContext.ts";
|
|
3
|
+
import * as Apply from "./Apply.ts";
|
|
4
|
+
import type { Input } from "./Input.ts";
|
|
5
|
+
import * as Plan from "./Plan.ts";
|
|
6
|
+
import { evalStack, type CompiledStack, type StackEffect } from "./Stack.ts";
|
|
7
|
+
import { Stage } from "./Stage.ts";
|
|
8
|
+
|
|
9
|
+
export const deploy = <A>({
|
|
10
|
+
stack,
|
|
11
|
+
stage,
|
|
12
|
+
}: {
|
|
13
|
+
stack: StackEffect<CompiledStack<A>, Stage | AlchemyContext>;
|
|
14
|
+
stage: string;
|
|
15
|
+
}) =>
|
|
16
|
+
evalStack(
|
|
17
|
+
stack,
|
|
18
|
+
(stack) =>
|
|
19
|
+
Effect.gen(function* () {
|
|
20
|
+
const plan = yield* Plan.make(stack);
|
|
21
|
+
const output = yield* Apply.apply(plan);
|
|
22
|
+
return output as Input.Resolve<A>;
|
|
23
|
+
}),
|
|
24
|
+
{ stage },
|
|
25
|
+
);
|
package/src/Destroy.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { AlchemyContext } from "./AlchemyContext.ts";
|
|
3
|
+
import * as Apply from "./Apply.ts";
|
|
4
4
|
import * as Plan from "./Plan.ts";
|
|
5
|
-
import {
|
|
5
|
+
import type { CompiledStack, StackEffect } from "./Stack.ts";
|
|
6
|
+
import { evalStack } from "./Stack.ts";
|
|
7
|
+
import type { Stage } from "./Stage.ts";
|
|
6
8
|
|
|
7
|
-
export const destroy = (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
export const destroy = ({
|
|
10
|
+
stack,
|
|
11
|
+
stage,
|
|
12
|
+
}: {
|
|
13
|
+
stack: StackEffect<CompiledStack, Stage | AlchemyContext>;
|
|
14
|
+
stage: string;
|
|
15
|
+
}) =>
|
|
16
|
+
evalStack(
|
|
17
|
+
stack,
|
|
18
|
+
(stack) =>
|
|
19
|
+
Plan.make({
|
|
20
|
+
...stack,
|
|
21
|
+
// zero these out (destroy will treat all as orphans)
|
|
22
|
+
// TODO(sam): probably better to have Plan.destroy and Plan.update
|
|
23
|
+
resources: {},
|
|
24
|
+
bindings: {},
|
|
25
|
+
output: {},
|
|
26
|
+
}).pipe(Effect.flatMap(Apply.apply)),
|
|
27
|
+
{ stage },
|
|
16
28
|
);
|
package/src/Http.ts
CHANGED
|
@@ -25,7 +25,6 @@ export const serve = <Req = never>(
|
|
|
25
25
|
) =>
|
|
26
26
|
Effect.serviceOption(HttpServer).pipe(
|
|
27
27
|
Effect.map(Option.getOrUndefined),
|
|
28
|
-
Effect.tap((http) => Effect.logInfo("http", http)),
|
|
29
28
|
Effect.flatMap((http) => (http ? http.serve(handler) : Effect.void)),
|
|
30
29
|
);
|
|
31
30
|
|
|
@@ -45,22 +44,38 @@ export class HttpServer extends Context.Service<
|
|
|
45
44
|
}
|
|
46
45
|
>()("HttpServer") {}
|
|
47
46
|
|
|
48
|
-
export const safeHttpEffect = <Req = never>(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
export const safeHttpEffect = <Req = never>(
|
|
48
|
+
handler: HttpEffect<Req> | Effect.Effect<HttpEffect<Req>>,
|
|
49
|
+
): Effect.Effect<
|
|
50
|
+
HttpServerResponse.HttpServerResponse,
|
|
51
|
+
never,
|
|
52
|
+
Req | HttpServerRequest | Scope
|
|
53
|
+
> =>
|
|
54
|
+
Effect.catchCause(
|
|
55
|
+
handler.pipe(
|
|
56
|
+
// @ts-expect-error
|
|
57
|
+
Effect.flatMap((response) =>
|
|
58
|
+
HttpServerResponse.isHttpServerResponse(response)
|
|
59
|
+
? Effect.succeed(response)
|
|
60
|
+
: response,
|
|
61
|
+
),
|
|
62
|
+
) as any as HttpEffect<Req>,
|
|
63
|
+
(cause) => {
|
|
64
|
+
const message = Option.match(Cause.findErrorOption(cause), {
|
|
65
|
+
onNone: () => "Internal Server Error",
|
|
66
|
+
onSome: (error) => error.message ?? "Internal Server Error",
|
|
67
|
+
});
|
|
54
68
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
return Effect.map(
|
|
70
|
+
Effect.all([Effect.logError(message), Effect.logError(cause)]),
|
|
71
|
+
() =>
|
|
72
|
+
HttpServerResponse.text(message, {
|
|
73
|
+
status: 500,
|
|
74
|
+
statusText: message,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
64
79
|
|
|
65
80
|
export const resolvePort = (options: { port?: number } | undefined) =>
|
|
66
81
|
options?.port !== undefined
|
package/src/Output.ts
CHANGED
|
@@ -208,8 +208,6 @@ export const map: {
|
|
|
208
208
|
new ApplyExpr(output as Expr<A, Req>, args[0]) as any
|
|
209
209
|
: new ApplyExpr(args[0] as any, args[1])) as any;
|
|
210
210
|
|
|
211
|
-
type _ = Parameters<typeof map>;
|
|
212
|
-
|
|
213
211
|
//Output.ApplyExpr<any, any, ResourceLike, any>
|
|
214
212
|
export const isApplyExpr = <In = any, Out = any, Req = any>(
|
|
215
213
|
node: Output<Out, Req>,
|
package/src/Platform.ts
CHANGED
|
@@ -28,8 +28,14 @@ export interface PlatformProps {
|
|
|
28
28
|
isExternal?: boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export type Main<
|
|
32
|
-
fetch:
|
|
31
|
+
export type Main<InitServices = never> = void | {
|
|
32
|
+
fetch:
|
|
33
|
+
| HttpEffect<InitServices | PlatformServices>
|
|
34
|
+
| Effect.Effect<
|
|
35
|
+
HttpEffect<InitServices | PlatformServices>,
|
|
36
|
+
never,
|
|
37
|
+
InitServices | PlatformServices
|
|
38
|
+
>;
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
export type Rpc<Shape> = {
|