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,168 @@
1
+ import * as user from "@distilled.cloud/cloudflare/user";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Redacted from "effect/Redacted";
4
+ import { isResolved } from "../../Diff.ts";
5
+ import { createPhysicalName } from "../../PhysicalName.ts";
6
+ import * as Provider from "../../Provider.ts";
7
+ import { Resource } from "../../Resource.ts";
8
+ import type { Providers } from "../Providers.ts";
9
+ import {
10
+ buildConditionPayload,
11
+ conditionFingerprint,
12
+ policyFingerprint,
13
+ resolvePolicies,
14
+ type BaseApiTokenProps,
15
+ } from "./Common.ts";
16
+
17
+ export type UserApiTokenProps = BaseApiTokenProps;
18
+
19
+ export type UserApiToken = Resource<
20
+ "Cloudflare.UserApiToken",
21
+ UserApiTokenProps,
22
+ {
23
+ tokenId: string;
24
+ name: string;
25
+ status: "active" | "disabled" | "expired";
26
+ /**
27
+ * The plaintext token value. Cloudflare returns this only once, on
28
+ * creation, so we persist it here for downstream consumers.
29
+ */
30
+ value: Redacted.Redacted<string>;
31
+ },
32
+ never,
33
+ Providers
34
+ >;
35
+
36
+ /**
37
+ * A Cloudflare user-owned API token (`POST /user/tokens`).
38
+ *
39
+ * User-owned tokens are tied to the authenticated user's identity. They can
40
+ * be created by any authenticated user (including OAuth-derived sessions
41
+ * from `alchemy login`) without needing the account-level
42
+ * `API Tokens > Write` permission, but they are also revoked if the user
43
+ * leaves the account.
44
+ *
45
+ * For CI tokens, prefer {@link AccountApiToken} so the token survives
46
+ * personnel changes.
47
+ *
48
+ * Policy `resources` are passed through verbatim — no `accountId` rewriting
49
+ * is performed because user tokens aren't bound to a single account.
50
+ *
51
+ * @section Creating a Token
52
+ * @example A token bound to the authenticated user
53
+ * ```typescript
54
+ * const token = yield* Cloudflare.UserApiToken("personal-token", {
55
+ * name: "my-personal-token",
56
+ * policies: [
57
+ * {
58
+ * effect: "allow",
59
+ * permissionGroups: ["Workers Scripts Read"],
60
+ * resources: { [`com.cloudflare.api.account.${accountId}`]: "*" },
61
+ * },
62
+ * ],
63
+ * });
64
+ * ```
65
+ */
66
+ export const UserApiToken = Resource<UserApiToken>("Cloudflare.UserApiToken");
67
+
68
+ type UserApiTokenAttributes = UserApiToken["Attributes"];
69
+
70
+ const resolveName = (id: string, name: string | undefined) =>
71
+ Effect.gen(function* () {
72
+ return name ?? (yield* createPhysicalName({ id }));
73
+ });
74
+
75
+ export const UserApiTokenProvider = () =>
76
+ Provider.effect(
77
+ UserApiToken,
78
+ Effect.gen(function* () {
79
+ const createToken = yield* user.createToken;
80
+ const updateToken = yield* user.updateToken;
81
+ const deleteToken = yield* user.deleteToken;
82
+ const getToken = yield* user.getToken;
83
+
84
+ const buildAttributes = (
85
+ tokenData: {
86
+ id?: string | null;
87
+ name?: string | null;
88
+ status?: "active" | "disabled" | "expired" | null;
89
+ },
90
+ value: Redacted.Redacted<string>,
91
+ ): UserApiTokenAttributes => ({
92
+ tokenId: tokenData.id ?? "",
93
+ name: tokenData.name ?? "",
94
+ status: tokenData.status ?? "active",
95
+ value,
96
+ });
97
+
98
+ return {
99
+ stables: ["tokenId"],
100
+ diff: Effect.fn(function* ({ id, olds, news, output }) {
101
+ if (!isResolved(news)) return undefined;
102
+ const oldName = output?.name ?? (yield* resolveName(id, olds?.name));
103
+ const newName = yield* resolveName(id, news.name);
104
+ const oldPolicyFp = policyFingerprint(
105
+ resolvePolicies(olds?.policies ?? [], undefined),
106
+ );
107
+ const newPolicyFp = policyFingerprint(
108
+ resolvePolicies(news.policies, undefined),
109
+ );
110
+ const oldCondFp = conditionFingerprint(olds?.condition);
111
+ const newCondFp = conditionFingerprint(news.condition);
112
+ if (
113
+ oldName !== newName ||
114
+ oldPolicyFp !== newPolicyFp ||
115
+ oldCondFp !== newCondFp ||
116
+ (olds?.expiresOn ?? undefined) !== (news.expiresOn ?? undefined) ||
117
+ (olds?.notBefore ?? undefined) !== (news.notBefore ?? undefined)
118
+ ) {
119
+ return { action: "update" } as const;
120
+ }
121
+ }),
122
+ create: Effect.fn(function* ({ id, news }) {
123
+ const name = yield* resolveName(id, news.name);
124
+ const policies = resolvePolicies(news.policies, undefined);
125
+ const result = yield* createToken({
126
+ name,
127
+ policies,
128
+ condition: buildConditionPayload(news.condition),
129
+ expiresOn: news.expiresOn,
130
+ notBefore: news.notBefore,
131
+ });
132
+ if (!result.value) {
133
+ return yield* Effect.die(
134
+ `Cloudflare did not return a value for token "${name}".`,
135
+ );
136
+ }
137
+ return buildAttributes(result, Redacted.make(result.value));
138
+ }),
139
+ update: Effect.fn(function* ({ id, news, output }) {
140
+ const name = yield* resolveName(id, news.name);
141
+ const policies = resolvePolicies(news.policies, undefined);
142
+ const result = yield* updateToken({
143
+ tokenId: output.tokenId,
144
+ name,
145
+ policies,
146
+ condition: buildConditionPayload(news.condition),
147
+ expiresOn: news.expiresOn,
148
+ notBefore: news.notBefore,
149
+ });
150
+ // Cloudflare doesn't return the value on update; preserve the
151
+ // value captured at creation.
152
+ return buildAttributes(result, output.value);
153
+ }),
154
+ delete: Effect.fn(function* ({ output }) {
155
+ yield* deleteToken({ tokenId: output.tokenId }).pipe(
156
+ Effect.catchTag("TokenNotFound", () => Effect.void),
157
+ );
158
+ }),
159
+ read: Effect.fn(function* ({ output }) {
160
+ if (!output?.tokenId) return undefined;
161
+ return yield* getToken({ tokenId: output.tokenId }).pipe(
162
+ Effect.map((token) => buildAttributes(token, output.value)),
163
+ Effect.catchTag("TokenNotFound", () => Effect.succeed(undefined)),
164
+ );
165
+ }),
166
+ };
167
+ }),
168
+ );
@@ -0,0 +1,4 @@
1
+ export * from "./AccountApiToken.ts";
2
+ export * from "./Common.ts";
3
+ export * from "./PermissionGroups.ts";
4
+ export * from "./UserApiToken.ts";
@@ -46,8 +46,8 @@ const options: Array<{
46
46
  },
