alchemy 2.0.0-beta.13 → 2.0.0-beta.15

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 (311) hide show
  1. package/bin/alchemy.js +9477 -9290
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +6 -2
  4. package/bin/cli.js +61 -0
  5. package/bin/commands/deploy.ts +6 -4
  6. package/bin/commands/login.ts +1 -1
  7. package/bin/commands/logs.ts +1 -1
  8. package/bin/commands/state.ts +233 -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/lsp/client.d.ts +2 -2
  53. package/lib/Agent/util/ripgrep.d.ts +2 -2
  54. package/lib/AlchemyContext.d.ts +13 -0
  55. package/lib/AlchemyContext.d.ts.map +1 -0
  56. package/lib/AlchemyContext.js +18 -0
  57. package/lib/AlchemyContext.js.map +1 -0
  58. package/lib/Auth/AuthProvider.js +1 -1
  59. package/lib/Auth/AuthProvider.js.map +1 -1
  60. package/lib/Auth/Profile.d.ts +1 -1
  61. package/lib/Auth/index.d.ts +3 -1
  62. package/lib/Auth/index.d.ts.map +1 -1
  63. package/lib/Auth/index.js +3 -0
  64. package/lib/Auth/index.js.map +1 -1
  65. package/lib/Build/Command.d.ts +1 -1
  66. package/lib/Build/Memo.d.ts +1 -1
  67. package/lib/Bundle/Docker.d.ts +3 -3
  68. package/lib/Bundle/TempRoot.d.ts +3 -3
  69. package/lib/Cli/LoggingCli.d.ts +4 -0
  70. package/lib/Cli/LoggingCli.d.ts.map +1 -0
  71. package/lib/{Test/TestCli.js → Cli/LoggingCli.js} +3 -3
  72. package/lib/Cli/LoggingCli.js.map +1 -0
  73. package/lib/Cli/index.d.ts +0 -1
  74. package/lib/Cli/index.d.ts.map +1 -1
  75. package/lib/Cli/index.js +4 -1
  76. package/lib/Cli/index.js.map +1 -1
  77. package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts +58 -0
  78. package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts.map +1 -0
  79. package/lib/Cloudflare/ApiToken/AccountApiToken.js +137 -0
  80. package/lib/Cloudflare/ApiToken/AccountApiToken.js.map +1 -0
  81. package/lib/Cloudflare/ApiToken/Common.d.ts +91 -0
  82. package/lib/Cloudflare/ApiToken/Common.d.ts.map +1 -0
  83. package/lib/Cloudflare/ApiToken/Common.js +61 -0
  84. package/lib/Cloudflare/ApiToken/Common.js.map +1 -0
  85. package/lib/Cloudflare/ApiToken/PermissionGroups.d.ts +1549 -0
  86. package/lib/Cloudflare/ApiToken/PermissionGroups.d.ts.map +1 -0
  87. package/lib/Cloudflare/ApiToken/PermissionGroups.js +1856 -0
  88. package/lib/Cloudflare/ApiToken/PermissionGroups.js.map +1 -0
  89. package/lib/Cloudflare/ApiToken/UserApiToken.d.ts +49 -0
  90. package/lib/Cloudflare/ApiToken/UserApiToken.d.ts.map +1 -0
  91. package/lib/Cloudflare/ApiToken/UserApiToken.js +113 -0
  92. package/lib/Cloudflare/ApiToken/UserApiToken.js.map +1 -0
  93. package/lib/Cloudflare/ApiToken/index.d.ts +5 -0
  94. package/lib/Cloudflare/ApiToken/index.d.ts.map +1 -0
  95. package/lib/Cloudflare/ApiToken/index.js +5 -0
  96. package/lib/Cloudflare/ApiToken/index.js.map +1 -0
  97. package/lib/Cloudflare/Auth/AuthProvider.js +2 -2
  98. package/lib/Cloudflare/Auth/AuthProvider.js.map +1 -1
  99. package/lib/Cloudflare/CloudflareEnvironment.d.ts +1 -1
  100. package/lib/Cloudflare/Container/ContainerApplication.d.ts +2 -2
  101. package/lib/Cloudflare/Container/ContainerApplication.d.ts.map +1 -1
  102. package/lib/Cloudflare/Container/ContainerApplication.js +7 -2
  103. package/lib/Cloudflare/Container/ContainerApplication.js.map +1 -1
  104. package/lib/Cloudflare/EdgeSession.d.ts +53 -0
  105. package/lib/Cloudflare/EdgeSession.d.ts.map +1 -0
  106. package/lib/Cloudflare/EdgeSession.js +78 -0
  107. package/lib/Cloudflare/EdgeSession.js.map +1 -0
  108. package/lib/Cloudflare/Providers.d.ts +1 -1
  109. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  110. package/lib/Cloudflare/Providers.js +4 -1
  111. package/lib/Cloudflare/Providers.js.map +1 -1
  112. package/lib/Cloudflare/StateStore/Api.d.ts +61 -0
  113. package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -0
  114. package/lib/Cloudflare/StateStore/Api.js +103 -0
  115. package/lib/Cloudflare/StateStore/Api.js.map +1 -0
  116. package/lib/Cloudflare/StateStore/State.d.ts +39 -0
  117. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -0
  118. package/lib/Cloudflare/StateStore/State.js +217 -0
  119. package/lib/Cloudflare/StateStore/State.js.map +1 -0
  120. package/lib/Cloudflare/StateStore/Store.d.ts +82 -0
  121. package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -0
  122. package/lib/Cloudflare/StateStore/Store.js +191 -0
  123. package/lib/Cloudflare/StateStore/Store.js.map +1 -0
  124. package/lib/Cloudflare/StateStore/Token.d.ts +43 -0
  125. package/lib/Cloudflare/StateStore/Token.d.ts.map +1 -0
  126. package/lib/Cloudflare/StateStore/Token.js +61 -0
  127. package/lib/Cloudflare/StateStore/Token.js.map +1 -0
  128. package/lib/Cloudflare/StateStore/index.d.ts +2 -0
  129. package/lib/Cloudflare/StateStore/index.d.ts.map +1 -0
  130. package/lib/Cloudflare/StateStore/index.js +2 -0
  131. package/lib/Cloudflare/StateStore/index.js.map +1 -0
  132. package/lib/Cloudflare/Workers/Assets.d.ts +2 -2
  133. package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts +2 -2
  134. package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts.map +1 -1
  135. package/lib/Cloudflare/Workers/DurableObjectNamespace.js +0 -1
  136. package/lib/Cloudflare/Workers/DurableObjectNamespace.js.map +1 -1
  137. package/lib/Cloudflare/Workers/HttpServer.d.ts +1 -1
  138. package/lib/Cloudflare/Workers/HttpServer.d.ts.map +1 -1
  139. package/lib/Cloudflare/Workers/HttpServer.js.map +1 -1
  140. package/lib/Cloudflare/Workers/Worker.d.ts +5 -5
  141. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  142. package/lib/Cloudflare/Workers/Worker.js +0 -1
  143. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  144. package/lib/Cloudflare/index.d.ts +4 -1
  145. package/lib/Cloudflare/index.d.ts.map +1 -1
  146. package/lib/Cloudflare/index.js +4 -1
  147. package/lib/Cloudflare/index.js.map +1 -1
  148. package/lib/Deploy.d.ts +10 -0
  149. package/lib/Deploy.d.ts.map +1 -0
  150. package/lib/Deploy.js +12 -0
  151. package/lib/Deploy.js.map +1 -0
  152. package/lib/Destroy.d.ts +8 -3
  153. package/lib/Destroy.d.ts.map +1 -1
  154. package/lib/Destroy.js +8 -8
  155. package/lib/Destroy.js.map +1 -1
  156. package/lib/GitHub/Providers.d.ts +3 -0
  157. package/lib/GitHub/Providers.d.ts.map +1 -0
  158. package/lib/GitHub/Providers.js +5 -0
  159. package/lib/GitHub/Providers.js.map +1 -0
  160. package/lib/GitHub/index.d.ts +1 -0
  161. package/lib/GitHub/index.d.ts.map +1 -1
  162. package/lib/GitHub/index.js +1 -0
  163. package/lib/GitHub/index.js.map +1 -1
  164. package/lib/Http.d.ts +1 -1
  165. package/lib/Http.d.ts.map +1 -1
  166. package/lib/Http.js +7 -3
  167. package/lib/Http.js.map +1 -1
  168. package/lib/Kubernetes/client.d.ts +10 -10
  169. package/lib/Output.d.ts.map +1 -1
  170. package/lib/Output.js +4 -0
  171. package/lib/Output.js.map +1 -1
  172. package/lib/Plan.d.ts.map +1 -1
  173. package/lib/Plan.js +4 -0
  174. package/lib/Plan.js.map +1 -1
  175. package/lib/Platform.d.ts +2 -2
  176. package/lib/Platform.d.ts.map +1 -1
  177. package/lib/Platform.js.map +1 -1
  178. package/lib/Provider.d.ts.map +1 -1
  179. package/lib/Provider.js +0 -1
  180. package/lib/Provider.js.map +1 -1
  181. package/lib/Stack.d.ts +24 -12
  182. package/lib/Stack.d.ts.map +1 -1
  183. package/lib/Stack.js +47 -10
  184. package/lib/Stack.js.map +1 -1
  185. package/lib/State/HttpStateApi.d.ts +96 -0
  186. package/lib/State/HttpStateApi.d.ts.map +1 -0
  187. package/lib/State/HttpStateApi.js +96 -0
  188. package/lib/State/HttpStateApi.js.map +1 -0
  189. package/lib/State/HttpStateStore.d.ts +10 -0
  190. package/lib/State/HttpStateStore.d.ts.map +1 -0
  191. package/lib/State/HttpStateStore.js +102 -0
  192. package/lib/State/HttpStateStore.js.map +1 -0
  193. package/lib/State/InMemoryState.d.ts +2 -30
  194. package/lib/State/InMemoryState.d.ts.map +1 -1
  195. package/lib/State/InMemoryState.js +23 -18
  196. package/lib/State/InMemoryState.js.map +1 -1
  197. package/lib/State/LocalState.d.ts +4 -2
  198. package/lib/State/LocalState.d.ts.map +1 -1
  199. package/lib/State/LocalState.js +9 -41
  200. package/lib/State/LocalState.js.map +1 -1
  201. package/lib/State/State.d.ts +11 -4
  202. package/lib/State/State.d.ts.map +1 -1
  203. package/lib/State/State.js +1 -1
  204. package/lib/State/State.js.map +1 -1
  205. package/lib/State/StateEncoding.d.ts +31 -0
  206. package/lib/State/StateEncoding.d.ts.map +1 -0
  207. package/lib/State/StateEncoding.js +82 -0
  208. package/lib/State/StateEncoding.js.map +1 -0
  209. package/lib/State/Sync.d.ts +23 -0
  210. package/lib/State/Sync.d.ts.map +1 -0
  211. package/lib/State/Sync.js +49 -0
  212. package/lib/State/Sync.js.map +1 -0
  213. package/lib/State/index.d.ts +4 -0
  214. package/lib/State/index.d.ts.map +1 -1
  215. package/lib/State/index.js +4 -0
  216. package/lib/State/index.js.map +1 -1
  217. package/lib/Test/Bun.d.ts +13 -17
  218. package/lib/Test/Bun.d.ts.map +1 -1
  219. package/lib/Test/Bun.js +17 -36
  220. package/lib/Test/Bun.js.map +1 -1
  221. package/lib/Test/TestState.d.ts +1 -2
  222. package/lib/Test/TestState.d.ts.map +1 -1
  223. package/lib/Test/TestState.js +5 -9
  224. package/lib/Test/TestState.js.map +1 -1
  225. package/lib/Test/Vitest.d.ts +3 -2
  226. package/lib/Test/Vitest.d.ts.map +1 -1
  227. package/lib/Test/Vitest.js +20 -11
  228. package/lib/Test/Vitest.js.map +1 -1
  229. package/lib/Test/index.d.ts +0 -1
  230. package/lib/Test/index.d.ts.map +1 -1
  231. package/lib/Test/index.js +0 -1
  232. package/lib/Test/index.js.map +1 -1
  233. package/lib/Util/FileLogger.d.ts +2 -2
  234. package/lib/Util/FileLogger.d.ts.map +1 -1
  235. package/lib/Util/FileLogger.js +2 -2
  236. package/lib/Util/FileLogger.js.map +1 -1
  237. package/lib/cli/index.d.ts +10 -5
  238. package/lib/cli/index.d.ts.map +1 -1
  239. package/lib/cli/index.js +2 -43045
  240. package/lib/cli/index.js.map +1 -1
  241. package/lib/index.d.ts +2 -1
  242. package/lib/index.d.ts.map +1 -1
  243. package/lib/index.js +2 -1
  244. package/lib/index.js.map +1 -1
  245. package/package.json +30 -15
  246. package/src/AWS/ECS/Task.ts +4 -4
  247. package/src/AWS/Lambda/Function.ts +2 -3
  248. package/src/AWS/SQS/QueueSink.ts +1 -2
  249. package/src/Agent/coding/bash.ts +2 -3
  250. package/src/Agent/coding/glob.ts +1 -2
  251. package/src/Agent/coding/grep.ts +1 -2
  252. package/src/AlchemyContext.ts +27 -0
  253. package/src/Auth/AuthProvider.ts +1 -1
  254. package/src/Auth/index.ts +3 -1
  255. package/src/{Test/TestCli.ts → Cli/LoggingCli.ts} +2 -2
  256. package/src/Cli/index.ts +4 -1
  257. package/src/Cloudflare/ApiToken/AccountApiToken.ts +197 -0
  258. package/src/Cloudflare/ApiToken/Common.ts +148 -0
  259. package/src/Cloudflare/ApiToken/PermissionGroups.ts +1910 -0
  260. package/src/Cloudflare/ApiToken/UserApiToken.ts +168 -0
  261. package/src/Cloudflare/ApiToken/index.ts +4 -0
  262. package/src/Cloudflare/Auth/AuthProvider.ts +2 -2
  263. package/src/Cloudflare/Container/ContainerApplication.ts +8 -2
  264. package/src/Cloudflare/EdgeSession.ts +144 -0
  265. package/src/Cloudflare/Providers.ts +5 -0
  266. package/src/Cloudflare/StateStore/Api.ts +159 -0
  267. package/src/Cloudflare/StateStore/State.ts +318 -0
  268. package/src/Cloudflare/StateStore/Store.ts +290 -0
  269. package/src/Cloudflare/StateStore/Token.ts +68 -0
  270. package/src/Cloudflare/StateStore/index.ts +1 -0
  271. package/src/Cloudflare/Workers/DurableObjectNamespace.ts +1 -6
  272. package/src/Cloudflare/Workers/HttpServer.ts +1 -1
  273. package/src/Cloudflare/Workers/Worker.ts +7 -7
  274. package/src/Cloudflare/index.ts +4 -1
  275. package/src/Deploy.ts +25 -0
  276. package/src/Destroy.ts +25 -13
  277. package/src/GitHub/Providers.ts +6 -0
  278. package/src/GitHub/index.ts +1 -0
  279. package/src/Http.ts +31 -16
  280. package/src/Output.ts +3 -2
  281. package/src/Plan.ts +3 -0
  282. package/src/Platform.ts +8 -2
  283. package/src/Provider.ts +0 -2
  284. package/src/Stack.ts +146 -45
  285. package/src/State/HttpStateApi.ts +155 -0
  286. package/src/State/HttpStateStore.ts +156 -0
  287. package/src/State/InMemoryState.ts +15 -17
  288. package/src/State/LocalState.ts +13 -45
  289. package/src/State/State.ts +13 -4
  290. package/src/State/StateEncoding.ts +83 -0
  291. package/src/State/Sync.ts +89 -0
  292. package/src/State/index.ts +4 -0
  293. package/src/Test/Bun.ts +31 -89
  294. package/src/Test/TestState.ts +6 -8
  295. package/src/Test/Vitest.ts +32 -19
  296. package/src/Test/index.ts +0 -1
  297. package/src/Util/FileLogger.ts +2 -2
  298. package/src/index.ts +2 -1
  299. package/lib/Config.d.ts +0 -20
  300. package/lib/Config.d.ts.map +0 -1
  301. package/lib/Config.js +0 -22
  302. package/lib/Config.js.map +0 -1
  303. package/lib/Observability.d.ts +0 -2
  304. package/lib/Observability.d.ts.map +0 -1
  305. package/lib/Observability.js +0 -1
  306. package/lib/Observability.js.map +0 -1
  307. package/lib/Test/TestCli.d.ts +0 -4
  308. package/lib/Test/TestCli.d.ts.map +0 -1
  309. package/lib/Test/TestCli.js.map +0 -1
  310. package/src/Config.ts +0 -31
  311. package/src/Observability.ts +0 -0
