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.
Files changed (264) hide show
  1. package/bin/alchemy.js +8386 -8205
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +4 -2
  4. package/bin/cli.js +61 -0
  5. package/bin/commands/deploy.ts +6 -3
  6. package/bin/commands/login.ts +1 -1
  7. package/bin/commands/logs.ts +1 -1
  8. package/bin/commands/state.ts +230 -0
  9. package/bin/commands/tail.ts +1 -1
  10. package/lib/AWS/AutoScaling/LaunchTemplate.d.ts +1 -1
  11. package/lib/AWS/EC2/Instance.d.ts +1 -1
  12. package/lib/AWS/EC2/hosted.d.ts +2 -2
  13. package/lib/AWS/ECS/Task.d.ts +4 -4
  14. package/lib/AWS/ECS/Task.d.ts.map +1 -1
  15. package/lib/AWS/ECS/Task.js +2 -2
  16. package/lib/AWS/ECS/Task.js.map +1 -1
  17. package/lib/AWS/Environment.d.ts +2 -2
  18. package/lib/AWS/Lambda/BucketEventSource.d.ts +1 -1
  19. package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
  20. package/lib/AWS/Lambda/EventBridgeEventSource.d.ts +1 -1
  21. package/lib/AWS/Lambda/EventBridgeEventSource.d.ts.map +1 -1
  22. package/lib/AWS/Lambda/Function.d.ts +2 -2
  23. package/lib/AWS/Lambda/Function.d.ts.map +1 -1
  24. package/lib/AWS/Lambda/Function.js +1 -2
  25. package/lib/AWS/Lambda/Function.js.map +1 -1
  26. package/lib/AWS/Lambda/QueueEventSource.d.ts +1 -1
  27. package/lib/AWS/Lambda/QueueEventSource.d.ts.map +1 -1
  28. package/lib/AWS/Lambda/StreamEventSource.d.ts +1 -1
  29. package/lib/AWS/Lambda/StreamEventSource.d.ts.map +1 -1
  30. package/lib/AWS/Lambda/TableEventSource.d.ts +1 -1
  31. package/lib/AWS/Lambda/TableEventSource.d.ts.map +1 -1
  32. package/lib/AWS/Lambda/TopicEventSource.d.ts +1 -1
  33. package/lib/AWS/Lambda/TopicEventSource.d.ts.map +1 -1
  34. package/lib/AWS/SQS/QueueSink.d.ts.map +1 -1
  35. package/lib/AWS/SQS/QueueSink.js +1 -2
  36. package/lib/AWS/SQS/QueueSink.js.map +1 -1
  37. package/lib/Agent/coding/bash.d.ts +2 -6
  38. package/lib/Agent/coding/bash.d.ts.map +1 -1
  39. package/lib/Agent/coding/bash.js +2 -3
  40. package/lib/Agent/coding/bash.js.map +1 -1
  41. package/lib/Agent/coding/edit.d.ts +1 -1
  42. package/lib/Agent/coding/glob.d.ts +2 -4
  43. package/lib/Agent/coding/glob.d.ts.map +1 -1
  44. package/lib/Agent/coding/glob.js +1 -2
  45. package/lib/Agent/coding/glob.js.map +1 -1
  46. package/lib/Agent/coding/grep.d.ts +2 -4
  47. package/lib/Agent/coding/grep.d.ts.map +1 -1
  48. package/lib/Agent/coding/grep.js +1 -2
  49. package/lib/Agent/coding/grep.js.map +1 -1
  50. package/lib/Agent/coding/read.d.ts +1 -1
  51. package/lib/Agent/coding/write.d.ts +1 -1
  52. package/lib/Agent/util/ripgrep.d.ts +2 -2
  53. package/lib/AlchemyContext.d.ts +13 -0
  54. package/lib/AlchemyContext.d.ts.map +1 -0
  55. package/lib/AlchemyContext.js +18 -0
  56. package/lib/AlchemyContext.js.map +1 -0
  57. package/lib/Auth/AuthProvider.js +1 -1
  58. package/lib/Auth/AuthProvider.js.map +1 -1
  59. package/lib/Auth/Profile.d.ts +1 -1
  60. package/lib/Auth/index.d.ts +3 -1
  61. package/lib/Auth/index.d.ts.map +1 -1
  62. package/lib/Auth/index.js +3 -0
  63. package/lib/Auth/index.js.map +1 -1
  64. package/lib/Build/Command.d.ts +1 -1
  65. package/lib/Build/Memo.d.ts +1 -1
  66. package/lib/Bundle/Docker.d.ts +3 -3
  67. package/lib/Bundle/TempRoot.d.ts +3 -3
  68. package/lib/Cli/InkCLI.d.ts +1 -0
  69. package/lib/Cli/InkCLI.d.ts.map +1 -1
  70. package/lib/Cli/InkCLI.js +5 -0
  71. package/lib/Cli/InkCLI.js.map +1 -1
  72. package/lib/Cli/LoggingCli.d.ts +4 -0
  73. package/lib/Cli/LoggingCli.d.ts.map +1 -0
  74. package/lib/{Test/TestCli.js → Cli/LoggingCli.js} +3 -3
  75. package/lib/Cli/LoggingCli.js.map +1 -0
  76. package/lib/Cloudflare/CloudflareEnvironment.d.ts +1 -1
  77. package/lib/Cloudflare/Container/ContainerApplication.d.ts +2 -2
  78. package/lib/Cloudflare/Container/ContainerApplication.d.ts.map +1 -1
  79. package/lib/Cloudflare/Container/ContainerApplication.js +7 -2
  80. package/lib/Cloudflare/Container/ContainerApplication.js.map +1 -1
  81. package/lib/Cloudflare/EdgeSession.d.ts +53 -0
  82. package/lib/Cloudflare/EdgeSession.d.ts.map +1 -0
  83. package/lib/Cloudflare/EdgeSession.js +78 -0
  84. package/lib/Cloudflare/EdgeSession.js.map +1 -0
  85. package/lib/Cloudflare/Providers.d.ts +1 -1
  86. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  87. package/lib/Cloudflare/StateStore/Api.d.ts +62 -0
  88. package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -0
  89. package/lib/Cloudflare/StateStore/Api.js +101 -0
  90. package/lib/Cloudflare/StateStore/Api.js.map +1 -0
  91. package/lib/Cloudflare/StateStore/State.d.ts +39 -0
  92. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -0
  93. package/lib/Cloudflare/StateStore/State.js +217 -0
  94. package/lib/Cloudflare/StateStore/State.js.map +1 -0
  95. package/lib/Cloudflare/StateStore/Store.d.ts +82 -0
  96. package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -0
  97. package/lib/Cloudflare/StateStore/Store.js +189 -0
  98. package/lib/Cloudflare/StateStore/Store.js.map +1 -0
  99. package/lib/Cloudflare/StateStore/Token.d.ts +41 -0
  100. package/lib/Cloudflare/StateStore/Token.d.ts.map +1 -0
  101. package/lib/Cloudflare/StateStore/Token.js +57 -0
  102. package/lib/Cloudflare/StateStore/Token.js.map +1 -0
  103. package/lib/Cloudflare/StateStore/index.d.ts +2 -0
  104. package/lib/Cloudflare/StateStore/index.d.ts.map +1 -0
  105. package/lib/Cloudflare/StateStore/index.js +2 -0
  106. package/lib/Cloudflare/StateStore/index.js.map +1 -0
  107. package/lib/Cloudflare/Workers/Assets.d.ts +2 -2
  108. package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts +2 -2
  109. package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts.map +1 -1
  110. package/lib/Cloudflare/Workers/DurableObjectNamespace.js +0 -1
  111. package/lib/Cloudflare/Workers/DurableObjectNamespace.js.map +1 -1
  112. package/lib/Cloudflare/Workers/HttpServer.d.ts +1 -1
  113. package/lib/Cloudflare/Workers/HttpServer.d.ts.map +1 -1
  114. package/lib/Cloudflare/Workers/HttpServer.js.map +1 -1
  115. package/lib/Cloudflare/Workers/Worker.d.ts +5 -5
  116. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  117. package/lib/Cloudflare/Workers/Worker.js +0 -1
  118. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  119. package/lib/Cloudflare/index.d.ts +3 -1
  120. package/lib/Cloudflare/index.d.ts.map +1 -1
  121. package/lib/Cloudflare/index.js +3 -1
  122. package/lib/Cloudflare/index.js.map +1 -1
  123. package/lib/Deploy.d.ts +10 -0
  124. package/lib/Deploy.d.ts.map +1 -0
  125. package/lib/Deploy.js +12 -0
  126. package/lib/Deploy.js.map +1 -0
  127. package/lib/Destroy.d.ts +8 -3
  128. package/lib/Destroy.d.ts.map +1 -1
  129. package/lib/Destroy.js +8 -8
  130. package/lib/Destroy.js.map +1 -1
  131. package/lib/Http.d.ts +1 -1
  132. package/lib/Http.d.ts.map +1 -1
  133. package/lib/Http.js +7 -3
  134. package/lib/Http.js.map +1 -1
  135. package/lib/Kubernetes/client.d.ts +10 -10
  136. package/lib/Output.d.ts.map +1 -1
  137. package/lib/Output.js.map +1 -1
  138. package/lib/Platform.d.ts +2 -2
  139. package/lib/Platform.d.ts.map +1 -1
  140. package/lib/Platform.js.map +1 -1
  141. package/lib/Provider.d.ts.map +1 -1
  142. package/lib/Provider.js +0 -1
  143. package/lib/Provider.js.map +1 -1
  144. package/lib/Stack.d.ts +23 -12
  145. package/lib/Stack.d.ts.map +1 -1
  146. package/lib/Stack.js +32 -14
  147. package/lib/Stack.js.map +1 -1
  148. package/lib/State/HttpStateApi.d.ts +96 -0
  149. package/lib/State/HttpStateApi.d.ts.map +1 -0
  150. package/lib/State/HttpStateApi.js +96 -0
  151. package/lib/State/HttpStateApi.js.map +1 -0
  152. package/lib/State/HttpStateStore.d.ts +10 -0
  153. package/lib/State/HttpStateStore.d.ts.map +1 -0
  154. package/lib/State/HttpStateStore.js +102 -0
  155. package/lib/State/HttpStateStore.js.map +1 -0
  156. package/lib/State/InMemoryState.d.ts +2 -30
  157. package/lib/State/InMemoryState.d.ts.map +1 -1
  158. package/lib/State/InMemoryState.js +23 -18
  159. package/lib/State/InMemoryState.js.map +1 -1
  160. package/lib/State/LocalState.d.ts +4 -2
  161. package/lib/State/LocalState.d.ts.map +1 -1
  162. package/lib/State/LocalState.js +9 -41
  163. package/lib/State/LocalState.js.map +1 -1
  164. package/lib/State/State.d.ts +11 -4
  165. package/lib/State/State.d.ts.map +1 -1
  166. package/lib/State/State.js +1 -1
  167. package/lib/State/State.js.map +1 -1
  168. package/lib/State/StateEncoding.d.ts +31 -0
  169. package/lib/State/StateEncoding.d.ts.map +1 -0
  170. package/lib/State/StateEncoding.js +82 -0
  171. package/lib/State/StateEncoding.js.map +1 -0
  172. package/lib/State/Sync.d.ts +23 -0
  173. package/lib/State/Sync.d.ts.map +1 -0
  174. package/lib/State/Sync.js +49 -0
  175. package/lib/State/Sync.js.map +1 -0
  176. package/lib/State/index.d.ts +4 -0
  177. package/lib/State/index.d.ts.map +1 -1
  178. package/lib/State/index.js +4 -0
  179. package/lib/State/index.js.map +1 -1
  180. package/lib/Test/Bun.d.ts +13 -17
  181. package/lib/Test/Bun.d.ts.map +1 -1
  182. package/lib/Test/Bun.js +16 -33
  183. package/lib/Test/Bun.js.map +1 -1
  184. package/lib/Test/TestState.d.ts +1 -2
  185. package/lib/Test/TestState.d.ts.map +1 -1
  186. package/lib/Test/TestState.js +5 -9
  187. package/lib/Test/TestState.js.map +1 -1
  188. package/lib/Test/Vitest.d.ts +3 -2
  189. package/lib/Test/Vitest.d.ts.map +1 -1
  190. package/lib/Test/Vitest.js +20 -15
  191. package/lib/Test/Vitest.js.map +1 -1
  192. package/lib/Test/index.d.ts +0 -1
  193. package/lib/Test/index.d.ts.map +1 -1
  194. package/lib/Test/index.js +0 -1
  195. package/lib/Test/index.js.map +1 -1
  196. package/lib/Util/FileLogger.d.ts +2 -2
  197. package/lib/Util/FileLogger.d.ts.map +1 -1
  198. package/lib/Util/FileLogger.js +2 -2
  199. package/lib/Util/FileLogger.js.map +1 -1
  200. package/lib/cli/index.d.ts +10 -1
  201. package/lib/cli/index.d.ts.map +1 -1
  202. package/lib/cli/index.js +9 -4
  203. package/lib/cli/index.js.map +1 -1
  204. package/lib/index.d.ts +2 -1
  205. package/lib/index.d.ts.map +1 -1
  206. package/lib/index.js +2 -1
  207. package/lib/index.js.map +1 -1
  208. package/package.json +26 -11
  209. package/src/AWS/ECS/Task.ts +4 -4
  210. package/src/AWS/Lambda/Function.ts +2 -3
  211. package/src/AWS/SQS/QueueSink.ts +1 -2
  212. package/src/Agent/coding/bash.ts +2 -3
  213. package/src/Agent/coding/glob.ts +1 -2
  214. package/src/Agent/coding/grep.ts +1 -2
  215. package/src/AlchemyContext.ts +27 -0
  216. package/src/Auth/AuthProvider.ts +1 -1
  217. package/src/Auth/index.ts +3 -1
  218. package/src/Cli/InkCLI.tsx +10 -0
  219. package/src/{Test/TestCli.ts → Cli/LoggingCli.ts} +2 -2
  220. package/src/Cloudflare/Container/ContainerApplication.ts +8 -2
  221. package/src/Cloudflare/EdgeSession.ts +144 -0
  222. package/src/Cloudflare/StateStore/Api.ts +157 -0
  223. package/src/Cloudflare/StateStore/State.ts +318 -0
  224. package/src/Cloudflare/StateStore/Store.ts +288 -0
  225. package/src/Cloudflare/StateStore/Token.ts +62 -0
  226. package/src/Cloudflare/StateStore/index.ts +1 -0
  227. package/src/Cloudflare/Workers/DurableObjectNamespace.ts +1 -6
  228. package/src/Cloudflare/Workers/HttpServer.ts +1 -1
  229. package/src/Cloudflare/Workers/Worker.ts +7 -7
  230. package/src/Cloudflare/index.ts +3 -1
  231. package/src/Deploy.ts +25 -0
  232. package/src/Destroy.ts +25 -13
  233. package/src/Http.ts +31 -16
  234. package/src/Output.ts +0 -2
  235. package/src/Platform.ts +8 -2
  236. package/src/Provider.ts +0 -2
  237. package/src/Stack.ts +122 -47
  238. package/src/State/HttpStateApi.ts +155 -0
  239. package/src/State/HttpStateStore.ts +155 -0
  240. package/src/State/InMemoryState.ts +15 -17
  241. package/src/State/LocalState.ts +13 -45
  242. package/src/State/State.ts +13 -4
  243. package/src/State/StateEncoding.ts +83 -0
  244. package/src/State/Sync.ts +89 -0
  245. package/src/State/index.ts +4 -0
  246. package/src/Test/Bun.ts +29 -87
  247. package/src/Test/TestState.ts +6 -8
  248. package/src/Test/Vitest.ts +32 -23
  249. package/src/Test/index.ts +0 -1
  250. package/src/Util/FileLogger.ts +2 -2
  251. package/src/index.ts +2 -1
  252. package/lib/Config.d.ts +0 -20
  253. package/lib/Config.d.ts.map +0 -1
  254. package/lib/Config.js +0 -22
  255. package/lib/Config.js.map +0 -1
  256. package/lib/Observability.d.ts +0 -2
  257. package/lib/Observability.d.ts.map +0 -1
  258. package/lib/Observability.js +0 -1
  259. package/lib/Observability.js.map +0 -1
  260. package/lib/Test/TestCli.d.ts +0 -4
  261. package/lib/Test/TestCli.d.ts.map +0 -1
  262. package/lib/Test/TestCli.js.map +0 -1
  263. package/src/Config.ts +0 -31
  264. 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 | WorkerEnvironment | Request;
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>(handler: HttpEffect<Req>) =>
768
- ctx.listen(workersHttpHandler(handler)),
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),
@@ -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 * as Effect from "effect/Effect";
2
- import { apply } from "./Apply.ts";
3
- import { provideFreshArtifactStore } from "./Artifacts.ts";
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 { Stack } from "./Stack.ts";
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
- Stack.use((stack) =>
9
- Plan.make({
10
- name: stack.name,
11
- stage: stack.stage,
12
- resources: {},
13
- bindings: {},
14
- output: {},
15
- }).pipe(Effect.flatMap(apply), provideFreshArtifactStore),
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>(handler: HttpEffect<Req>) =>
49
- Effect.catchCause(handler, (cause) => {
50
- const message = Option.match(Cause.findErrorOption(cause), {
51
- onNone: () => "Internal Server Error",
52
- onSome: (error) => error.message ?? "Internal Server Error",
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
- return Effect.map(
56
- Effect.all([Effect.logInfo(message), Effect.logInfo(cause)]),
57
- () =>
58
- HttpServerResponse.text(message, {
59
- status: 500,
60
- statusText: message,
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<Services = never> = void | {
32
- fetch: HttpEffect<Services | PlatformServices>;
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> = {
package/src/Provider.ts CHANGED
@@ -381,7 +381,5 @@ export const tryFindProviderByType: {
381
381
  }
382
382
  }
383
383
  }
384
-
385
- console.log(context.mapUnsafe.keys());
386
384
  return Option.none();
387
385
  }) as any;