47
47
  {
48
48
  value: "stored",
49
- label: "Stored",
50
- hint: "stored in ~/.alchemy/credentials",
49
+ label: "API Token or API Key",
50
+ hint: "enter credentials interactively, stored in ~/.alchemy/credentials",
51
51
  },
52
52
  ];
53
53
 
@@ -5,6 +5,7 @@ import * as Option from "effect/Option";
5
5
  import * as Schedule from "effect/Schedule";
6
6
  import type * as rolldown from "rolldown";
7
7
  import { AdoptPolicy } from "../../AdoptPolicy.ts";
8
+ import { AlchemyContext } from "../../AlchemyContext.ts";
8
9
  import * as Bundle from "../../Bundle/Bundle.ts";
9
10
  import {
10
11
  dockerBuild,
@@ -16,7 +17,6 @@ import {
16
17
  findCwdForBundle,
17
18
  getStableContextDir,
18
19
  } from "../../Bundle/TempRoot.ts";
19
- import { DotAlchemy } from "../../Config.ts";
20
20
  import { deepEqual, isResolved } from "../../Diff.ts";
21
21
  import { createPhysicalName } from "../../PhysicalName.ts";
22
22
  import {
@@ -359,7 +359,7 @@ export const ContainerProvider = () =>
359
359
  const adoptPolicy = yield* Effect.serviceOption(AdoptPolicy).pipe(
360
360
  Effect.map(Option.getOrElse(() => false)),
361
361
  );
362
- const dotAlchemy = yield* DotAlchemy;
362
+ const { dotAlchemy } = yield* AlchemyContext;
363
363
  const fs = yield* FileSystem.FileSystem;
364
364
  const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
365
365
  const createContainerApplication =
@@ -498,6 +498,12 @@ export const ContainerProvider = () =>
498
498
  ...external,
499
499
  ],
500
500
  platform: "node",
501
+ resolve: {
502
+ conditionNames:
503
+ runtime === "bun"
504
+ ? ["bun", "import", "module", "default"]
505
+ : ["node", "import", "module", "default"],
506
+ },
501
507
  plugins,
502
508
  treeshake: true,
503
509
  },
