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,155 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import * as Redacted from "effect/Redacted";
5
+ import * as Schema from "effect/Schema";
6
+ import * as HttpApi from "effect/unstable/httpapi/HttpApi";
7
+ import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
8
+ import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";
9
+ import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
10
+ import * as HttpApiMiddleware from "effect/unstable/httpapi/HttpApiMiddleware";
11
+ import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema";
12
+ import * as HttpApiSecurity from "effect/unstable/httpapi/HttpApiSecurity";
13
+
14
+ export const ResourceStateSchema = Schema.Any;
15
+
16
+ export class BearerTokenValidator extends Context.Service<
17
+ BearerTokenValidator,
18
+ {
19
+ readonly validate: (
20
+ token: string,
21
+ ) => Effect.Effect<void, HttpApiError.Unauthorized>;
22
+ }
23
+ >()("alchemy/State/BearerTokenValidator") {}
24
+
25
+ export class StateAuth extends HttpApiMiddleware.Service<
26
+ StateAuth,
27
+ { requires: BearerTokenValidator }
28
+ >()("alchemy/State/StateAuth", {
29
+ security: {
30
+ bearer: HttpApiSecurity.bearer,
31
+ },
32
+ error: HttpApiError.UnauthorizedNoContent,
33
+ }) {}
34
+
35
+ export const StateAuthLive: Layer.Layer<
36
+ StateAuth,
37
+ never,
38
+ BearerTokenValidator
39
+ > = Layer.effect(
40
+ StateAuth,
41
+ Effect.gen(function* () {
42
+ const validator = yield* BearerTokenValidator;
43
+ return {
44
+ bearer: (httpEffect, { credential }) =>
45
+ validator
46
+ .validate(Redacted.value(credential))
47
+ .pipe(Effect.flatMap(() => httpEffect)),
48
+ };
49
+ }),
50
+ );
51
+
52
+ /** `stack` path segment for nested REST resources. */
53
+ const StackParams = Schema.Struct({
54
+ stack: Schema.String,
55
+ });
56
+
57
+ /** Optional stage selector for stack deletion. */
58
+ const OptionalStageQuery = Schema.Struct({
59
+ stage: Schema.optional(Schema.String),
60
+ });
61
+
62
+ /** `(stack, stage)` path segments shared by stage-scoped endpoints. */
63
+ const StackStage = Schema.Struct({
64
+ stack: Schema.String,
65
+ stage: Schema.String,
66
+ });
67
+
68
+ /** `(stack, stage, fqn)` path segments for a single resource. */
69
+ const ResourceKey = Schema.Struct({
70
+ stack: Schema.String,
71
+ stage: Schema.String,
72
+ fqn: Schema.String,
73
+ });
74
+
75
+ export const ListStacks = HttpApiEndpoint.get("listStacks", "/state/stacks", {
76
+ success: Schema.Array(Schema.String),
77
+ });
78
+
79
+ export const ListStages = HttpApiEndpoint.get(
80
+ "listStages",
81
+ "/state/stacks/:stack/stages",
82
+ {
83
+ params: StackParams,
84
+ success: Schema.Array(Schema.String),
85
+ },
86
+ );
87
+
88
+ export const ListResources = HttpApiEndpoint.get(
89
+ "listResources",
90
+ "/state/stacks/:stack/stages/:stage/resources",
91
+ {
92
+ params: StackStage,
93
+ success: Schema.Array(Schema.String),
94
+ },
95
+ );
96
+
97
+ export const GetState = HttpApiEndpoint.get(
98
+ "getState",
99
+ "/state/stacks/:stack/stages/:stage/resources/:fqn",
100
+ {
101
+ params: ResourceKey,
102
+ success: Schema.UndefinedOr(ResourceStateSchema),
103
+ },
104
+ );
105
+
106
+ export const SetState = HttpApiEndpoint.put(
107
+ "setState",
108
+ "/state/stacks/:stack/stages/:stage/resources/:fqn",
109
+ {
110
+ params: ResourceKey,
111
+ payload: ResourceStateSchema,
112
+ success: ResourceStateSchema,
113
+ },
114
+ );
115
+
116
+ export const DeleteState = HttpApiEndpoint.delete(
117
+ "deleteState",
118
+ "/state/stacks/:stack/stages/:stage/resources/:fqn",
119
+ {
120
+ params: ResourceKey,
121
+ success: HttpApiSchema.NoContent,
122
+ },
123
+ );
124
+
125
+ export const DeleteStack = HttpApiEndpoint.delete(
126
+ "deleteStack",
127
+ "/state/stacks/:stack",
128
+ {
129
+ params: StackParams,
130
+ query: OptionalStageQuery,
131
+ success: HttpApiSchema.NoContent,
132
+ },
133
+ );
134
+
135
+ export const GetReplacedResources = HttpApiEndpoint.get(
136
+ "getReplacedResources",
137
+ "/state/stacks/:stack/stages/:stage/replaced-resources",
138
+ {
139
+ params: StackStage,
140
+ success: Schema.Array(ResourceStateSchema),
141
+ },
142
+ );
143
+
144
+ export class StateGroup extends HttpApiGroup.make("state")
145
+ .add(ListStacks)
146
+ .add(ListStages)
147
+ .add(ListResources)
148
+ .add(GetState)
149
+ .add(SetState)
150
+ .add(DeleteState)
151
+ .add(GetReplacedResources)
152
+ .add(DeleteStack)
153
+ .middleware(StateAuth) {}
154
+
155
+ export class StateApi extends HttpApi.make("alchemy-state").add(StateGroup) {}
@@ -0,0 +1,156 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schedule from "effect/Schedule";
3
+ import * as HttpClient from "effect/unstable/http/HttpClient";
4
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
5
+ import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
6
+ import { StateApi } from "./HttpStateApi.ts";
7
+
8
+ import type { ReplacedResourceState, ResourceState } from "./ResourceState.ts";
9
+ import { StateStoreError, type StateService } from "./State.ts";
10
+ import { encodeState, reviveStateRecursive } from "./StateEncoding.ts";
11
+
12
+ export interface HttpStateStoreProps {
13
+ url: string;
14
+ /** Bearer token used to authenticate every request. */
15
+ authToken: string;
16
+ }
17
+
18
+ export const makeHttpStateStore = ({ url, authToken }: HttpStateStoreProps) =>
19
+ Effect.gen(function* () {
20
+ const apiClient = yield* HttpApiClient.make(StateApi, {
21
+ baseUrl: url,
22
+ transformClient: HttpClient.mapRequest(
23
+ HttpClientRequest.bearerToken(authToken),
24
+ ),
25
+ });
26
+ const state = apiClient.state;
27
+
28
+ const service: StateService = {
29
+ listStacks: () =>
30
+ state.listStacks().pipe(
31
+ Effect.map((stacks) => [...stacks]),
32
+ mapStateStoreError,
33
+ ),
34
+ listStages: (stack) =>
35
+ state.listStages({ params: { stack } }).pipe(mapStateStoreError),
36
+ list: (request) =>
37
+ state.listResources({ params: request }).pipe(mapStateStoreError),
38
+ get: (request) =>
39
+ state
40
+ .getState({
41
+ params: {
42
+ stack: request.stack,
43
+ stage: request.stage,
44
+ fqn: encodeURIComponent(request.fqn),
45
+ },
46
+ })
47
+ .pipe(
48
+ Effect.map((s) =>
49
+ s == null
50
+ ? undefined
51
+ : (reviveStateRecursive(s) as ResourceState),
52
+ ),
53
+ mapStateStoreError,
54
+ ),
55
+ getReplacedResources: (request) =>
56
+ state.getReplacedResources({ params: request }).pipe(
57
+ Effect.map((resources) =>
58
+ resources.map(
59
+ (s) => reviveStateRecursive(s) as ReplacedResourceState,
60
+ ),
61
+ ),
62
+ mapStateStoreError,
63
+ ),
64
+ set: <V extends ResourceState>(request: {
65
+ stack: string;
66
+ stage: string;
67
+ fqn: string;
68
+ value: V;
69
+ }) =>
70
+ state
71
+ .setState({
72
+ params: {
73
+ stack: request.stack,
74
+ stage: request.stage,
75
+ fqn: encodeURIComponent(request.fqn),
76
+ },
77
+ payload: encodeState(request.value),
78
+ })
79
+ .pipe(
80
+ // Server echoes the stored value, but the client already
81
+ // has the canonical object (including any Redacted<T>
82
+ // instances); returning the input avoids a lossy round-trip.
83
+ Effect.map(() => request.value),
84
+ mapStateStoreError,
85
+ ),
86
+ delete: (request) =>
87
+ state
88
+ .deleteState({
89
+ params: {
90
+ stack: request.stack,
91
+ stage: request.stage,
92
+ fqn: encodeURIComponent(request.fqn),
93
+ },
94
+ })
95
+ .pipe(Effect.asVoid, mapStateStoreError),
96
+ deleteStack: (request) =>
97
+ state
98
+ .deleteStack({
99
+ params: { stack: request.stack },
100
+ query: request.stage === undefined ? {} : { stage: request.stage },
101
+ })
102
+ .pipe(Effect.asVoid, mapStateStoreError),
103
+ };
104
+ return service;
105
+ });
106
+
107
+ /**
108
+ * Predicate over an `HttpClientError`-shaped failure that returns `true`
109
+ * for failures we expect to clear up on their own.
110
+ *
111
+ * We retry:
112
+ * - all transport-level errors (no `response` — DNS, TCP reset, TLS,
113
+ * abort, etc.)
114
+ * - 408 (request timeout) and 429 (rate limit)
115
+ * - 404, which is normal in the seconds after a worker is first
116
+ * deployed and the route hasn't propagated yet
117
+ * - every 5xx
118
+ *
119
+ * Anything else (400/401/403/etc.) is a real client-side problem and
120
+ * shouldn't be hidden behind retries.
121
+ */
122
+ const isTransient = (e: any): boolean => {
123
+ if (e?._tag !== "HttpClientError") return false;
124
+ const status: number | undefined = e.response?.status;
125
+ if (status == null) return true;
126
+ if (status === 404 || status === 408 || status === 429) return true;
127
+ return status >= 500 && status < 600;
128
+ };
129
+
130
+ const retryTransient = <A, Err, Req>(eff: Effect.Effect<A, Err, Req>) =>
131
+ Effect.retry(eff, {
132
+ while: isTransient,
133
+ // Exponential backoff capped at 2s, no attempt limit. The state
134
+ // store is on the deploy critical path; we'd rather block than
135
+ // surface a transient blip as a hard failure (which historically
136
+ // stranded `syncState` mid-flight, leaving the remote store
137
+ // partially populated).
138
+ schedule: Schedule.exponential(100).pipe(
139
+ Schedule.either(Schedule.spaced("2 seconds")),
140
+ ),
141
+ });
142
+
143
+ /** Collapse any client failure into a {@link StateStoreError}. */
144
+ const mapStateStoreError = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
145
+ eff.pipe(
146
+ Effect.tapError(Effect.log),
147
+ retryTransient,
148
+ Effect.catch((e: E) =>
149
+ Effect.fail(
150
+ new StateStoreError({
151
+ message: e instanceof Error ? e.message : String(e),
152
+ cause: e instanceof Error ? e : undefined,
153
+ }),
154
+ ),
155
+ ),
156
+ ) as Effect.Effect<A, StateStoreError, R>;
@@ -1,32 +1,23 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import * as Layer from "effect/Layer";
3
3
  import type { ResourceState } from "./ResourceState.ts";
