alchemy 2.0.0-beta.45 → 2.0.0-beta.47

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 (158) hide show
  1. package/bin/exec.js +18 -10
  2. package/bin/exec.js.map +1 -1
  3. package/lib/AlchemyContext.d.ts +0 -1
  4. package/lib/AlchemyContext.d.ts.map +1 -1
  5. package/lib/AlchemyContext.js.map +1 -1
  6. package/lib/Apply.js +2 -2
  7. package/lib/Apply.js.map +1 -1
  8. package/lib/Cli/commands/_shared.d.ts +7 -1
  9. package/lib/Cli/commands/_shared.d.ts.map +1 -1
  10. package/lib/Cli/commands/_shared.js +1 -0
  11. package/lib/Cli/commands/_shared.js.map +1 -1
  12. package/lib/Cli/commands/cloudflare.d.ts.map +1 -1
  13. package/lib/Cli/commands/cloudflare.js +5 -1
  14. package/lib/Cli/commands/cloudflare.js.map +1 -1
  15. package/lib/Cli/commands/login.d.ts +1 -1
  16. package/lib/Cli/commands/login.d.ts.map +1 -1
  17. package/lib/Cli/commands/login.js +45 -42
  18. package/lib/Cli/commands/login.js.map +1 -1
  19. package/lib/Cli/commands/logs.js +1 -1
  20. package/lib/Cli/commands/logs.js.map +1 -1
  21. package/lib/Cli/commands/state.js +1 -1
  22. package/lib/Cli/commands/state.js.map +1 -1
  23. package/lib/Cli/commands/tail.js +1 -1
  24. package/lib/Cli/commands/tail.js.map +1 -1
  25. package/lib/Cloudflare/AiGateway/AiGateway.d.ts +112 -1
  26. package/lib/Cloudflare/AiGateway/AiGateway.d.ts.map +1 -1
  27. package/lib/Cloudflare/AiGateway/AiGateway.js +112 -1
  28. package/lib/Cloudflare/AiGateway/AiGateway.js.map +1 -1
  29. package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts +43 -10
  30. package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts.map +1 -1
  31. package/lib/Cloudflare/AiGateway/AiGatewayBinding.js +38 -10
  32. package/lib/Cloudflare/AiGateway/AiGatewayBinding.js.map +1 -1
  33. package/lib/Cloudflare/AiGateway/LanguageModel.d.ts +35 -0
  34. package/lib/Cloudflare/AiGateway/LanguageModel.d.ts.map +1 -0
  35. package/lib/Cloudflare/AiGateway/LanguageModel.js +576 -0
  36. package/lib/Cloudflare/AiGateway/LanguageModel.js.map +1 -0
  37. package/lib/Cloudflare/AiGateway/index.d.ts +1 -0
  38. package/lib/Cloudflare/AiGateway/index.d.ts.map +1 -1
  39. package/lib/Cloudflare/AiGateway/index.js +1 -0
  40. package/lib/Cloudflare/AiGateway/index.js.map +1 -1
  41. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  42. package/lib/Cloudflare/Providers.js +5 -1
  43. package/lib/Cloudflare/Providers.js.map +1 -1
  44. package/lib/Cloudflare/Queue/QueueConsumer.d.ts.map +1 -1
  45. package/lib/Cloudflare/Queue/QueueConsumer.js +1 -2
  46. package/lib/Cloudflare/Queue/QueueConsumer.js.map +1 -1
  47. package/lib/Cloudflare/StateStore/Api.d.ts +1 -1
  48. package/lib/Cloudflare/StateStore/Api.js +1 -1
  49. package/lib/Cloudflare/StateStore/State.d.ts +11 -65
  50. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  51. package/lib/Cloudflare/StateStore/State.js +313 -368
  52. package/lib/Cloudflare/StateStore/State.js.map +1 -1
  53. package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -1
  54. package/lib/Cloudflare/StateStore/Store.js +16 -29
  55. package/lib/Cloudflare/StateStore/Store.js.map +1 -1
  56. package/lib/Cloudflare/Vectorize/VectorizeIndex.d.ts +114 -0
  57. package/lib/Cloudflare/Vectorize/VectorizeIndex.d.ts.map +1 -0
  58. package/lib/Cloudflare/Vectorize/VectorizeIndex.js +157 -0
  59. package/lib/Cloudflare/Vectorize/VectorizeIndex.js.map +1 -0
  60. package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.d.ts +38 -0
  61. package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.d.ts.map +1 -0
  62. package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.js +44 -0
  63. package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.js.map +1 -0
  64. package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.d.ts +72 -0
  65. package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.d.ts.map +1 -0
  66. package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.js +131 -0
  67. package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.js.map +1 -0
  68. package/lib/Cloudflare/Vectorize/index.d.ts +4 -0
  69. package/lib/Cloudflare/Vectorize/index.d.ts.map +1 -0
  70. package/lib/Cloudflare/Vectorize/index.js +4 -0
  71. package/lib/Cloudflare/Vectorize/index.js.map +1 -0
  72. package/lib/Cloudflare/Workers/DurableObjectBridge.d.ts.map +1 -1
  73. package/lib/Cloudflare/Workers/DurableObjectBridge.js +10 -1
  74. package/lib/Cloudflare/Workers/DurableObjectBridge.js.map +1 -1
  75. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  76. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +13 -1
  77. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  78. package/lib/Cloudflare/Workers/Rpc.d.ts.map +1 -1
  79. package/lib/Cloudflare/Workers/Rpc.js +20 -2
  80. package/lib/Cloudflare/Workers/Rpc.js.map +1 -1
  81. package/lib/Cloudflare/Workers/WorkerAsyncBindings.d.ts.map +1 -1
  82. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js +8 -0
  83. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js.map +1 -1
  84. package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -1
  85. package/lib/Cloudflare/Workers/WorkerBinding.d.ts.map +1 -1
  86. package/lib/Cloudflare/Workers/WorkerBinding.js.map +1 -1
  87. package/lib/Cloudflare/Workers/WorkerBridge.d.ts.map +1 -1
  88. package/lib/Cloudflare/Workers/WorkerBridge.js +20 -2
  89. package/lib/Cloudflare/Workers/WorkerBridge.js.map +1 -1
  90. package/lib/Cloudflare/index.d.ts +1 -0
  91. package/lib/Cloudflare/index.d.ts.map +1 -1
  92. package/lib/Cloudflare/index.js +1 -0
  93. package/lib/Cloudflare/index.js.map +1 -1
  94. package/lib/Output.js +2 -2
  95. package/lib/Output.js.map +1 -1
  96. package/lib/Plan.d.ts +2 -2
  97. package/lib/Plan.d.ts.map +1 -1
  98. package/lib/Plan.js +3 -3
  99. package/lib/Plan.js.map +1 -1
  100. package/lib/Stack.d.ts.map +1 -1
  101. package/lib/Stack.js +6 -0
  102. package/lib/Stack.js.map +1 -1
  103. package/lib/State/HttpStateStore.d.ts +4 -0
  104. package/lib/State/HttpStateStore.d.ts.map +1 -1
  105. package/lib/State/HttpStateStore.js +13 -0
  106. package/lib/State/HttpStateStore.js.map +1 -1
  107. package/lib/State/InMemoryState.d.ts +2 -1
  108. package/lib/State/InMemoryState.d.ts.map +1 -1
  109. package/lib/State/InMemoryState.js +3 -3
  110. package/lib/State/InMemoryState.js.map +1 -1
  111. package/lib/State/LocalState.d.ts.map +1 -1
  112. package/lib/State/LocalState.js +5 -1
  113. package/lib/State/LocalState.js.map +1 -1
  114. package/lib/State/State.d.ts +2 -2
  115. package/lib/State/State.d.ts.map +1 -1
  116. package/lib/State/State.js.map +1 -1
  117. package/lib/Util/poll.d.ts +30 -0
  118. package/lib/Util/poll.d.ts.map +1 -0
  119. package/lib/Util/poll.js +26 -0
  120. package/lib/Util/poll.js.map +1 -0
  121. package/lib/tsconfig.test.tsbuildinfo +1 -1
  122. package/package.json +7 -7
  123. package/src/AlchemyContext.ts +0 -1
  124. package/src/Apply.ts +2 -2
  125. package/src/Cli/commands/_shared.ts +7 -1
  126. package/src/Cli/commands/cloudflare.ts +5 -3
  127. package/src/Cli/commands/login.ts +69 -60
  128. package/src/Cli/commands/logs.ts +1 -1
  129. package/src/Cli/commands/state.ts +1 -1
  130. package/src/Cli/commands/tail.ts +1 -1
  131. package/src/Cloudflare/AiGateway/AiGateway.ts +112 -1
  132. package/src/Cloudflare/AiGateway/AiGatewayBinding.ts +52 -10
  133. package/src/Cloudflare/AiGateway/LanguageModel.ts +907 -0
  134. package/src/Cloudflare/AiGateway/index.ts +1 -0
  135. package/src/Cloudflare/Providers.ts +7 -0
  136. package/src/Cloudflare/Queue/QueueConsumer.ts +0 -2
  137. package/src/Cloudflare/StateStore/Api.ts +1 -1
  138. package/src/Cloudflare/StateStore/State.ts +452 -508
  139. package/src/Cloudflare/StateStore/Store.ts +22 -35
  140. package/src/Cloudflare/Vectorize/VectorizeIndex.ts +261 -0
  141. package/src/Cloudflare/Vectorize/VectorizeIndexBinding.ts +103 -0
  142. package/src/Cloudflare/Vectorize/VectorizeMetadataIndex.ts +208 -0
  143. package/src/Cloudflare/Vectorize/index.ts +3 -0
  144. package/src/Cloudflare/Workers/DurableObjectBridge.ts +10 -1
  145. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +13 -1
  146. package/src/Cloudflare/Workers/Rpc.ts +38 -9
  147. package/src/Cloudflare/Workers/WorkerAsyncBindings.ts +7 -0
  148. package/src/Cloudflare/Workers/WorkerBinding.ts +2 -0
  149. package/src/Cloudflare/Workers/WorkerBridge.ts +20 -2
  150. package/src/Cloudflare/index.ts +1 -0
  151. package/src/Output.ts +2 -2
  152. package/src/Plan.ts +4 -4
  153. package/src/Stack.ts +6 -0
  154. package/src/State/HttpStateStore.ts +28 -0
  155. package/src/State/InMemoryState.ts +87 -85
  156. package/src/State/LocalState.ts +15 -1
  157. package/src/State/State.ts +5 -4
  158. package/src/Util/poll.ts +47 -0
