8000 feat(cloudflare): allow binding individual secrets (#393) · sam-goodwin/alchemy@394833a · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Commit 394833a

Browse files
authored
feat(cloudflare): allow binding individual secrets (#393)
1 parent 8733a38 commit 394833a

File tree

6 files changed

+132
-66
lines changed

6 files changed

+132
-66
lines changed

alchemy/src/cloudflare/bindings.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ import type { HyperdriveResource } from "./hyperdrive.ts";
1818
import type { KVNamespaceResource } from "./kv-namespace.ts";
1919
import type { PipelineResource } from "./pipeline.ts";
2020
import type { QueueResource } from "./queue.ts";
21-
import type { SecretsStore } from "./secrets-store.ts";
2221
import type { VectorizeIndexResource } from "./vectorize-index.ts";
2322
import type { VersionMetadata } from "./version-metadata.ts";
2423
import type { WorkerStub } from "./worker-stub.ts";
2524
import type { Worker, WorkerRef } from "./worker.ts";
2625
import type { Workflow } from "./workflow.ts";
2726
import type { Images } from "./images.ts";
27+
import type { Secret as CloudflareSecret } from "./secret.ts";
2828

2929
export type Bindings = {
3030
[bindingName: string]: Binding;
@@ -43,6 +43,7 @@ export type Binding =
4343
| Ai
4444
| AiGatewayResource
4545
| Assets
46+
| CloudflareSecret
4647
| D1DatabaseResource
4748
| DispatchNamespaceResource
4849
| AnalyticsEngineDataset
@@ -53,7 +54,6 @@ export type Binding =
5354
| PipelineResource
5455
| QueueResource
5556
| R2BucketResource
56-
| SecretsStore<any>
5757
| {
5858
type: "kv_namespace";
5959
id: string;
@@ -107,6 +107,7 @@ export type WorkerBindingSpec =
107107
| WorkerBindingR2Bucket
108108
| WorkerBindingSecretText
109109
| WorkerBindingSecretsStore
110+
| WorkerBindingSecretsStoreSecret
110111
| WorkerBindingService
111112
| WorkerBindingStaticContent
112113
| WorkerBindingTailConsumer
@@ -313,6 +314,20 @@ export interface WorkerBindingSecretsStore {
313314
secret_name: string;
314315
}
315316

317+
/**
318+
* Secrets Store Secret binding type for individual secrets
319+
*/
320+
export interface WorkerBindingSecretsStoreSecret {
321+
/** The name of the binding */
322+
name: string;
323+
/** Type identifier for Secrets Store Secret binding */
324+
type: "secrets_store_secret";
325+
/** Store ID */
326+
store_id: string;
327+
/** Secret name */
328+
secret_name: string;
329+
}
330+
316331
/**
317332
* Service binding type
318333
*/

alchemy/src/cloudflare/bound.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Pipeline } from "cloudflare:pipelines";
22
import type { Secret } from "../secret.ts";
3+
import type { Secret as CloudflareSecret } from "./secret.ts";
34
import type { AiGatewayResource as _AiGateway } from "./ai-gateway.ts";
45
import type { Ai as _Ai } from "./ai.ts";
56
import type { AnalyticsEngineDataset as _AnalyticsEngineDataset } from "./analytics-engine.ts";
@@ -14,7 +15,6 @@ import type { HyperdriveResource as _Hyperdrive } from "./hyperdrive.ts";
1415
import type { Images as _Images } from "./images.ts";
1516
import type { PipelineResource as _Pipeline } from "./pipeline.ts";
1617
import type { QueueResource as _Queue } from "./queue.ts";
17-
import type { SecretsStore as _SecretsStore } from "./secrets-store.ts";
1818
import type { VectorizeIndexResource as _VectorizeIndex } from "./vectorize-index.ts";
1919
import type { VersionMetadata as _VersionMetadata } from "./version-metadata.ts";
2020
import type { Worker as _Worker, WorkerRef } from "./worker.ts";
@@ -45,20 +45,20 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace<
4545
? Hyperdrive
4646
: T extends Secret
4747
? string
48-
: T extends Assets
49-
? Service
50-
: T extends _Workflow<infer P>
51-
? Workflow<P>
52-
: T extends D1DatabaseResource
53-
? D1Database
54-
: T extends DispatchNamespaceResource
55-
? { get(name: string): Fetcher }
56-
: T extends _VectorizeIndex
57-
? VectorizeIndex
58-
: T extends _Queue<infer Body>
59-
? Queue<Body>
60-
: T extends _SecretsStore<infer S>
61-
? SecretsStoreBinding<S>
48+
: T extends CloudflareSecret
49+
? string
50+
: T extends Assets
51+
? Service
52+
: T extends _Workflow<infer P>
53+
? Workflow<P>
54+
: T extends D1DatabaseResource
55+
? D1Database
56+
: T extends DispatchNamespaceResource
57+
? { get(name: string): Fetcher }
58+
: T extends _VectorizeIndex
59+
? VectorizeIndex
60+
: T extends _Queue<infer Body>
61+
? Queue<Body>
6262
: T extends _AnalyticsEngineDataset
6363
? AnalyticsEngineDataset
6464
: T extends _Pipeline<infer R>
@@ -78,11 +78,3 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace<
7878
: T extends Json<infer T>
7979
? T
8080
: Service;
81-
82-
interface SecretsStoreBinding<
83-
S extends Record<string, Secret> | undefined = undefined,
84-
> {
85-
get(
86-
key: (S extends Record<string, any> ? keyof S : never) | (string & {}),
87-
): Promise<string>;
88-
}

alchemy/src/cloudflare/secret.ts

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export function isSecret(resource: Resource): resource is Secret {
7474
export interface Secret
7575
extends Resource<"cloudflare::Secret">,
7676
Omit<_SecretProps, "delete"> {
77+
/**
78+
* The binding type for Cloudflare Workers
79+
*/
80+
type: "secrets_store_secret";
81+
7782
/**
7883
* The name of the secret
7984
*/
@@ -190,6 +195,7 @@ const _Secret = Resource(
190195
await insertSecret(api, props.store.id, name, props.value);
191196

192197
return this({
198+
type: "secrets_store_secret",
193199
name,
194200
storeId: props.store.id,
195201
store: props.store,
@@ -209,24 +215,98 @@ export async function insertSecret(
209215
secretName: string,
210216
secretValue: AlchemySecret,
211217
): Promise<void> {
212-
const response = await api.post(
218+
// First try to create the secret
219+
const createResponse = await api.post(
213220
`/accounts/${api.accountId}/secrets_store/stores/${storeId}/secrets`,
214221
[
215222
{
216223
name: secretName,
217224
value: secretValue.unencrypted,
218-
scopes: [],
225+
scopes: ["workers"],
219226
},
220227
],
221228
);
222229

230+
if (createResponse.ok) {
231+
return; // Secret created successfully
232+
}
233+
234+
// If creation failed, check if it's because the secret already exists
235+
const createErrorData: any = await createResponse.json().catch(() => ({
236+
errors: [{ message: createResponse.statusText }],
237+
}));
238+
239+
const isAlreadyExists = createErrorData.errors?.some(
240+
(error: any) =>
241+
error.code === 10021 ||
242+
error.message?.includes("secret_name_already_exists"),
243+
);
244+
245+
if (isAlreadyExists) {
246+
// Secret already exists, find its ID and update it
247+
const secretId = await getSecretId(api, storeId, secretName);
248+
if (!secretId) {
249+
throw new Error(`Secret '${secretName}' not found in store`);
250+
}
251+
252+
const updateResponse = await api.patch(
253+
`/accounts/${api.accountId}/secrets_store/stores/${storeId}/secrets/${secretId}`,
254+
{
255+
value: secretValue.unencrypted,
256+
scopes: ["workers"],
257+
},
258+
);
259+
260+
if (!updateResponse.ok) {
261+
const updateErrorData: any = await updateResponse.json().catch(() => ({
262+
errors: [{ message: updateResponse.statusText }],
263+
}));
264+
const updateErrorMessage =
265+
updateErrorData.errors?.[0]?.message || updateResponse.statusText;
266+
throw new Error(
267+
`Error updating secret '${secretName}': ${updateErrorMessage}`,
268+
);
269+
}
270+
} else {
271+
// Some other error occurred during creation
272+
const createErrorMessage =
273+
createErrorData.errors?.[0]?.message || createResponse.statusText;
274+
throw new Error(
275+
`Error creating secret '${secretName}': ${createErrorMessage}`,
276+
);
277+
}
278+
}
279+
280+
/**
281+
* Get the ID of a secret by its name
282+
*/
283+
async function getSecretId(
284+
api: CloudflareApi,
285+
storeId: string,
286+
secretName: string,
287+
): Promise<string | null> {
288+
const response = await api.get(
289+
`/accounts/${api.accountId}/secrets_store/stores/${storeId}/secrets`,
290+
);
291+
223292
if (!response.ok) {
224-
const errorData: any = await response.json().catch(() => ({
225-
errors: [{ message: response.statusText }],
226-
}));
227-
const errorMessage = errorData.errors?.[0]?.message || response.statusText;
228-
throw new Error(`Error creating secret '${secretName}': ${errorMessage}`);
293+
return null;
229294
}
295+
296+
const data = (await response.json()) as {
297+
result: Array<{
298+
id: string;
299+
name: string;
300+
created: string;
301+
modified: string;
302+
status: string;
303+
}>;
304+
success: boolean;
305+
errors: any[];
306+
};
307+
308+
const secret = data.result.find((s) => s.name === secretName);
309+
return secret?.id || null;
230310
}
231311

232312
/**

alchemy/src/cloudflare/secrets-store.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import type { Context } from "../context.ts";
22
import { Resource, ResourceKind } from "../resource.ts";
3-
import { bind } from "../runtime/bind.ts";
43
import { secret, type Secret } from "../secret.ts";
54
import { handleApiError } from "./api-error.ts";
65
import {
76
createCloudflareApi,
87
type CloudflareApi,
98
type CloudflareApiOptions,
109
} from "./api.ts";
11-
import type { Bound } from "./bound.ts";
1210

1311
/**
1412
* Properties for creating or updating a Secrets Store
@@ -66,11 +64,6 @@ export interface SecretsStore<
6664
S extends Record<string, Secret> | undefined = undefined,
6765
> extends Resource<"cloudflare::SecretsStore">,
6866
Omit<SecretsStoreProps<S>, "delete"> {
69-
/**
70-
* The binding type for Cloudflare Workers
71-
*/
72-
type: "secrets_store";
73-
7467
/**
7568
* The unique identifier of the secrets store
7669
*/
@@ -98,10 +91,6 @@ export interface SecretsStore<
9891
modifiedAt: number;
9992
}
10093

101-
export type SecretsStoreWithBinding<
102-
S extends Record<string, Secret> | undefined = undefined,
103-
> = SecretsStore<S> & Bound<SecretsStore<S>>;
104-
10594
/**
10695
* A Cloudflare Secrets Store is a secure, centralized location for storing account-level secrets.
10796
*
@@ -162,7 +151,7 @@ export async function SecretsStore<
162151
>(
163152
name: string,
164153
props: SecretsStoreProps<S> = {} as SecretsStoreProps<S>,
165-
): Promise<SecretsStoreWithBinding<S>> {
154+
): Promise<SecretsStore<S>> {
166155
// Convert string values to Secret instances
167156
const normalizedProps: SecretsStoreProps<S> = {
168157
...props,
@@ -176,12 +165,7 @@ export async function SecretsStore<
176165
: undefined,
177166
};
178167

179-
const store = await _SecretsStore(name, normalizedProps);
180-
const binding = await bind(store);
181-
return {
182-
...store,
183-
get: binding.get,
184-
} as SecretsStoreWithBinding<S>;
168+
return _SecretsStore(name, normalizedProps);
185169
}
186170

187171
const _SecretsStore = Resource("cloudflare::SecretsStore", async function <
@@ -254,7 +238,6 @@ const _SecretsStore = Resource("cloudflare::SecretsStore", async function <
254238
}
255239

256240
return this({
257-
type: "secrets_store",
258241
id: storeId,
259242
name: name,
260243
secrets: props.secrets as S,
@@ -345,7 +328,7 @@ export async function insertSecrets<
345328
const bulkPayload = batch.map(([name, secretValue]) => ({
346329
name,
347330
value: (secretValue as Secret).unencrypted,
348-
scopes: [],
331+
scopes: ["workers"],
349332
}));
350333

351334
const bulkResponse = await api.post(

alchemy/src/cloudflare/worker-metadata.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,12 @@ export async function prepareWorkerMetadata<B extends Bindings>(
387387
name: bindingName,
388388
bucket_name: binding.name,
389389
});
390-
} else if (binding.type === "secrets_store") {
390+
} else if (binding.type === "secrets_store_secret") {
391391
meta.bindings.push({
392-
type: "secrets_store",
392+
type: "secrets_store_secret",
393393
name: bindingName,
394-
store_id: binding.id,
395-
secret_name: bindingName,
394+
store_id: binding.storeId,
395+
secret_name: binding.name,
396396
});
397397
} else if (binding.type === "assets") {
398398
meta.bindings.push({

alchemy/src/cloudflare/wrangler.json.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -596,16 +596,12 @@ function processBindings(
596596
});
597597
} else if (binding.type === "json") {
598598
// TODO(sam): anything to do here? not sure wrangler.json supports this
599-
} else if (binding.type === "secrets_store") {
600-
if (binding.secrets) {
601-
for (const [secretName, _secret] of Object.entries(binding.secrets)) {
602-
secretsStoreSecrets.push({
603-
binding: bindingName,
604-
store_id: binding.id,
605-
secret_name: secretName,
606-
});
607-
}
608-
}
599+
} else if (binding.type === "secrets_store_secret") {
600+
secretsStoreSecrets.push({
601+
binding: bindingName,
602+
store_id: binding.storeId,
603+
secret_name: binding.name,
604+
});
609605
} else if (binding.type === "dispatch_namespace") {
610606
dispatchNamespaces.push({
611607
binding: bindingName,

0 commit comments

Comments
 (0)
0