4
- import { State, type StateService } from "./State.ts";
4
+ import { State } from "./State.ts";
5
5
 
6
6
  type StackId = string;
7
7
  type StageId = string;
8
8
  type Fqn = string;
9
9
 
10
- export const InMemory = (
10
+ export const inMemoryState = (
11
11
  initialState: Record<
12
12
  StackId,
13
13
  Record<StageId, Record<Fqn, ResourceState>>
14
14
  > = {},
15
- ) =>
16
- Layer.succeed(State, InMemoryService(initialState)) as Layer.Layer<
17
- State,
18
- never,
19
- never
20
- >;
15
+ ) => Layer.succeed(State, InMemoryService(initialState));
21
16
 
22
17
  export const InMemoryService = (
23
- initialState: Record<
24
- StackId,
25
- Record<StageId, Record<Fqn, ResourceState>>
26
- > = {},
27
- ) => {
28
- const state = initialState;
29
- return {
18
+ state: Record<StackId, Record<StageId, Record<Fqn, ResourceState>>> = {},
19
+ ) =>
20
+ State.of({
30
21
  listStacks: () => Effect.succeed(Array.from(Object.keys(state))),
31
22
  listStages: (stack: string) =>
32
23
  Effect.succeed(
@@ -78,9 +69,16 @@ export const InMemoryService = (
78
69
  stage: string;
79
70
  fqn: string;
80
71
  }) => Effect.succeed(delete state[stack]?.[stage]?.[fqn]),
72
+ deleteStack: ({ stack, stage }: { stack: string; stage?: string }) =>
73
+ Effect.sync(() => {
74
+ if (stage === undefined) {
75
+ delete state[stack];
76
+ } else {
77
+ delete state[stack]?.[stage];
78
+ }
79
+ }),
81
80
  list: ({ stack, stage }: { stack: string; stage: string }) =>
82
81
  Effect.succeed(
83
82
  Array.from(Object.keys(state[stack]?.[stage] ?? {}) ?? []),
84
83
  ),
85
- } satisfies StateService;
86
- };
84
+ });
@@ -3,53 +3,13 @@ import * as FileSystem from "effect/FileSystem";
3
3
  import * as Layer from "effect/Layer";