@@ -8,86 +8,139 @@ import * as HttpClient from "effect/unstable/http/HttpClient";
8
8
  import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
9
9
  import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
10
10
  import crypto from "node:crypto";
11
- import * as Output from "../../Output.js";
12
11
  import * as Config from "effect/Config";
12
+ import { isHttpClientError } from "effect/unstable/http/HttpClientError";
13
13
  import { adopt } from "../../AdoptPolicy.js";
14
14
  import { AuthError } from "../../Auth/AuthProvider.js";
15
15
  import { CredentialsStore, CredentialsStoreLive, } from "../../Auth/Credentials.js";
16
16
  import { ALCHEMY_PROFILE, ProfileLive } from "../../Auth/Profile.js";
17
17
  import * as Cloudflare from "../../Cloudflare/Providers.js";
18
18
  import { deploy } from "../../Deploy.js";
19
+ import * as Output from "../../Output.js";
19
20
  import * as Alchemy from "../../Stack.js";
20
21
  import { StateApi } from "../../State/HttpStateApi.js";
21
- import { makeHttpStateStore, } from "../../State/HttpStateStore.js";
22
- import * as CloudflareEnvironment from "../CloudflareEnvironment.js";
23
- import * as Credentials from "../Credentials.js";
24
- import { AlchemyContext } from "../../AlchemyContext.js";
22
+ import { checkHttpStateStoreAuth, makeHttpStateStore, } from "../../State/HttpStateStore.js";
25
23
  import { makeLocalState } from "../../State/LocalState.js";
26
24
  import { State } from "../../State/State.js";
27
25
  import { recordStateStoreInit, recordStateStoreOp, } from "../../Telemetry/Metrics.js";
28
26
  import * as Clank from "../../Util/Clank.js";
29
27
  import * as Access from "../Access.js";
30
28
  import { CloudflareAuth } from "../Auth/AuthProvider.js";
29
+ import * as CloudflareEnvironment from "../CloudflareEnvironment.js";
30
+ import * as Credentials from "../Credentials.js";
31
31
  import { EdgeSessionError, createEdgeSession } from "../EdgeSession.js";
32
32
  import Api, { STATE_STORE_SCRIPT_NAME, STATE_STORE_VERSION } from "./Api.js";
33
33
  import { AuthToken, AuthTokenSecretName, TokenValue } from "./Token.js";
34
+ const CI = Config.boolean("CI").pipe(Config.withDefault(false));
34
35
  /** Filename used for stored credentials under the profile directory. */