@@ -0,0 +1,318 @@
1
+ import * as SecretsStore from "@distilled.cloud/cloudflare/secrets-store";
2
+ import * as workers from "@distilled.cloud/cloudflare/workers";
3
+ import * as Output from "alchemy/Output";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Redacted from "effect/Redacted";
7
+ import * as HttpClient from "effect/unstable/http/HttpClient";
8
+
9
+ import * as Config from "effect/Config";
10
+ import { AuthError } from "../../Auth/AuthProvider.ts";
11
+ import { readCredentials, writeCredentials } from "../../Auth/Credentials.ts";
12
+ import { ALCHEMY_PROFILE } from "../../Auth/Profile.ts";
13
+ import * as Cloudflare from "../../Cloudflare/Providers.ts";
14
+ import { deploy } from "../../Deploy.ts";
15
+ import * as Alchemy from "../../Stack.ts";
16
+ import {
17
+ makeHttpStateStore,
18
+ type HttpStateStoreProps,
19
+ } from "../../State/HttpStateStore.ts";
20
+ import * as CloudflareEnvironment from "../CloudflareEnvironment.ts";
21
+ import * as Credentials from "../Credentials.ts";
22
+
23
+ import { AlchemyContext } from "../../AlchemyContext.ts";
24
+ import { makeLocalState } from "../../State/LocalState.ts";
25
+ import { State, type StateService } from "../../State/State.ts";
26
+ import { syncState } from "../../State/Sync.ts";
27
+ import * as Clank from "../../Util/Clank.ts";
28
+ import { CloudflareAuth } from "../Auth/AuthProvider.ts";
29
+ import { EdgeSessionError, createEdgeSession } from "../EdgeSession.ts";
30
+ import Api, { STATE_STORE_SCRIPT_NAME } from "./Api.ts";
31
+ import { AuthTokenSecretName, TokenValue } from "./Token.ts";
32
+
33
+ /** Filename used for stored credentials under the profile directory. */
34
+ const CREDENTIALS_FILE = "cloudflare-state-store";
35
+
36
+ export const state = (props?: {
37
+ /**
38
+ * The name of the script to use for the state store.
39
+ * @default "alchemy-state-store"
40
+ */
41
+ workerName?: string;
42
+ }) =>
43
+ Layer.effect(
44
+ State,
45
+ Effect.gen(function* () {
46
+ const isCI = yield* Config.boolean("CI").pipe(Config.withDefault(false));
47
+ const alchemyContext = yield* AlchemyContext;
48
+ const profileName = yield* ALCHEMY_PROFILE;
49
+ const credentials = yield* readCredentials<HttpStateStoreProps>(
50
+ profileName,
51
+ CREDENTIALS_FILE,
52
+ );
53
+
54
+ const scriptName = props?.workerName ?? STATE_STORE_SCRIPT_NAME;
55
+
56
+ // TODO(sam): support upgrading state store, right now we only deploy once
57
+ if (credentials) {
58
+ // it's in the profile, let's go
59
+ const httpState = yield* makeHttpStateStore(credentials);
60
+
61
+ if (alchemyContext.updateStateStore) {
62
+ yield* deployStateStore(scriptName, httpState);
63
+ }
64
+
65
+ return httpState;
66
+ } else {
67
+ // our profile does not contain a reference to the state store, let's try and resolve it
68
+ const { accountId } =
69
+ yield* CloudflareEnvironment.CloudflareEnvironment;
70
+ const workerExists = yield* workers
71
+ .getScriptSetting({
72
+ accountId,
73
+ scriptName,
74
+ })
75
+ .pipe(
76
+ Effect.map((setting) => setting !== undefined),
77
+ Effect.catchTag("WorkerNotFound", () => Effect.succeed(false)),
78
+ );
79
+
80
+ if (workerExists) {
81
+ // it exists, so fetch the secret token
82
+ const credentials = yield* loginWithCloudflare();
83
+ if (!isCI) {
84
+ yield* writeCredentials<HttpStateStoreProps>(
85
+ profileName,
86
+ CREDENTIALS_FILE,
87
+ credentials,
88
+ );
89
+ }
90
+ const httpState = yield* makeHttpStateStore(credentials);
91
+
92
+ if (alchemyContext.updateStateStore) {
93
+ yield* deployStateStore(scriptName, httpState);
94
+ }
95
+
96
+ return httpState;
97
+ } else if (isCI) {
98
+ // TODO(sam): do we want to support bootstrapping the state store from CI?
99
+ // for now - just die here
100
+ return yield* Effect.die(
101
+ new AuthError({
102
+ message: `State store not found for script ${scriptName}. Deploy the state store first.`,
103
+ }),
104
+ );
105
+ }
106
+
107
+ const shouldDeploy = yield* Clank.confirm({
108
+ message:
109
+ "Cloudflare State Store not found. Do you want to deploy it?",
110
+ });
111
+
112
+ if (!shouldDeploy) {
113
+ return yield* Effect.die(new Clank.PromptCancelled());
114
+ }
115
+
116
+ const { url, authToken, localState } =
117
+ yield* deployStateStore(scriptName);
118
+
119
+ const httpState = yield* makeHttpStateStore({ url, authToken });
120
+
121
+ yield* syncState(localState, httpState);
122
+
123
+ yield* writeCredentials<HttpStateStoreProps>(
124
+ profileName,
125
+ CREDENTIALS_FILE,
126
+ {
127
+ url,
128
+ authToken,
129
+ },
130
+ );
131
+
132
+ yield* localState.deleteStack({
133
+ stack: "CloudflareStateStore",
134
+ stage: scriptName,
135
+ });
136
+
137
+ return httpState;
138
+ }
139
+ }).pipe(Effect.orDie),
140
+ ).pipe(
141
+ Layer.provideMerge(Credentials.fromAuthProvider()),
142
+ Layer.provideMerge(CloudflareEnvironment.fromProfile()),
143
+ Layer.provideMerge(CloudflareAuth),
144
+ Layer.orDie,
145
+ );
146
+
147
+ const deployStateStore = (scriptName: string, state?: StateService) =>
148
+ Effect.gen(function* () {
149
+ const localState = state ?? (yield* makeLocalState());
150
+ // deploy it with local state (which we will then hoist into the Cloudflare state store)
151
+ const stateLayer = Layer.succeed(State, localState);
152
+ const { url, authToken } = yield* deploy({
153
+ // use the script name as the stage name (so the user can have multiple state stores)
154
+ stage: scriptName,
155
+ stack: Alchemy.Stack(
156
+ "CloudflareStateStore",
157
+ {
158
+ providers: Cloudflare.providers(),
159
+ state: stateLayer,
160
+ },
161
+ Effect.gen(function* () {
162
+ const token = yield* TokenValue;
163
+ const api = yield* Api;
164
+
165
+ // Surface the bearer token so tests and clients can authenticate
166
+ // after deploy. The underlying value lives in the Cloudflare
167
+ // Secrets Store; this output carries the same generated string.
168
+ return {
169
+ url: api.url.as<string>(),
170
+ authToken: token.text.pipe(Output.map(Redacted.value)),
171
+ };
172
+ }),
173
+ ),
174
+ }).pipe(
175
+ // TODO(sam): we should not need to do this, but types do complain. fix deploy
176
+ Effect.provide(stateLayer),
177
+ );
178
+ return { url, authToken, localState };
179
+ });
180
+
181
+ /**
182
+ * Log in to a Cloudflare-deployed HTTP state-store.
183
+ *
184
+ * 1. Find the single account-wide Secrets Store.
185
+ * 2. Upload a short-lived edge-preview worker that binds the
186
+ * auth-token secret and returns its value.
187
+ * 3. Derive the state-store worker URL from
188
+ * {@link STATE_STORE_SCRIPT_NAME} and the account's workers.dev
189
+ * subdomain.
190
+ * 4. Persist `{ url, token }` under the `http-state-store`
191
+ * credentials file.
192
+ *
193
+ * Requirements are covered by the Cloudflare provider stack —
194
+ * `CloudflareEnvironment`, `Credentials`, `HttpClient`, and
195
+ * `FileSystem`.
196
+ */
197
+ export const loginWithCloudflare = () =>
198
+ Effect.gen(function* () {
199
+ const isCI = yield* Config.boolean("CI").pipe(Config.withDefault(false));
200
+ const profileName = yield* ALCHEMY_PROFILE;
201
+ const { accountId } = yield* CloudflareEnvironment.CloudflareEnvironment;
202
+
203
+ // 1. Locate the single Secrets Store on the account.
204
+ const stores = yield* SecretsStore.listStores({ accountId });
205
+ const store = stores.result[0];
206
+ if (!store) {
207
+ return yield* Effect.fail(
208
+ new AuthError({
209
+ message:
210
+ "No Secrets Store found on this account. Deploy the state store first.",
211
+ }),
212
+ );
213
+ }
214
+
215
+ // 2. Fetch the auth-token value via an edge-preview worker.
216
+ const authToken = yield* readSecretViaEdge(store.id, AuthTokenSecretName);
217
+
218
+ // 3. Derive the deployed worker URL.
219
+ const { subdomain } = yield* workers.getSubdomain({ accountId });
220
+ const url = `https://${STATE_STORE_SCRIPT_NAME}.${subdomain}.workers.dev`;
221
+
222
+ if (!isCI) {
223
+ // 4. Persist credentials. The profile entry is managed by
224
+ // `loadOrConfigure` when this is invoked through `configure`.
225
+ yield* writeCredentials<HttpStateStoreProps>(
226
+ profileName,
227
+ CREDENTIALS_FILE,
228
+ {
229
+ url,
230
+ authToken: authToken.trim(),
231
+ },
232
+ ).pipe(
233
+ Effect.mapError(
234
+ (e) =>
235
+ new AuthError({ message: "Failed to write credentials", cause: e }),
236
+ ),
237
+ );
238
+
239
+ yield* Clank.success(
240
+ `HTTP state store credentials saved for '${profileName}'.`,
241
+ );
242
+ yield* Clank.info(` url: ${url}`);
243
+ }
244
+
245
+ return {
246
+ url,
247
+ authToken: authToken.trim(),
248
+ };
249
+ }).pipe(
250
+ Effect.catchTag("EdgeSessionError", (e) =>
251
+ Effect.fail(
252
+ new AuthError({
253
+ message: `Edge-preview secret read failed: ${e.message}`,
254
+ cause: e.cause,
255
+ }),
256
+ ),
257
+ ),
258
+ );
259
+
260
+ /** Preview script name used by the edge-probe worker. */
261
+ const PROBE_SCRIPT_NAME = "alchemy-state-store-probe";
262
+
263
+ /**
264
+ * Tiny ES-module worker that reads `env.SECRET.get()` and echoes it
265
+ * back. Uploaded as an ephemeral edge-preview, called once, then
266
+ * discarded — see {@link readSecretViaEdge}.
267
+ */
268
+ const SECRET_PROBE_SOURCE = `export default {
269
+ async fetch(_request, env) {
270
+ try {
271
+ const value = await env.SECRET.get();
272
+ return new Response(value ?? "", { status: 200, headers: { "content-type": "text/plain" } });
273
+ } catch (e) {
274
+ return new Response("Error: " + (e && e.message ? e.message : String(e)), { status: 500 });
275
+ }
276
+ },
277
+ };`;
278
+
279
+ /**
280
+ * Upload a tiny edge-preview worker that binds the given Secrets
281
+ * Store secret, call it once, and return the decoded value. The
282
+ * Cloudflare REST API deliberately hides secret values; only worker
283
+ * bindings can resolve them, so this is the out-of-band path.
284
+ */
285
+ const readSecretViaEdge = (storeId: string, secretName: string) =>
286
+ Effect.gen(function* () {
287
+ const http = yield* HttpClient.HttpClient;
288
+ const file = new File([SECRET_PROBE_SOURCE], "worker.js", {
289
+ type: "application/javascript+module",
290
+ });
291
+ const session = yield* createEdgeSession({
292
+ scriptName: PROBE_SCRIPT_NAME,
293
+ files: [file],
294
+ bindings: [
295
+ { type: "secrets_store_secret", name: "SECRET", secretName, storeId },
296
+ ],
297
+ });
298
+ const response = yield* http.get(session.url, {
299
+ headers: session.headers,
300
+ });
301
+ if (response.status !== 200) {
302
+ const body = yield* response.text.pipe(
303
+ Effect.catch(() => Effect.succeed("")),
304
+ );
305
+ return yield* Effect.fail(
306
+ new EdgeSessionError({
307
+ message: `Secret probe returned ${response.status}: ${body.slice(0, 200)}`,
308
+ }),
309
+ );
310
+ }
311
+ return yield* response.text;
312
+ }).pipe(
313
+ Effect.mapError((cause) =>
314
+ cause instanceof EdgeSessionError
315
+ ? cause
316
+ : new EdgeSessionError({ message: "Failed to read secret", cause }),
317
+ ),
318
+ );
@@ -0,0 +1,290 @@
1
+ import type { ReplacedResourceState, ResourceState } from "alchemy/State";
2
+ import { encodeState } from "alchemy/State";
3
+ import * as Effect from "effect/Effect";
4
+ import { pipe } from "effect/Function";
5
+ import * as Secret from "../SecretsStore/Secret.ts";
6
+ import { DurableObjectNamespace } from "../Workers/DurableObjectNamespace.ts";
7
+ import { DurableObjectState } from "../Workers/DurableObjectState.ts";
8
+ import { EncryptionKey } from "./Token.ts";
9
+
10
+ export default class Store extends DurableObjectNamespace<Store>()(
11
+ "Store",
12
+ Effect.gen(function* () {
13
+ // Outer (class-level) phase — resolve the binding factory once.
14
+ // The actual secret read happens inside each DO instance below,
15
+ // since `SecretClient.get()` needs the per-instance worker env.
16
+ const encryptionSecret = yield* Secret.Secret.bind(EncryptionKey);
17
+
18
+ return Effect.gen(function* () {
19
+ // Inner (per-instance) phase — set up storage and the AES key.
20
+ // The key is imported once per DO boot and reused thereafter.
21
+ const doState = yield* DurableObjectState;
22
+ const storage = doState.storage;
23
+
24
+ const keyHex = yield* encryptionSecret.get().pipe(Effect.orDie);
25
+ const cryptoKey = yield* Effect.tryPromise(() =>
26
+ crypto.subtle.importKey(
27
+ "raw",
28
+ hexToBytes(keyHex),
29
+ { name: "AES-CTR" },
30
+ false,
31
+ ["encrypt", "decrypt"],
32
+ ),
33
+ ).pipe(Effect.orDie);
34
+
35
+ const encryptValue = (value: ResourceState) =>
36
+ Effect.tryPromise(async () => {
37
+ const plaintext = new TextEncoder().encode(
38
+ JSON.stringify(encodeState(value)),
39
+ );
40
+ const counter = crypto.getRandomValues(allocBytes(NONCE_BYTES));
41
+ const ct = new Uint8Array(
42
+ await crypto.subtle.encrypt(
43
+ { name: "AES-CTR", counter, length: 64 },
44
+ cryptoKey,
45
+ plaintext,
46
+ ),
47
+ );
48
+ // Frame as a single base64 string: nonce || ciphertext.
49
+ const framed = allocBytes(counter.byteLength + ct.byteLength);
50
+ framed.set(counter, 0);
51
+ framed.set(ct, counter.byteLength);
52
+ return toB64(framed);
53
+ }).pipe(Effect.orDie);
54
+
55
+ const decryptEntry = (entry: string) =>
56
+ Effect.tryPromise(async () => {
57
+ const framed = fromB64(entry);
58
+ const counter = framed.slice(0, NONCE_BYTES);
59
+ const ciphertext = framed.slice(NONCE_BYTES);
60
+ const pt = await crypto.subtle.decrypt(
61
+ { name: "AES-CTR", counter, length: 64 },
62
+ cryptoKey,
63
+ ciphertext,
64
+ );
65
+ return JSON.parse(new TextDecoder().decode(pt)) as ResourceState;
66
+ }).pipe(Effect.orDie);
67
+
68
+ return {
69
+ // -- Root DO methods -----------------------------------------
70
+
71
+ /**
72
+ * (Root DO only) List every stack name ever registered.
73
+ */
74
+ listStacks: Effect.fnUntraced(function* () {
75
+ const entries = yield* storage.list<number>({
76
+ prefix: STACK_INDEX_PREFIX,
77
+ });
78
+ const stacks: string[] = [];
79
+ for (const key of entries.keys()) {
80
+ stacks.push(key.slice(STACK_INDEX_PREFIX.length));
81
+ }
82
+ return stacks;
83
+ }),
84
+
85
+ /**
86
+ * (Root DO only) Register a stack name. Idempotent — safe to
87
+ * call on every `set` to the corresponding stack DO.
88
+ */
89
+ registerStack: ({ stack }: { stack: string }) =>
90
+ storage.put(`${STACK_INDEX_PREFIX}${stack}`, 1),
91
+
92
+ /**
93
+ * (Root DO only) Remove a stack name from the global index.
94
+ */
95
+ unregisterStack: ({ stack }: { stack: string }) =>
96
+ storage.delete(`${STACK_INDEX_PREFIX}${stack}`),
97
+
98
+ // -- Stack DO methods ----------------------------------------
99
+
100
+ /** (Stack DO only) List stages with at least one resource. */
101
+ listStages: () =>
102
+ storage
103
+ .list<string>({
104
+ prefix: RESOURCE_PREFIX,
105
+ })
106
+ .pipe(
107
+ Effect.map((entries) => {
108
+ const stages = new Set<string>();
109
+ for (const key of entries.keys()) {
110
+ const parsed = parseResourceKey(key);
111
+ if (parsed) stages.add(parsed.stage);
112
+ }
113
+ return [...stages];
114
+ }),
115
+ ),
116
+
117
+ /** (Stack DO only) List every resource FQN in a stage. */
118
+ listResources: ({ stage }: { stage: string }) =>
119
+ storage
120
+ .list<string>({
121
+ prefix: stagePrefix(stage),
122
+ })
123
+ .pipe(
124
+ Effect.map((entries) => {
125
+ const fqns: string[] = [];
126
+ for (const key of entries.keys()) {
127
+ const parsed = parseResourceKey(key);
128
+ if (parsed) fqns.push(parsed.fqn);
129
+ }
130
+ return fqns;
131
+ }),
132
+ ),
133
+
134
+ /**
135
+ * (Stack DO only) Get a resource by (stage, fqn). Returns
136
+ * null if missing.
137
+ */
138
+ get: ({ stage, fqn }: { stage: string; fqn: string }) =>
139
+ storage
140
+ .get<string>(resourceKey(stage, fqn))
141
+ .pipe(
142
+ Effect.flatMap((entry) =>
143
+ entry == null ? Effect.succeed(undefined) : decryptEntry(entry),
144
+ ),
145
+ ),
146
+
147
+ /**
148
+ * (Stack DO only) Persist a resource. Returns the stored
149
+ * value unchanged.
150
+ */
151
+ set: ({
152
+ stage,
153
+ fqn,
154
+ value,
155
+ }: {
156
+ stage: string;
157
+ fqn: string;
158
+ value: ResourceState;
159
+ }) =>
160
+ encryptValue(value).pipe(
161
+ Effect.flatMap((encrypted) =>
162
+ storage
163
+ .put<string>(resourceKey(stage, fqn), encrypted)
164
+ .pipe(Effect.asVoid),
165
+ ),
166
+ Effect.map(() => value),
167
+ ),
168
+
169
+ /**
170
+ * (Stack DO only) Delete a resource. Idempotent.
171
+ *
172
+ * Exposed as `remove` (not `delete`) because Cloudflare's
173
+ * Durable Object RPC stub reserves `delete` and refuses to
174
+ * proxy the call, surfacing as "RPC receiver does not
175
+ * implement the method 'delete'".
176
+ */
177
+ remove: ({ stage, fqn }: { stage: string; fqn: string }) =>
178
+ storage.delete(resourceKey(stage, fqn)),
179
+
180
+ /**
181
+ * (Stack DO only) Delete every resource in this stack, or every
182
+ * resource in a single stage when specified.
183
+ */
184
+ deleteStack: ({ stage }: { stage?: string } = {}) =>
185
+ stage === undefined
186
+ ? storage.deleteAll()
187
+ : storage
188
+ .list<string>({ prefix: stagePrefix(stage) })
189
+ .pipe(
190
+ Effect.flatMap((entries) =>
191
+ entries.size === 0
192
+ ? Effect.void
193
+ : storage.delete([...entries.keys()]).pipe(Effect.asVoid),
194
+ ),
195
+ ),
196
+
197
+ /**
198
+ * (Stack DO only) Return every resource in a stage whose
199
+ * `status === "replaced"`. Each entry is decrypted so the
200
+ * `status` field can be inspected.
201
+ */
202
+ getReplacedResources: ({ stage }: { stage: string }) =>
203
+ pipe(
204
+ storage.list<string>({ prefix: stagePrefix(stage) }),
205
+ Effect.map((entries) =>
206
+ [...entries.values()].filter((e): e is string => !!e),
207
+ ),
208
+ Effect.flatMap(
209
+ Effect.forEach(decryptEntry, { concurrency: "unbounded" }),
210
+ ),
211
+ Effect.map((decoded) =>
212
+ decoded.filter(
213
+ (d): d is ReplacedResourceState => d?.status === "replaced",
214
+ ),
215
+ ),
216
+ ),
217
+ };
218
+ });
219
+ }),
220
+ ) {
221
+ /**
222
+ * Well-known DO name whose sole job is to track the set of stacks
223
+ * that have ever had resources written. `listStacks` queries it;
224
+ * every `set` asks it to register the stack (idempotent).
225
+ */
226
+ static readonly ROOT_DO_NAME = "__root__" as const;
227
+ }
228
+
229
+ /** NUL byte separator for composite keys. */
230
+ const SEP = "\x00";
231
+
232
+ /** Key prefix for resource entries in a stack DO. */
233
+ const RESOURCE_PREFIX = `r${SEP}`;
234
+
235
+ /** Key prefix for stack-index entries in the root DO. */
236
+ const STACK_INDEX_PREFIX = "s:";
237
+
238
+ /** AES-CTR counter block length. */
239
+ const NONCE_BYTES = 16;
240
+
241
+ /** Build the resource key inside a *stack DO*. */
242
+ const resourceKey = (stage: string, fqn: string) =>
243
+ `${RESOURCE_PREFIX}${stage}${SEP}${fqn}`;
244
+
245
+ /** Prefix matching every resource key inside a specific stage. */
246
+ const stagePrefix = (stage: string) => `${RESOURCE_PREFIX}${stage}${SEP}`;
247
+
248
+ /**
249
+ * Parse a resource key back into its (stage, fqn) tuple. Returns
250
+ * undefined for keys that do not match the expected shape.
251
+ */
252
+ const parseResourceKey = (
253
+ key: string,
254
+ ): { stage: string; fqn: string } | undefined => {
255
+ if (!key.startsWith(RESOURCE_PREFIX)) return undefined;
256
+ const rest = key.slice(RESOURCE_PREFIX.length);
257
+ const sep = rest.indexOf(SEP);
258
+ if (sep < 0) return undefined;
259
+ return { stage: rest.slice(0, sep), fqn: rest.slice(sep + 1) };
260
+ };
261
+
262
+ /**
263
+ * Allocate a `Uint8Array` over a fresh `ArrayBuffer` (not shared) so
264
+ * the resulting buffer satisfies Web Crypto's `BufferSource` type
265
+ * constraint under strict DOM typings.
266
+ */
267
+ const allocBytes = (size: number): Uint8Array<ArrayBuffer> =>
268
+ new Uint8Array(new ArrayBuffer(size));
269
+
270
+ const toB64 = (bytes: Uint8Array): string => {
271
+ let s = "";
272
+ for (let i = 0; i < bytes.byteLength; i++)
273
+ s += String.fromCharCode(bytes[i]!);
274
+ return btoa(s);
275
+ };
276
+
277
+ const fromB64 = (s: string): Uint8Array<ArrayBuffer> => {
278
+ const bin = atob(s);
279
+ const bytes = allocBytes(bin.length);
280
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
281
+ return bytes;
282
+ };
283
+
284
+ const hexToBytes = (hex: string): Uint8Array<ArrayBuffer> => {
285
+ const bytes = allocBytes(hex.length / 2);
286
+ for (let i = 0; i < bytes.length; i++) {
287
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
288
+ }
289
+ return bytes;
290
+ };
@@ -0,0 +1,68 @@
1
+ import { Random } from "alchemy";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Secret from "../SecretsStore/Secret.ts";
4
+ import { SecretsStore } from "../SecretsStore/SecretsStore.ts";
5
+
6
+ /**
7
+ * The account-wide Secrets Store that backs every secret used by the
8
+ * state store worker. `SecretsStore` adopts the single store that
9
+ * already exists on the account, or creates one if none exists.
10
+ */
11
+ export const Store = SecretsStore("StateStoreSecrets");
12
+
13
+ /**
14
+ * The randomly generated bearer token value. Generated once on create
15
+ * and persisted in alchemy state, so subsequent deploys keep the same
16
+ * value unless the resource is replaced.
17
+ */
18
+ export const TokenValue = Random("StateStoreAuthTokenValue");
19
+
20
+ /**
21
+ * The name of the secret in the Cloudflare Secrets Store that contains the bearer token.
22
+ */
23
+ export const AuthTokenSecretName = "AlchemyStateStoreToken" as const;
24
+
25
+ /**
26
+ * The bearer token used to authenticate every request to the state
27
+ * store worker. The value comes from {@link TokenValue} and lives in
28
+ * the account-wide Cloudflare Secrets Store so it can be bound into
29
+ * the worker without bundling the raw string.
30
+ */
31
+ export const AuthToken = Effect.gen(function* () {
32
+ const store = yield* Store;
33
+ const random = yield* TokenValue;
34
+ return yield* Secret.Secret(AuthTokenSecretName, {
35
+ name: AuthTokenSecretName,
36
+ store,
37
+ value: random.text,
38
+ });
39
+ });
40
+
41
+ /**
42
+ * A 32-byte (256-bit) random value, hex-encoded, that seeds the
43
+ * AES-CTR key used to encrypt resource state at rest. Generated once
44
+ * and persisted, so the ciphertext stored by the Durable Object can
45
+ * always be decrypted by subsequent worker boots.
46
+ */
47
+ export const EncryptionKeyValue = Random("StateStoreEncryptionKeyValue", {
48
+ bytes: 32,
49
+ });
50
+
51
+ export const EncryptionKeySecretName =
52
+ "AlchemyStateStoreEncryptionKey" as const;
53
+
54
+ /**
55
+ * The encryption key secret. The raw hex-encoded bytes live inside
56
+ * Cloudflare's Secrets Store; the Durable Object binds to it at
57
+ * runtime to derive an AES-CTR `CryptoKey` via Web Crypto's
58
+ * `subtle.importKey`.
59
+ */
60
+ export const EncryptionKey = Effect.gen(function* () {
61
+ const store = yield* Store;
62
+ const random = yield* EncryptionKeyValue;
63
+ return yield* Secret.Secret("StateStoreEncryptionKey", {
64
+ name: EncryptionKeySecretName,
65
+ store,
66
+ value: random.text,
67
+ });
68
+ });
@@ -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];