4
4
  import * as Path from "effect/Path";
5
5
  import type { PlatformError } from "effect/PlatformError";
6
- import * as Redacted from "effect/Redacted";
7
6
  import { decodeFqn, encodeFqn } from "../FQN.ts";
8
- import { isResource } from "../Resource.ts";
9
7
  import { State, StateStoreError, type StateService } from "./State.ts";
8
+ import { encodeState, reviveState } from "./StateEncoding.ts";
10
9
 
11
- const REDACTED_MARKER = "__redacted__";
10
+ export const localState = () => Layer.effect(State, makeLocalState());
12
11
 
13
- const encodeState = (value: unknown): unknown => {
14
- if (value === null || value === undefined) return value;
15
- if (Redacted.isRedacted(value)) {
16
- return {
17
- [REDACTED_MARKER]: encodeState(Redacted.value(value)),
18
- };
19
- }
20
- if (isResource(value)) {
21
- return {
22
- id: value.LogicalId,
23
- type: value.Type,
24
- props: encodeState(value.Props),
25
- attr: encodeState(value.Attributes),
26
- };
27
- }
28
- if (Array.isArray(value)) return value.map(encodeState);
29
- if (typeof value === "object") {
30
- const result: Record<string, unknown> = {};
31
- for (const [k, v] of Object.entries(value)) {
32
- result[k] = encodeState(v);
33
- }
34
- return result;
35
- }
36
- return value;
37
- };
38
-
39
- const reviveState = (_key: string, value: unknown): unknown => {
40
- if (
41
- value !== null &&
42
- typeof value === "object" &&
43
- !Array.isArray(value) &&
44
- REDACTED_MARKER in value
45
- ) {
46
- return Redacted.make((value as Record<string, unknown>)[REDACTED_MARKER]);
47
- }
48
- return value;
49
- };
50
-
51
- export const LocalState = Layer.effect(
52
- State,
12
+ export const makeLocalState = () =>
53
13
  Effect.gen(function* () {
54
14
  const fs = yield* FileSystem.FileSystem;
55
15
  const path = yield* Path.Path;
@@ -132,6 +92,15 @@ export const LocalState = Layer.effect(
132
92
  Effect.map(() => request.value),
133
93
  ),
134
94
  delete: (request) => fs.remove(resource(request)).pipe(recover),
95
+ deleteStack: ({ stack, stage }) =>
96
+ fs
97
+ .remove(
98
+ stage === undefined
99
+ ? path.join(stateDir, stack)
100
+ : stageDir({ stack, stage }),
101
+ { recursive: true },
102
+ )
103
+ .pipe(recover),
135
104
  list: (request) =>
136
105
  fs.readDirectory(stageDir(request)).pipe(
137
106
  recover,
@@ -143,5 +112,4 @@ export const LocalState = Layer.effect(
143
112
  ),
144
113
  };
145
114
  return state;
146
- }),
147
- );
115
+ });
@@ -9,7 +9,7 @@ export class StateStoreError extends Data.TaggedError("StateStoreError")<{
9
9
  }> {}