@@ -0,0 +1,144 @@
1
+ import { Credentials } from "@distilled.cloud/cloudflare/Credentials";
2
+ import * as workers from "@distilled.cloud/cloudflare/workers";
3
+ import * as Data from "effect/Data";
4
+ import * as Effect from "effect/Effect";
5
+ import * as HttpClient from "effect/unstable/http/HttpClient";
6
+ import { CloudflareEnvironment } from "./CloudflareEnvironment.ts";
7
+
8
+ /**
9
+ * A "remote binding" as understood by the Cloudflare Workers
10
+ * edge-preview API. We reuse the distilled request schema so the
11
+ * shape tracks Cloudflare's surface.
12
+ */
13
+ type EdgeMetadata = NonNullable<
14
+ NonNullable<workers.CreateScriptEdgePreviewRequest["metadata"]>
15
+ >;
16
+ export type EdgeBinding = NonNullable<
17
+ NonNullable<EdgeMetadata["bindings"]>
18
+ >[number];
19
+
20
+ export class EdgeSessionError extends Data.TaggedError("EdgeSessionError")<{
21
+ readonly message: string;
22
+ readonly cause?: unknown;
23
+ }> {}
24
+
25
+ export interface EdgeSessionOptions {
26
+ /**
27
+ * Name used as the preview script identifier on Cloudflare's side.
28
+ * Shows up as the hostname: `{scriptName}.{subdomain}.workers.dev`.
29
+ */
30
+ readonly scriptName: string;
31
+ /** ES-module worker script(s). The first file is the entrypoint. */
32
+ readonly files: ReadonlyArray<File>;
33
+ /** Remote bindings attached to the preview worker. */
34
+ readonly bindings: ReadonlyArray<EdgeBinding>;
35
+ /** @default "2025-04-28" */
36
+ readonly compatibilityDate?: string;
37
+ }
38
+
39
+ /**
40
+ * Information needed to call a running edge-preview worker. Attach
41
+ * `headers` (including the `cf-workers-preview-token`) and fetch
42
+ * `url` or a path under it.
43
+ */
44
+ export interface EdgeSessionHandle {
45
+ readonly url: string;
46
+ readonly headers: Record<string, string>;
47
+ }
48
+
49
+ const DEFAULT_COMPATIBILITY_DATE = "2025-04-28";
50
+
51
+ const wrap = <A, E, R>(effect: Effect.Effect<A, E, R>, message: string) =>
52
+ effect.pipe(
53
+ Effect.mapError((cause) => new EdgeSessionError({ message, cause })),
54
+ );
55
+
56
+ /**
57
+ * Ask Cloudflare for a preview upload token. Some accounts return an
58
+ * `exchangeUrl` that must be GET'd to swap the session token for a
59
+ * short-lived upload token; when present we do the swap, falling back
60
+ * to the original if the exchange fails.
61
+ */
62
+ const createUploadToken = Effect.gen(function* () {
63
+ const env = yield* CloudflareEnvironment;
64
+ const http = yield* HttpClient.HttpClient;
65
+ const createSubdomainEdgePreviewSession =
66
+ yield* workers.createSubdomainEdgePreviewSession;
67
+ const { token, exchangeUrl } = yield* createSubdomainEdgePreviewSession({
68
+ accountId: env.accountId,
69
+ });
70
+ if (!exchangeUrl) return token;
71
+ const json = yield* http.get(exchangeUrl).pipe(
72
+ Effect.flatMap((r) => r.json),
73
+ Effect.timeout(30_000),
74
+ Effect.catch(() => Effect.succeed(null as unknown)),
75
+ );
76
+ if (
77
+ typeof json === "object" &&
78
+ json !== null &&
79
+ "token" in json &&
80
+ typeof (json as Record<string, unknown>).token === "string"
81
+ ) {
82
+ return (json as { token: string }).token;
83
+ }
84
+ return token;
85
+ });
86
+
87
+ const uploadScript = (options: EdgeSessionOptions, uploadToken: string) =>
88
+ Effect.gen(function* () {
89
+ const env = yield* CloudflareEnvironment;
90
+ const createScriptEdgePreview = yield* workers.createScriptEdgePreview;
91
+ return yield* createScriptEdgePreview({
92
+ accountId: env.accountId,
93
+ scriptName: options.scriptName,
94
+ cfPreviewUploadConfigToken: uploadToken,
95
+ wranglerSessionConfig: { workersDev: true, minimalMode: true },
96
+ metadata: {
97
+ compatibilityDate:
98
+ options.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE,
99
+ bindings: options.bindings as EdgeMetadata["bindings"],
100
+ mainModule: options.files[0]!.name,
101
+ },
102
+ // Distilled's generated schema expects a mutable array; our
103
+ // public surface keeps `readonly` so callers can pass a frozen
104
+ // tuple without a cast on their side.
105
+ files: options.files.slice(),
106
+ }).pipe(Effect.timeout(30_000));
107
+ });
108
+
109
+ const workerHost = (scriptName: string) =>
110
+ Effect.gen(function* () {
111
+ const env = yield* CloudflareEnvironment;
112
+ const getSubdomain = yield* workers.getSubdomain;
113
+ const { subdomain } = yield* getSubdomain({ accountId: env.accountId });
114
+ return `${scriptName}.${subdomain}.workers.dev`;
115
+ });
116
+
117
+ /**
118
+ * Upload a short-lived preview worker to Cloudflare's edge and
119
+ * return its URL plus the headers needed to reach it.
120
+ *
121
+ * Preview workers are ephemeral: they live only while a client keeps
122
+ * sending requests with the returned preview token and expire soon
123
+ * after. Nothing is deployed or billed.
124
+ */
125
+ export const createEdgeSession = (
126
+ options: EdgeSessionOptions,
127
+ ): Effect.Effect<
128
+ EdgeSessionHandle,
129
+ EdgeSessionError,
130
+ CloudflareEnvironment | HttpClient.HttpClient | Credentials
131
+ > =>
132
+ Effect.gen(function* () {
133
+ const [{ previewToken }, host] = yield* Effect.all(
134
+ [
135
+ createUploadToken.pipe(Effect.flatMap((t) => uploadScript(options, t))),
136
+ workerHost(options.scriptName),
137
+ ],
138
+ { concurrency: "unbounded" },
139
+ );
140
+ return {
141
+ url: `https://${host}`,
142
+ headers: { "cf-workers-preview-token": previewToken },
143
+ } satisfies EdgeSessionHandle;
144
+ }).pipe((e) => wrap(e, "Failed to create edge preview session"));
@@ -3,6 +3,7 @@ import { Command } from "../Build/Command.ts";
3
3
  import * as Build from "../Build/index.ts";