35
36
  const CREDENTIALS_FILE = "cloudflare-state-store";
36
- /**
37
- * SHA-256 hex digest of the Cloudflare account ID. Used as a stable
38
- * pseudonymous identifier on telemetry spans so the dashboard can
39
- * count distinct state-store deployments without leaking the raw
40
- * accountId. Mirrors the `alchemy.git.origin_hash` pattern in
41
- * `Telemetry/Attributes.ts`.
42
- */
43
- const hashAccountId = (accountId) => Effect.sync(() => crypto.createHash("sha256").update(accountId).digest("hex"));
44
- /**
45
- * Best-effort Cloudflare-account-hash annotation on the current span.
46
- * Resolves the accountId from {@link CloudflareEnvironment} and
47
- * attaches `alchemy.cloudflare.account_hash` to whichever span is
48
- * active. Silently no-ops if the environment isn't resolvable so
49
- * State-store layer construction still succeeds in degraded paths.
50
- *
51
- * `noTrack` controls whether the hash is attached:
52
- * - `true` — never annotate (caller-level opt-out).
53
- * - `false` — always annotate, regardless of env.
54
- * - `undefined` — fall back to the `NO_TRACK` env var; default off.
55
- */
56
- const annotateAccountHash = (noTrack) => Effect.gen(function* () {
57
- if (noTrack === true)
58
- return;
59
- if (noTrack === undefined) {
60
- const fromEnv = yield* Config.boolean("NO_TRACK").pipe(Config.withDefault(false));
61
- if (fromEnv)
62
- return;
63
- }
64
- const env = yield* Effect.serviceOption(CloudflareEnvironment.CloudflareEnvironment);
65
- if (env._tag !== "Some")
66
- return;
67
- const hash = yield* hashAccountId(env.value.accountId);
68
- yield* Effect.annotateCurrentSpan("alchemy.cloudflare.account_hash", hash);
69
- }).pipe(Effect.catch(() => Effect.void));
70
- /**
71
- * Manually bootstrap (or repair) the Cloudflare-hosted HTTP State
72
- * Store. Used by `alchemy bootstrap cloudflare`.
73
- *
74
- * The flow mirrors what {@link state} does on first use, but is
75
- * exposed as a standalone effect so users can trigger it (or recover
76
- * from a partially-failed previous deploy) explicitly:
77
- *
78
- * 1. Resume any in-progress local `CloudflareStateStore` stack.
79
- * 2. If the worker already exists in Cloudflare and `force` is not
80
- * set, adopt it: read the auth token via an edge-preview probe
81
- * and persist credentials.
82
- * 3. Otherwise run the full deploy with adoption enabled, sync local
83
- * state into the deployed store, persist credentials, and delete
84
- * the local stack.
85
- */
86
- export const bootstrap = (options = {}) => Effect.gen(function* () {
87
- const isCI = yield* Config.boolean("CI").pipe(Config.withDefault(false));
37
+ export const state = () => Layer.effect(State, Effect.gen(function* () {
38
+ const isCI = yield* CI;
39
+ const scriptName = STATE_STORE_SCRIPT_NAME;
88
40
  const profileName = yield* ALCHEMY_PROFILE;
41
+ const localStage = `${profileName}_${scriptName}`;
42
+ const credStore = yield* CredentialsStore;
43
+ const context = yield* Effect.context();
44
+ const init = Effect.gen(function* () {
45
+ if (yield* hasLocalStack(localStage)) {
46
+ // if there's still a local stack, then we need to finish the bootstrap
47
+ // TODO(sam): what if the local stack was
48
+ return yield* deployWithLocalState({
49
+ scriptName,
50
+ profileName,
51
+ isCI,
52
+ force: false,
53
+ });
54
+ }
55
+ const ensureLatest = ({ url, authToken, }) => Effect.gen(function* () {
56
+ const { matches, expected, observed } = yield* checkStateStoreVersion(url);
57
+ if (observed === undefined) {
58
+ const shouldDeploy = yield* Clank.confirm({
59
+ message: `Cloudflare State Store '${scriptName}' is not available. Do you want to deploy it?`,
60
+ });
61
+ if (shouldDeploy) {
62
+ return yield* bootstrap({
63
+ workerName: scriptName,
64
+ profile: profileName,
65
+ });
66
+ }
67
+ else {
68
+ return yield* Effect.die(new Clank.PromptCancelled());
69
+ }
70
+ }
71
+ const httpState = yield* ensureAccess({ url, authToken });
72
+ if (matches) {
73
+ return httpState;
74
+ }
75
+ else if (isCI) {
76
+ return yield* Effect.die(new AuthError({
77
+ message: `Cloudflare State store not found. Run 'alchemy bootstrap cloudflare --profile <your-ci-profile>' to deploy it first.`,
78
+ }));
79
+ }
80
+ else {
81
+ const shouldDeploy = yield* Clank.confirm({
82
+ message: `Cloudflare State Store '${scriptName}' is out of date ` +
83
+ `(expected v${expected}, observed v${observed ?? "unknown"})`,
84
+ });
85
+ if (shouldDeploy) {
86
+ const stateStoreOptions = yield* deployStateStore({
87
+ stage: scriptName,
88
+ state: httpState,
89
+ force: false,
90
+ });
91
+ return yield* makeCloudflareStateStore(stateStoreOptions);
92
+ }
93
+ else {
94
+ return yield* Effect.die(new Clank.PromptCancelled());
95
+ }
96
+ }
97
+ });
98
+ const ensureAccess = (credentials) => Effect.gen(function* () {
99
+ const isAuth = yield* checkHttpStateStoreAuth(credentials);
100
+ if (!isAuth) {
101
+ // our token is wrong, force a refresh
102
+ yield* Clank.info(`Cloudflare State store authentication failed, refreshing credentials...`);
103
+ const credentials = yield* loginWithCloudflare(profileName, true);
104
+ if (!(yield* checkHttpStateStoreAuth(credentials))) {
105
+ return yield* Effect.die(new AuthError({
106
+ message: `Cloudflare State store authentication failed, after refreshing credentials.`,
107
+ }));
108
+ }
109
+ return yield* makeCloudflareStateStore(credentials);
110
+ }
111
+ return yield* makeCloudflareStateStore(credentials);
112
+ });
113
+ const credentials = yield* credStore.read(profileName, CREDENTIALS_FILE);
114
+ if (credentials) {
115
+ return yield* ensureLatest(credentials);
116
+ }
117
+ const workerExists = yield* isStateStoreAvailable(scriptName);
118
+ if (workerExists) {
119
+ return yield* ensureLatest(yield* loginWithCloudflare(profileName, false));
120
+ }
121
+ else if (isCI) {
122
+ // TODO(sam): do we want to support bootstrapping the state store from CI?
123
+ // for now - just die here
124
+ return yield* Effect.die(new AuthError({
125
+ message: `Cloudflare State store not found. Run 'alchemy bootstrap cloudflare --profile <your-ci-profile>' to deploy it first.`,
126
+ }));
127
+ }
128
+ else {
129
+ return yield* Clank.confirm({
130
+ message: "Cloudflare State Store not found. Do you want to deploy it?",
131
+ }).pipe(Effect.flatMap((shouldDeploy) => shouldDeploy
132
+ ? bootstrap()
133
+ : Effect.die(new Clank.PromptCancelled())));
134
+ }
135
+ }).pipe(recordStateStoreInit, Effect.orDie);
136
+ return yield* Effect.cached(init.pipe(Effect.provideContext(context)));
137
+ })).pipe(Layer.provideMerge(Credentials.fromAuthProvider()), Layer.provideMerge(CloudflareEnvironment.fromProfile()), Layer.provideMerge(CloudflareAuth), Layer.provideMerge(Access.AccessLive), Layer.provideMerge(ProfileLive), Layer.provideMerge(CredentialsStoreLive), Layer.orDie);
138
+ export const bootstrap = (options = {}) => Effect.gen(function* () {
139
+ const isCI = yield* CI;
140
+ const profileName = options.profile ?? (yield* ALCHEMY_PROFILE);
89
141
  const scriptName = options.workerName ?? STATE_STORE_SCRIPT_NAME;
90
142
  const force = options.force ?? false;
143
+ const localStage = `${profileName}_${scriptName}`;
91
144
  yield* Effect.annotateCurrentSpan({
92
145
  "alchemy.state_store.script_name": scriptName,
93
146
  "alchemy.state_store.profile": profileName,
@@ -95,267 +148,76 @@ export const bootstrap = (options = {}) => Effect.gen(function* () {
95
148
  "alchemy.state_store.ci": isCI,
96
149
  });
97
150
  yield* annotateAccountHash();
98
- const localState = yield* makeLocalState();
99
- const hasLocalStack = yield* Effect.map(localState.listStages("CloudflareStateStore"), (stages) => stages.includes(scriptName));
100
- if (hasLocalStack) {
151
+ if (yield* hasLocalStack(localStage)) {
152
+ // if there's a local stack still, we can assume we did not finish hoisting it, so finish that
101
153
  yield* Clank.info(`Resuming Cloudflare State Store '${scriptName}' deployment...`);
102
- yield* finishBootstrap({
154
+ // resume deployment
155
+ return yield* deployWithLocalState({
103
156
  scriptName,
104
157
  profileName,
105
- localState,
106
158
  isCI,
107
159
  force,
108
- });
109
- yield* Clank.success(`Cloudflare State Store '${scriptName}' is ready.`);
110
- return;
160
+ }).pipe(Effect.tap(() => Clank.success(`Cloudflare State Store '${scriptName}' is ready.`)));
111
161
  }
112
- const { accountId } = yield* CloudflareEnvironment.CloudflareEnvironment;
113
- const workerExists = yield* workers
114
- .getScriptSetting({ accountId, scriptName })
115
- .pipe(Effect.map((setting) => setting !== undefined), Effect.catchTag("WorkerNotFound", () => Effect.succeed(false)));
116
- if (workerExists && !force) {
117
- yield* Clank.info(`Worker '${scriptName}' already exists; adopting and refreshing credentials. ` +
118
- `Use --force to redeploy.`);
119
- const credentials = yield* loginWithCloudflare();
162
+ const workerExists = yield* isStateStoreAvailable(scriptName);
163
+ if (workerExists) {
164
+ // this is a regular update, let's check if it needs an update and refresh credentials
165
+ if (!force) {
166
+ yield* Clank.info(`Worker '${scriptName}' already exists; adopting and refreshing credentials. ` +
167
+ `Use --force to redeploy.`);
168
+ }
169
+ const credentials = yield* loginWithCloudflare(profileName,
170
+ // force refresh during
171
+ true);
172
+ const { url, authToken } = credentials;
120
173
  if (!isCI) {
174
+ // we don't write credentials in CI because the file system is ephemeral
121
175
  const store = yield* CredentialsStore;
122
176
  yield* store.write(profileName, CREDENTIALS_FILE, credentials);
123
177
  }
124
- yield* redeployIfStale({
178
+ const { matches, expected, observed } = yield* checkStateStoreVersion(url);
179
+ const httpState = yield* makeCloudflareStateStore({ url, authToken });
180
+ if (!matches || force) {
181
+ if (matches && force) {
182
+ yield* Clank.info(`Cloudflare State Store '${scriptName}' is up to date; force redeploying...`);
183
+ }
184
+ else {
185
+ yield* Clank.info(`Cloudflare State Store '${scriptName}' is out of date ` +
186
+ `(expected v${expected}, observed v${observed ?? "unknown"}); redeploying...`);
187
+ }
188
+ return yield* makeCloudflareStateStore(yield* deployStateStore({
189
+ stage: scriptName,
190
+ state: httpState,
191
+ force,
192
+ }));
193
+ }
194
+ else {
195
+ return httpState;
196
+ }
197
+ }
198
+ else {
199
+ // fresh deploy - deploy from local for the first time
200
+ yield* Clank.info(`Deploying Cloudflare State Store '${scriptName}'...`);
201
+ return yield* deployWithLocalState({
125
202
  scriptName,
126
203
  profileName,
127
- localState,
128
204
  isCI,
129
- url: credentials.url,
130
205
  force,
131
- });
132
- yield* Clank.success(`Cloudflare State Store '${scriptName}' is ready.`);
133
- return;
134
- }
135
- if (workerExists) {
136
- yield* Clank.info(`Forcing redeploy of Cloudflare State Store '${scriptName}'...`);
206
+ }).pipe(Effect.tap(() => Clank.success(`Cloudflare State Store '${scriptName}' is ready.`)));
137
207
  }
138
- else {
139
- yield* Clank.info(`Deploying Cloudflare State Store '${scriptName}'...`);
140
- }
141
- yield* finishBootstrap({
142
- scriptName,
143
- profileName,
144
- localState,
145
- isCI,
146
- force,
147
- });
148
- yield* Clank.success(`Cloudflare State Store '${scriptName}' is ready.`);
149
208
  }).pipe(Effect.withSpan("state_store.bootstrap", {
150
209
  attributes: {
151
210
  "alchemy.state_store.op": "bootstrap",
152
211
  "alchemy.state_store.script_name": options.workerName ?? STATE_STORE_SCRIPT_NAME,
153
212
  },
154
213
  }));
155
- export const state = () => Layer.effect(State, Effect.gen(function* () {
156
- const isCI = yield* Config.boolean("CI").pipe(Config.withDefault(false));
157
- const alchemyContext = yield* AlchemyContext;
158
- const profileName = yield* ALCHEMY_PROFILE;
159
- const store = yield* CredentialsStore;
160
- const credentials = yield* store.read(profileName, CREDENTIALS_FILE);
161
- const scriptName = STATE_STORE_SCRIPT_NAME;
162
- yield* Effect.annotateCurrentSpan({
163
- "alchemy.state_store.script_name": scriptName,
164
- "alchemy.state_store.profile": profileName,
165
- "alchemy.state_store.ci": isCI,
166
- "alchemy.state_store.force": false,
167
- });
168
- yield* annotateAccountHash(yield* Config.boolean("NO_TRACK").pipe(Config.withDefault(false)));
169
- // The bootstrap of the Cloudflare State Store is only considered
170
- // successful once two invariants hold:
171
- // 1. the worker has been deployed and the credentials persisted
172
- // 2. the local CloudflareStateStore stack has been synced into
173
- // the HTTP store and removed from disk
174
- //
175
- // If a previous run failed anywhere between deploying the worker
176
- // and deleting the local stack (e.g. the worker got created but
177
- // sync/credential-write/delete never ran) we will detect the
178
- // leftover local stack here and resume the bootstrap. This loop
179
- // re-runs deploy (idempotent over already-applied resources) and
180
- // finishes the sync/credential/cleanup steps until both
181
- // invariants are satisfied.
182
- const localState = yield* makeLocalState();
183
- const hasLocalStack = yield* Effect.map(localState.listStages("CloudflareStateStore"), (stages) => stages.includes(scriptName));
184
- if (hasLocalStack) {
185
- yield* Clank.info(`Resuming Cloudflare State Store '${scriptName}' deployment...`);
186
- return yield* finishBootstrap({
187
- scriptName,
188
- profileName,
189
- localState,
190
- isCI,
191
- force: false,
192
- });
193
- }
194
- if (credentials) {
195
- // We have local credentials. Before trusting them, verify
196
- // the deployed worker is on the version this CLI was built
197
- // against. If it isn't, `redeployIfStale` runs the idempotent
198
- // bootstrap flow and short-circuits with the freshly-deployed
199
- // store.
200
- const fresh = yield* redeployIfStale({
201
- scriptName,
202
- profileName,
203
- localState,
204
- isCI,
205
- url: credentials.url,
206
- force: false,
207
- });
208
- if (fresh)
209
- return fresh;
210
- const httpState = yield* makeCloudflareStateStore(credentials);
211
- if (alchemyContext.updateStateStore) {
212
- yield* deployStateStore(scriptName, httpState);
213
- }
214
- return httpState;
215
- }
216
- // our profile does not contain a reference to the state store, let's try and resolve it
217
- const { accountId } = yield* CloudflareEnvironment.CloudflareEnvironment;
218
- const workerExists = yield* workers
219
- .getScriptSetting({
220
- accountId,
221
- scriptName,
222
- })
223
- .pipe(Effect.map((setting) => setting !== undefined), Effect.catchTag("WorkerNotFound", () => Effect.succeed(false)));
224
- if (workerExists) {
225
- // it exists, so fetch the secret token
226
- const credentials = yield* loginWithCloudflare();
227
- if (!isCI) {
228
- yield* store.write(profileName, CREDENTIALS_FILE, credentials);
229
- }
230
- const fresh = yield* redeployIfStale({
231
- scriptName,
232
- profileName,
233
- localState,
234
- isCI,
235
- url: credentials.url,
236
- force: false,
237
- });
238
- if (fresh)
239
- return fresh;
240
- const httpState = yield* makeCloudflareStateStore(credentials);
241
- if (alchemyContext.updateStateStore) {
242
- yield* deployStateStore(scriptName, httpState);
243
- }
244
- return httpState;
245
- }
246
- if (isCI) {
247
- // TODO(sam): do we want to support bootstrapping the state store from CI?
248
- // for now - just die here
249
- return yield* Effect.die(new AuthError({
250
- message: `State store not found for script ${scriptName}. Deploy the state store first.`,
251
- }));
252
- }
253
- const shouldDeploy = yield* Clank.confirm({
254
- message: "Cloudflare State Store not found. Do you want to deploy it?",
255
- });
256
- if (!shouldDeploy) {
257
- return yield* Effect.die(new Clank.PromptCancelled());
258
- }
259
- return yield* finishBootstrap({
260
- scriptName,
261
- profileName,
262
- localState,
263
- isCI,
264
- force: false,
265
- });
266
- }).pipe(recordStateStoreInit, Effect.orDie)).pipe(Layer.provideMerge(Credentials.fromAuthProvider()), Layer.provideMerge(CloudflareEnvironment.fromProfile()), Layer.provideMerge(CloudflareAuth), Layer.provideMerge(Access.AccessLive), Layer.provideMerge(ProfileLive), Layer.provideMerge(CredentialsStoreLive), Layer.orDie);
267
- const makeCloudflareStateStore = Effect.fnUntraced(function* ({ url, authToken, }) {
268
- const access = yield* Access.Access;
269
- const accessHeaders = yield* access.getAccessHeaders(new URL(url).host);
270
- return yield* makeHttpStateStore({
271
- url,
272
- authToken,
273
- transformClient: HttpClientRequest.setHeaders(accessHeaders),
274
- id: "cloudflare-http",
275
- });
276
- });
277
- /**
278
- * Non-destructively copy every resource in the
279
- * `CloudflareStateStore/<scriptName>` stack from `source` into
280
- * `destination`, leaving every other stack in `destination` untouched.
281
- *
282
- * This intentionally does not delete anything from `destination`: at
283
- * bootstrap time the destination is the user's live remote state
284
- * store, and removing entries that happen to be missing locally would
285
- * be catastrophic.
286
- */
287
- const hoistBootstrapStack = Effect.fnUntraced(function* (source, destination, scriptName) {
288
- const stack = "CloudflareStateStore";
289
- const stage = scriptName;
290
- const fqns = yield* source.list({ stack, stage });
291
- yield* Effect.annotateCurrentSpan({
292
- "alchemy.state_store.stack": stack,
293
- "alchemy.state_store.stage": stage,
294
- "alchemy.state_store.resources.count": fqns.length,
295
- });
296
- yield* Effect.forEach(fqns, Effect.fnUntraced(function* (fqn) {
297
- const value = yield* source.get({ stack, stage, fqn });
298
- if (value) {
299
- yield* destination.set({ stack, stage, fqn, value });
300
- }
301
- }), { concurrency: "unbounded" });
302
- }, Effect.withSpan("state_store.hoist_bootstrap_stack"));
303
- /**
304
- * Finish (or resume) the bootstrap of the Cloudflare State Store using
305
- * the provided local state as the source of truth. This is idempotent
306
- * and safe to re-run: any resources already applied during a previous
307
- * partial run will be reconciled, the local stack will be synced into
308
- * the deployed HTTP store, credentials persisted, and finally the
309
- * local stack removed - which is what marks the bootstrap as complete.
310
- */
311
- const finishBootstrap = ({ scriptName, profileName, localState, isCI, force, }) => Effect.gen(function* () {
312
- const { authToken } = yield* deployStateStore(scriptName, localState, force);
313
- // Don't trust the `authToken` returned by `deploy(...)`: when
314
- // adoption kicks in (the Secrets Store secret already existed),
315
- // the locally-generated `Random` value won't match the value
316
- // actually persisted in Cloudflare, and any HTTP call to the
317
- // worker would 401. Re-read the live token from the deployed
318
- // worker via the same edge-preview probe `loginWithCloudflare`
319
- // uses, so the credentials we persist always reflect what is
320
- // actually deployed. `loginWithCloudflare` also persists the
321
- // credentials file (skipping the write in CI), so we don't need
322
- // to do that explicitly here.
323
- const { url } = yield* loginWithCloudflare();
324
- const httpState = yield* makeCloudflareStateStore({ url, authToken });
325
- // `profileName` is intentionally unused here — `loginWithCloudflare`
326
- // resolves it itself. Reference it to keep the surrounding API
327
- // explicit and to avoid an unused-parameter lint.
328
- void profileName;
329
- void isCI;
330
- // Copy the freshly-deployed CloudflareStateStore stack into the
331
- // remote store. We deliberately do NOT call `syncState` here:
332
- // syncState is a *mirror* that deletes everything in the
333
- // destination that is not in the source, which would wipe every
334
- // user stack in the remote store on any subsequent bootstrap
335
- // (resume / --force / partial-failure recovery), since by that
336
- // point local state only contains the bootstrap stack itself.
337
- yield* hoistBootstrapStack(localState, httpState, scriptName);
338
- yield* localState.deleteStack({
339
- stack: "CloudflareStateStore",
340
- stage: scriptName,
341
- });
342
- return httpState;
343
- }).pipe(Effect.withSpan("state_store.finish_bootstrap", {
344
- attributes: {
345
- "alchemy.state_store.op": "finish_bootstrap",
346
- "alchemy.state_store.script_name": scriptName,
347
- "alchemy.state_store.profile": profileName,
348
- "alchemy.state_store.ci": isCI,
349
- },
350
- }));
351
- const deployStateStore = (scriptName, state, force) => Effect.gen(function* () {
214
+ const deployStateStore = ({ stage, state, force, }) => Effect.gen(function* () {
352
215
  yield* annotateAccountHash();
353
- const localState = state ?? (yield* makeLocalState());
354
216
  // deploy it with local state (which we will then hoist into the Cloudflare state store)
355
- const stateLayer = Layer.succeed(State, localState);
217
+ const stateLayer = Layer.succeed(State, Effect.succeed(state));
356
218
  const { url, authToken } = yield* deploy({
357
219
  // use the script name as the stage name (so the user can have multiple state stores)
358
- stage: scriptName,
220
+ stage,
359
221
  force,
360
222
  stack: Alchemy.Stack("CloudflareStateStore", {
361
223
  providers: Cloudflare.providers(),
@@ -392,13 +254,92 @@ const deployStateStore = (scriptName, state, force) => Effect.gen(function* () {
392
254
  // observe stale data or trip the staleness check and recurse into
393
255
  // another redeploy.
394
256
  yield* waitForStateStoreVersion(url);
395
- return { url, authToken, localState };
257
+ return { url, authToken };
396
258
  }).pipe(Effect.withSpan("state_store.deploy", {
397
259
  attributes: {
398
- "alchemy.state_store.script_name": scriptName,
399
260
  "alchemy.state_store.op": "deploy",
400
261
  },
401
262
  }), recordStateStoreOp("deploy"));
263
+ const deployWithLocalState = ({ scriptName, isCI, force, profileName, }) => Effect.gen(function* () {
264
+ const localState = yield* makeLocalState();
265
+ const localStage = `${profileName}_${scriptName}`;
266
+ const remoteStage = scriptName;
267
+ const { authToken } = yield* deployStateStore({
268
+ stage: localStage,
269
+ state: localState,
270
+ force,
271
+ });
272
+ const { url } = yield* loginWithCloudflare(profileName, force);
273
+ const httpState = yield* makeCloudflareStateStore({ url, authToken });
274
+ yield* hoistBootstrapStack({
275
+ source: {
276
+ state: localState,
277
+ stage: localStage,
278
+ },
279
+ destination: {
280
+ state: httpState,
281
+ stage: remoteStage,
282
+ },
283
+ });
284
+ yield* localState.deleteStack({
285
+ stack: "CloudflareStateStore",
286
+ stage: localStage,
287
+ });
288
+ return httpState;
289
+ }).pipe(Effect.withSpan("state_store.finish_bootstrap", {
290
+ attributes: {
291
+ "alchemy.state_store.op": "finish_bootstrap",
292
+ "alchemy.state_store.ci": isCI,
293
+ },
294
+ }));
295
+ // check if there's a local stack that wasn't properly hoisted
296
+ const hasLocalStack = (stage) => Effect.gen(function* () {
297
+ const localState = yield* makeLocalState();
298
+ return yield* Effect.map(localState.listStages("CloudflareStateStore"),
299
+ // key off the profile name to avoid conflicts with other profiles
300
+ (stages) => stages.includes(stage));
301
+ });
302
+ /**
303
+ * Non-destructively copy every resource in the
304
+ * `CloudflareStateStore/<scriptName>` stack from `source` into
305
+ * `destination`, leaving every other stack in `destination` untouched.
306
+ *
307
+ * This intentionally does not delete anything from `destination`: at
308
+ * bootstrap time the destination is the user's live remote state
309
+ * store, and removing entries that happen to be missing locally would
310
+ * be catastrophic.
311
+ */
312
+ const hoistBootstrapStack = Effect.fnUntraced(function* ({ source, destination, }) {
313
+ const stack = "CloudflareStateStore";
314
+ const fqns = yield* source.state.list({ stack, stage: source.stage });
315
+ yield* Effect.annotateCurrentSpan({
316
+ "alchemy.state_store.stack": stack,
317
+ "alchemy.state_store.stage": source.stage,
318
+ "alchemy.state_store.resources.count": fqns.length,
319
+ });
320
+ yield* Effect.forEach(fqns, Effect.fnUntraced(function* (fqn) {
321
+ const value = yield* source.state.get({
322
+ stack,
323
+ stage: source.stage,
324
+ fqn,
325
+ });
326
+ if (value) {
327
+ yield* destination.state
328
+ .set({
329
+ stack,
330
+ stage: destination.stage,
331
+ fqn,
332
+ value,
333
+ })
334
+ .pipe(Effect.retry({
335
+ while: (error) => isHttpClientError(error.cause) &&
336
+ // worker can 404 for a bit on first deploy, retry these
337
+ error.cause.response?.status === 404,
338
+ schedule: Schedule.fixed(200),
339
+ }));
340
+ }
341
+ }), { concurrency: "unbounded" });
342
+ }, Effect.withSpan("state_store.hoist_bootstrap_stack"));
402
343
  /**
403
344
  * Log in to a Cloudflare-deployed HTTP state-store.
404
345
  *
@@ -415,10 +356,17 @@ const deployStateStore = (scriptName, state, force) => Effect.gen(function* () {
415
356
  * `CloudflareEnvironment`, `Credentials`, `HttpClient`, and
416
357
  * `FileSystem`.
417
358
  */
418
- export const loginWithCloudflare = () => Effect.gen(function* () {
419
- const isCI = yield* Config.boolean("CI").pipe(Config.withDefault(false));
420
- const profileName = yield* ALCHEMY_PROFILE;
359
+ export const loginWithCloudflare = (profileName, force) => Effect.gen(function* () {
360
+ const credStore = yield* CredentialsStore;
361
+ const isCI = yield* CI;
421
362
  const { accountId } = yield* CloudflareEnvironment.CloudflareEnvironment;
363
+ if (!force) {
364
+ // try and read from the cached credentials first if not forcing (force will always refresh)
365
+ const credentials = yield* credStore.read(profileName, CREDENTIALS_FILE);
366
+ if (credentials) {
367
+ return credentials;
368
+ }
369
+ }
422
370
  // 1. Locate the single Secrets Store on the account.
423
371
  const stores = yield* SecretsStore.listStores({ accountId });
424
372
  const store = stores.result[0];
@@ -427,15 +375,7 @@ export const loginWithCloudflare = () => Effect.gen(function* () {
427
375
  message: "No Secrets Store found on this account. Deploy the state store first.",
428
376
  }));
429
377
  }
430
- // 2. Fetch the auth-token value via an edge-preview worker.
431
- // We piggy-back on the already-deployed state-store script so the
432
- // `cf-workers-preview-token` header has a real workers.dev route to
433
- // swap onto. Uploading the probe under a brand-new script name fails
434
- // on accounts where that script has never been deployed (or where
435
- // workers.dev preview URLs are off by default — the post-2024
436
- // Cloudflare default for new accounts) because the host doesn't
437
- // resolve and Cloudflare's edge serves a 400 HTML error page
438
- // instead of routing to the preview.
378
+ // 2. Fetch the auth-token from Secrets Store with a temporary edge-preview worker
439
379
  const authToken = yield* readSecretViaEdge(STATE_STORE_SCRIPT_NAME, store.id, AuthTokenSecretName).pipe(Effect.retry({
440
380
  while: isWorkersPreviewConfigurationError,
441
381
  schedule: Schedule.exponential(200).pipe(Schedule.both(Schedule.recurs(15))),
@@ -446,7 +386,6 @@ export const loginWithCloudflare = () => Effect.gen(function* () {
446
386
  if (!isCI) {
447
387
  // 4. Persist credentials. The profile entry is managed by
448
388
  // `loadOrConfigure` when this is invoked through `configure`.
449
- const credStore = yield* CredentialsStore;
450
389
  yield* credStore
451
390
  .write(profileName, CREDENTIALS_FILE, {
452
391
  url,
@@ -472,69 +411,21 @@ export const loginWithCloudflare = () => Effect.gen(function* () {
472
411
  "alchemy.state_store.script_name": STATE_STORE_SCRIPT_NAME,
473
412
  },
474
413
  }));
475
- const writeCredentials = (url, authToken) => Effect.gen(function* () {
476
- const profileName = yield* ALCHEMY_PROFILE;
477
- const credStore = yield* CredentialsStore;
478
- yield* credStore.write(profileName, CREDENTIALS_FILE, {
414
+ const isStateStoreAvailable = (scriptName = "alchemy-state-store") => Effect.gen(function* () {
415
+ // otherwise, the remote one might exist
416
+ const { accountId } = yield* CloudflareEnvironment.CloudflareEnvironment;
417
+ return yield* workers.getScriptSetting({ accountId, scriptName }).pipe(Effect.map((setting) => setting !== undefined), Effect.catchTag("WorkerNotFound", () => Effect.succeed(false)));
418
+ });
419
+ const makeCloudflareStateStore = Effect.fnUntraced(function* ({ url, authToken, }) {
420
+ const access = yield* Access.Access;
421
+ const accessHeaders = yield* access.getAccessHeaders(new URL(url).host);
422
+ return yield* makeHttpStateStore({
479
423
  url,
480
424
  authToken,
425
+ transformClient: HttpClientRequest.setHeaders(accessHeaders),
426
+ id: "cloudflare-http",
481
427
  });
482
428
  });
483
- const isWorkersPreviewConfigurationError = (error) => error instanceof EdgeSessionError &&
484
- (error.message.includes("Invalid Workers Preview configuration") ||
485
- error.message.includes("Error 1031"));
486
- /**
487
- * Run {@link finishBootstrap} iff the worker at `url` fails the
488
- * version probe; otherwise resolve to `undefined`. Centralises the
489
- * "is the deployed worker still compatible?" guard used by every
490
- * code path that already has a presumably-valid worker URL.
491
- */
492
- const redeployIfStale = ({ url, scriptName, profileName, localState, isCI, force, }) => Effect.gen(function* () {
493
- const { matches, expected, observed } = yield* checkStateStoreVersion(url);
494
- if (matches)
495
- return undefined;
496
- yield* Clank.info(`Cloudflare State Store '${scriptName}' is out of date ` +
497
- `(expected v${expected}, observed v${observed ?? "unknown"}); redeploying...`);
498
- return yield* finishBootstrap({
499
- scriptName,
500
- profileName,
501
- localState,
502
- isCI,
503
- force,
504
- });
505
- }).pipe(Effect.withSpan("state_store.redeploy_if_stale", {
506
- attributes: {
507
- "alchemy.state_store.op": "redeploy_if_stale",
508
- "alchemy.state_store.script_name": scriptName,
509
- "alchemy.state_store.url": url,
510
- },
511
- }));
512
- /**
513
- * Probe the deployed worker's `/version` endpoint and decide whether
514
- * it satisfies the current {@link STATE_STORE_VERSION} contract.
515
- *
516
- * Reuses the same typed {@link StateApi} client the rest of the store
517
- * speaks, just without the bearer-token transform — the `/version`
518
- * route is intentionally unauthenticated so we can probe it before
519
- * trusting any locally-cached credentials.
520
- *
521
- * Returns `true` only on a successful response carrying the matching
522
- * version. Any failure (404 from a pre-`/version` deploy, transport
523
- * error, schema mismatch, version mismatch) collapses to `false`,
524
- * since the caller's response in every case is the same: fall
525
- * through to the idempotent bootstrap flow.
526
- */
527
- /**
528
- * Block until the worker at `url` reports the {@link STATE_STORE_VERSION}
529
- * this CLI was built against, with bounded exponential retry. Used
530
- * post-deploy to wait out Cloudflare's edge cache so any subsequent
531
- * read or write goes to the new worker.
532
- *
533
- * Failures (transport errors, 404 from a pre-`/version` build, schema
534
- * mismatch, version mismatch) all collapse to "not ready yet" and are
535
- * retried; once the budget is exhausted we surface a hard failure
536
- * since continuing would talk to the wrong worker.
537
- */
538
429
  class StateStoreVersionNotReady extends Error {
539
430
  expected;
540
431
  observed;
@@ -565,13 +456,22 @@ const waitForStateStoreVersion = (url) => Effect.gen(function* () {
565
456
  }));
566
457
  const checkStateStoreVersion = (url) => Effect.gen(function* () {
567
458
  const client = yield* HttpApiClient.make(StateApi, { baseUrl: url });
459
+ const isAvailable = yield* Effect.cached(isStateStoreAvailable(STATE_STORE_SCRIPT_NAME));
568
460
  // The /version route may 404 transiently after a fresh deploy
569
461
  // while Cloudflare propagates the new script to the edge, and may
570
462
  // also surface transport-level blips on cold workers.dev hosts.
571
463
  // Retry the probe itself for ~10s before giving up — only after
572
464
  // exhausting that budget do we collapse to `undefined` and let
573
465
  // the caller treat it as a version mismatch.
574
- const result = yield* client.version.getVersion().pipe(Effect.retry({
466
+ const result = yield* client.version.getVersion().pipe(Effect.catchTag("HttpClientError", (e) =>
467
+ // if we get a 404 here, it means we assumed the worker shoudl exist, but it does not
468
+ // we should do a check to see if it does
469
+ e.response?.status === 404
470
+ ? isAvailable.pipe(Effect.flatMap((isAvailable) =>
471
+ // if the worker is available, then we should assume it was recently created and retry by propagating the error
472
+ // otherwise, return undefined (we don't know the version, there is no worker)
473
+ isAvailable ? Effect.fail(e) : Effect.succeed(undefined)))
474
+ : Effect.fail(e)), Effect.retry({
575
475
  schedule: Schedule.spaced("250 millis").pipe(Schedule.both(Schedule.recurs(40))),
576
476
  }), Effect.catch(() => Effect.succeed(undefined)));
577
477
  const matches = result?.version === STATE_STORE_VERSION;
@@ -654,4 +554,49 @@ const readSecretViaEdge = (scriptName, storeId, secretName) => Effect.gen(functi
654
554
  "alchemy.state_store.secret_name": secretName,
655
555
  },
656
556
  }));
557
+ const writeCredentials = (url, authToken) => Effect.gen(function* () {
558
+ const profileName = yield* ALCHEMY_PROFILE;
559
+ const credStore = yield* CredentialsStore;
560
+ yield* credStore.write(profileName, CREDENTIALS_FILE, {
561
+ url,
562
+ authToken,
563
+ });
564
+ });
565
+ const isWorkersPreviewConfigurationError = (error) => error instanceof EdgeSessionError &&
566
+ (error.message.includes("Invalid Workers Preview configuration") ||
567
+ error.message.includes("Error 1031"));
568
+ /**
569
+ * SHA-256 hex digest of the Cloudflare account ID. Used as a stable
570
+ * pseudonymous identifier on telemetry spans so the dashboard can
571
+ * count distinct state-store deployments without leaking the raw
572
+ * accountId. Mirrors the `alchemy.git.origin_hash` pattern in
573
+ * `Telemetry/Attributes.ts`.
574
+ */
575
+ const hashAccountId = (accountId) => Effect.sync(() => crypto.createHash("sha256").update(accountId).digest("hex"));
576
+ /**
577
+ * Best-effort Cloudflare-account-hash annotation on the current span.
578
+ * Resolves the accountId from {@link CloudflareEnvironment} and
579
+ * attaches `alchemy.cloudflare.account_hash` to whichever span is
580
+ * active. Silently no-ops if the environment isn't resolvable so
581
+ * State-store layer construction still succeeds in degraded paths.
582
+ *
583
+ * `noTrack` controls whether the hash is attached:
584
+ * - `true` — never annotate (caller-level opt-out).
585
+ * - `false` — always annotate, regardless of env.
586
+ * - `undefined` — fall back to the `NO_TRACK` env var; default off.
587
+ */
588
+ const annotateAccountHash = (noTrack) => Effect.gen(function* () {
589
+ if (noTrack === true)
590
+ return;
591
+ if (noTrack === undefined) {
592
+ const fromEnv = yield* Config.boolean("NO_TRACK").pipe(Config.withDefault(false));
593
+ if (fromEnv)
594
+ return;
595
+ }
596
+ const env = yield* Effect.serviceOption(CloudflareEnvironment.CloudflareEnvironment);
597
+ if (env._tag !== "Some")
598
+ return;
599
+ const hash = yield* hashAccountId(env.value.accountId);
600
+ yield* Effect.annotateCurrentSpan("alchemy.cloudflare.account_hash", hash);
601
+ }).pipe(Effect.catch(() => Effect.void));
657
602
  //# sourceMappingURL=State.js.map