10
10
 
11
11
  export class State extends Context.Service<State, StateService>()(
12
- "AWS::Lambda::State",
12
+ "alchemy/State",
13
13
  ) {}
14
14
 
15
15
  /**
@@ -21,8 +21,10 @@ export class State extends Context.Service<State, StateService>()(
21
21
  * for provider operations.
22
22
  */
23
23
  export interface StateService {
24
- listStacks(): Effect.Effect<string[], StateStoreError, never>;
25
- listStages(stack: string): Effect.Effect<string[], StateStoreError, never>;
24
+ listStacks(): Effect.Effect<readonly string[], StateStoreError, never>;
25
+ listStages(
26
+ stack: string,
27
+ ): Effect.Effect<readonly string[], StateStoreError, never>;
26
28
  /**
27
29
  * Get a resource by its FQN (namespace-qualified key).
28
30
  */
@@ -62,11 +64,18 @@ export interface StateService {
62
64
  stage: string;
63
65
  fqn: string;
64
66
  }): Effect.Effect<void, StateStoreError, never>;
67
+ /**
68
+ * Delete an entire stack, or a single stage when `stage` is provided.
69
+ */
70
+ deleteStack(request: {
71
+ stack: string;
72
+ stage?: string;
73
+ }): Effect.Effect<void, StateStoreError, never>;
65
74
  /**
66
75
  * List all resource FQNs in a stack/stage.
67
76
  */