4
4
  import * as Provider from "../Provider.ts";
5
5
  import { Random, RandomProvider } from "../Random.ts";
6
+ import * as ApiToken from "./ApiToken/index.ts";
6
7
  import { CloudflareAuth } from "./Auth/AuthProvider.ts";
7
8
  import * as CloudflareEnvironment from "./CloudflareEnvironment.ts";
8
9
  import * as Containers from "./Container/index.ts";
@@ -30,6 +31,8 @@ export const providers = () =>
30
31
  Layer.effect(
31
32
  Providers,
32
33
  Provider.collection([
34
+ ApiToken.AccountApiToken,
35
+ ApiToken.UserApiToken,
33
36
  Command,
34
37
  Containers.Container,
35
38
  D1.D1ConnectionPolicy,
@@ -53,6 +56,8 @@ export const providers = () =>
53
56
  ).pipe(
54
57
  Layer.provide(
55
58
  Layer.mergeAll(
59
+ ApiToken.AccountApiTokenProvider(),
60
+ ApiToken.UserApiTokenProvider(),
56
61
  Containers.ContainerProvider(),
57
62
  D1.D1ConnectionPolicyLive,
58
63
  D1.DatabaseProvider(),
@@ -0,0 +1,159 @@
1
+ import {
2
+ BearerTokenValidator,
3
+ StateApi,
4
+ StateAuthLive,
5
+ } from "alchemy/State/HttpStateApi";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Layer from "effect/Layer";
8
+ import * as Path from "effect/Path";
9
+ import * as Etag from "effect/unstable/http/Etag";
10
+ import * as HttpPlatform from "effect/unstable/http/HttpPlatform";
11
+ import * as HttpRouter from "effect/unstable/http/HttpRouter";
12
+ import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
13
+ import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";
14
+ import crypto from "node:crypto";
15
+ import { SecretBindingLive } from "../SecretsStore/SecretBinding.ts";
16
+ import * as Secret from "../SecretsStore/Secret.ts";
17
+ import { Worker } from "../Workers/Worker.ts";
18
+ import Store from "./Store.ts";
19
+ import { AuthToken } from "./Token.ts";
20
+
21
+ export const STATE_STORE_SCRIPT_NAME = "alchemy-state-store" as const;
22
+
23
+ export default Worker(
24
+ "Api",
25
+ {
26
+ name: STATE_STORE_SCRIPT_NAME,
27
+ main: import.meta.path,
28
+ url: true,
29
+ compatibility: {
30
+ flags: ["nodejs_compat"],
31
+ date: "2026-03-17",
32
+ },
33
+ },
34
+ Effect.gen(function* () {
35
+ const secret = yield* Secret.Secret.bind(AuthToken);
36
+ const store = yield* Store;
37
+
38
+ const bearerTokenValidator = Layer.effect(
39
+ BearerTokenValidator,
40
+ secret.get().pipe(
41
+ Effect.map((expected) =>
42
+ BearerTokenValidator.of({
43
+ validate: (token) =>
44
+ !!expected && timingSafeEqual(token, expected)
45
+ ? Effect.void
46
+ : Effect.fail(new HttpApiError.Unauthorized()),
47
+ }),
48
+ ),
49
+ Effect.orDie,
50
+ ),
51
+ );
52
+
53
+ const stateApi = HttpApiBuilder.group(StateApi, "state", (handlers) =>
54
+ handlers
55
+ .handle("listStacks", () =>
56
+ store.getByName(Store.ROOT_DO_NAME).listStacks(),
57
+ )
58
+ .handle("listStages", ({ params }) =>
59
+ store.getByName(params.stack).listStages(),
60
+ )
61
+ .handle("listResources", ({ params }) =>
62
+ store.getByName(params.stack).listResources({ stage: params.stage }),
63
+ )
64
+ .handle("getState", ({ params }) =>
65
+ store
66
+ .getByName(params.stack)
67
+ .get({ stage: params.stage, fqn: decodeURIComponent(params.fqn) }),
68
+ )
69
+ .handle("setState", ({ params, payload }) =>
70
+ store
71
+ .getByName(params.stack)
72
+ .set({
73
+ stage: params.stage,
74
+ fqn: decodeURIComponent(params.fqn),
75
+ value: payload as any,
76
+ })
77
+ .pipe(
78
+ Effect.tap(() =>
79
+ store
80
+ .getByName(Store.ROOT_DO_NAME)
81
+ .registerStack({ stack: params.stack }),
82
+ ),
83
+ ),
84
+ )
85
+ .handle("deleteState", ({ params }) =>
86
+ // The DO method is `remove`, not `delete` — `delete` is
87
+ // reserved by Cloudflare's RPC stub proxy.
88
+ store
89
+ .getByName(params.stack)
90
+ .remove({
91
+ stage: params.stage,
92
+ fqn: decodeURIComponent(params.fqn),
93
+ })
94
+ .pipe(Effect.asVoid),
95
+ )
96
+ .handle("getReplacedResources", ({ params }) =>
97
+ store
98
+ .getByName(params.stack)
99
+ .getReplacedResources({ stage: params.stage }),
100
+ )
101
+ .handle("deleteStack", ({ params, query }) =>
102
+ store
103
+ .getByName(params.stack)
104
+ .deleteStack(
105
+ query.stage === undefined ? {} : { stage: query.stage },
106
+ )
107
+ .pipe(
108
+ Effect.flatMap(() =>
109
+ query.stage === undefined
110
+ ? store
111
+ .getByName(Store.ROOT_DO_NAME)
112
+ .unregisterStack({ stack: params.stack })
113
+ : Effect.void,
114
+ ),
115
+ Effect.asVoid,
116
+ ),
117
+ ),
118
+ );
119
+
120
+ return {
121
+ fetch: HttpApiBuilder.layer(StateApi).pipe(
122
+ Layer.provide(stateApi),
123
+ Layer.provide(StateAuthLive),
124
+ Layer.provide(bearerTokenValidator),
125
+ // The state-store worker never serves files, so HttpPlatform's
126
+ // file-response surface is stubbed.
127
+ Layer.provide([Etag.layer, HttpPlatformStub, Path.layer]),
128
+ HttpRouter.toHttpEffect,
129
+ ),
130
+ };
131
+ }).pipe(Effect.provide(Layer.mergeAll(SecretBindingLive))),
132
+ );
133
+
134
+ /**
135
+ * Stub `HttpPlatform` for the worker. The state-store API never
136
+ * issues file responses, so both surface methods die if invoked. Lets
137
+ * us avoid pulling in a `FileSystem` dependency that workers don't
138
+ * have.
139
+ */
140
+ const HttpPlatformStub = Layer.succeed(HttpPlatform.HttpPlatform, {
141
+ fileResponse: () => Effect.die("HttpPlatform.fileResponse not supported"),
142
+ fileWebResponse: () =>
143
+ Effect.die("HttpPlatform.fileWebResponse not supported"),
144
+ });
145
+
146
+ /**
147
+ * Timing-safe string comparison using the Workers runtime's built-in
148
+ * `crypto.subtle.timingSafeEqual`.
149
+ *
150
+ * @see https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/
151
+ */
152
+ const timingSafeEqual = (a: string, b: string): boolean => {
153
+ const encoder = new TextEncoder();
154
+ const aBytes = encoder.encode(a);
155
+ const bBytes = encoder.encode(b);
156
+ if (aBytes.byteLength !== bBytes.byteLength) return false;
157
+ // @ts-expect-error - TODO(sam)
158
+ return crypto.subtle.timingSafeEqual(aBytes, bBytes);
159
+ };