68
77
  list(request: {
69
78
  stack: string;
70
79
  stage: string;
71
- }): Effect.Effect<string[], StateStoreError, never>;
80
+ }): Effect.Effect<readonly string[], StateStoreError, never>;
72
81
  }
@@ -0,0 +1,83 @@
1
+ import * as Redacted from "effect/Redacted";
2
+ import { isResource } from "../Resource.ts";
3
+
4
+ /**
5
+ * JSON marker used to tag a `Redacted<T>` value when writing state.
6
+ * The reviver recognises objects with exactly this key and rebuilds
7
+ * the `Redacted` wrapper on read.
8
+ */
9
+ export const REDACTED_MARKER = "__redacted__";
10
+
11
+ /**
12
+ * Recursively encode a state value for JSON serialisation.
13
+ *
14
+ * - `Redacted<T>` values are wrapped as `{ [REDACTED_MARKER]: <inner> }`
15
+ * so the actual string is persisted rather than the `<redacted>`
16
+ * placeholder produced by the default `toJSON`.
17
+ * - `Resource` instances are flattened to `{ id, type, props, attr }`
18
+ * so persisted state matches the schema used by the loader.
19
+ * - Plain objects and arrays are walked structurally.
20
+ */
21
+ export const encodeState = (value: unknown): unknown => {
22
+ if (value === null || value === undefined) return value;
23
+ if (Redacted.isRedacted(value)) {
24
+ return {
25
+ [REDACTED_MARKER]: encodeState(Redacted.value(value)),
26
+ };
27
+ }
28
+ if (isResource(value)) {
29
+ return {
30
+ id: value.LogicalId,
31
+ type: value.Type,
32
+ props: encodeState(value.Props),
33
+ attr: encodeState(value.Attributes),
34
+ };
35
+ }
36
+ if (Array.isArray(value)) return value.map(encodeState);
37
+ if (typeof value === "object") {
38
+ const result: Record<string, unknown> = {};
39
+ for (const [k, v] of Object.entries(value)) {
40
+ result[k] = encodeState(v);
41
+ }
42
+ return result;
43
+ }
44
+ return value;
45
+ };
46
+
47
+ /**
48
+ * JSON reviver that rebuilds `Redacted<T>` values that were written
49
+ * through {@link encodeState}. Intended for use with `JSON.parse`.
50
+ */
51
+ export const reviveState = (_key: string, value: unknown): unknown => {
52
+ if (
53
+ value !== null &&
54
+ typeof value === "object" &&
55
+ !Array.isArray(value) &&
56
+ REDACTED_MARKER in value
57
+ ) {
58
+ return Redacted.make((value as Record<string, unknown>)[REDACTED_MARKER]);
59
+ }
60
+ return value;
61
+ };
62
+
63
+ /**
64
+ * Recursively walk an already-decoded value and rebuild `Redacted<T>`
65
+ * instances from `{ [REDACTED_MARKER]: <inner> }` envelopes. Mirror
66
+ * image of {@link encodeState} for callers that hold a parsed JS
67
+ * value rather than a JSON string (e.g. the HTTP state-store client,
68
+ * which receives values pre-parsed by `HttpApiClient`).
69
+ */
70
+ export const reviveStateRecursive = (value: unknown): unknown => {
71
+ if (value === null || typeof value !== "object") return value;
72
+ if (Array.isArray(value)) return value.map(reviveStateRecursive);
73
+ const obj = value as Record<string, unknown>;
74
+ const keys = Object.keys(obj);
75
+ if (keys.length === 1 && keys[0] === REDACTED_MARKER) {
76
+ return Redacted.make(reviveStateRecursive(obj[REDACTED_MARKER]));
77
+ }
78
+ const result: Record<string, unknown> = {};
79
+ for (const [k, v] of Object.entries(obj)) {
80
+ result[k] = reviveStateRecursive(v);
81
+ }
82
+ return result;
83
+ };
@@ -0,0 +1,89 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { type StateService } from "./State.ts";
3
+
4
+ /**
5
+ * Synchronize all state (every stack/stage/resource) from `source` into
6
+ * `destination` so that `destination` becomes a mirror of `source`.
7
+ *
8
+ * For each `{ stack, stage, fqn }` present in `source`, the resource is
9
+ * written into `destination`, overwriting any existing entry under the same
10
+ * key. Any keys present in `destination` but absent from `source` are
11
+ * deleted, ensuring the two stores end up structurally identical.
12
+ *
13
+ * Stacks are walked sequentially; stages within a stack and resources
14
+ * within a stage are processed concurrently for throughput.
15
+ */
16
+ export const syncState = Effect.fnUntraced(function* (
17
+ source: StateService,
18
+ destination: StateService,
19
+ options?: {
20
+ stacks?: string[];
21
+ /**
22
+ * Maximum number of resources to copy in parallel within a single stage.
23
+ * @default "unbounded".
24
+ */
25
+ concurrency?: number | "unbounded";
26
+ },
27
+ ) {
28
+ const concurrency = options?.concurrency ?? "unbounded";
29
+ const [sourceStacks, destStacks] = yield* Effect.all([
30
+ source.listStacks(),
31
+ destination.listStacks(),
32
+ ]);
33
+ const sourceStackSet = new Set(
34
+ sourceStacks.filter((stack) => options?.stacks?.includes(stack) ?? true),
35
+ );
36
+
37
+ yield* Effect.forEach(
38
+ sourceStacks,
39
+ Effect.fnUntraced(function* (stack) {
40
+ const [sourceStages, destStages] = yield* Effect.all([
41
+ source.listStages(stack),
42
+ destination.listStages(stack),
43
+ ]);
44
+ const stages = union(sourceStages, destStages);
45
+
46
+ yield* Effect.forEach(
47
+ stages,
48
+ Effect.fnUntraced(function* (stage) {
49
+ const sourceFqns = yield* source.list({ stack, stage });
50
+ const destFqns = yield* destination.list({ stack, stage });
51
+
52
+ const sourceSet = new Set(sourceFqns);
53
+ const toDelete = destFqns.filter((fqn) => !sourceSet.has(fqn));
54
+
55
+ yield* Effect.all(
56
+ [
57
+ Effect.forEach(
58
+ sourceFqns,
59
+ Effect.fnUntraced(function* (fqn) {
60
+ const value = yield* source.get({ stack, stage, fqn });
61
+ if (value) {
62
+ yield* destination.set({ stack, stage, fqn, value });
63
+ }
64
+ }),
65
+ { concurrency },
66
+ ),
67
+ Effect.forEach(
68
+ toDelete,
69
+ (fqn) => destination.delete({ stack, stage, fqn }),
70
+ { concurrency },
71
+ ),
72
+ ],
73
+ { concurrency: "unbounded" },
74
+ );
75
+ }),
76
+ { concurrency: "unbounded" },
77
+ );
78
+ }),
79
+ );
80
+
81
+ yield* Effect.forEach(
82
+ destStacks.filter((stack) => !sourceStackSet.has(stack)),
83
+ (stack) => destination.deleteStack({ stack }),
84
+ );
85
+ });
86
+
87
+ const union = <T>(left: Iterable<T>, right: Iterable<T>) => [
88
+ ...new Set([...left, ...right]),
89
+ ];
@@ -1,4 +1,8 @@
1
+ export * from "./HttpStateApi.ts";
2
+ export * from "./HttpStateStore.ts";
1
3
  export * from "./InMemoryState.ts";
2
4
  export * from "./LocalState.ts";
3
5
  export * from "./ResourceState.ts";
4
6
  export * from "./State.ts";
7
+ export * from "./StateEncoding.ts";
8
+ export * from "./Sync.ts";