Skip to content

Commit 93267cf

Browse files
edmundhungpenalosaCarmenPopoviciu
authored
Secrets Store Secret binding local dev (#8394)
* feat(miniflare): secret store plugin * feat(wrangler): secret store setup * chore: add secret-store fixture * change name * generate docs * rebase on main * finalise * fix lint * unskip e2e * fix tests * Create calm-grapes-eat.md * lint * lint * fix tests * fix lint * Address comments * Apply suggestions from code review Co-authored-by: Carmen Popoviciu <[email protected]> * fix snapshots * fix lockfile --------- Co-authored-by: Samuel Macleod <[email protected]> Co-authored-by: Carmen Popoviciu <[email protected]>
1 parent 75b454c commit 93267cf

File tree

34 files changed

+1171
-179
lines changed

34 files changed

+1171
-179
lines changed

.changeset/calm-grapes-eat.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"miniflare": minor
3+
"wrangler": minor
4+
---
5+
6+
Support Secrets Store Secret bindings
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "secret-store",
3+
"private": true,
4+
"scripts": {
5+
"deploy": "wrangler deploy",
6+
"start": "wrangler dev"
7+
},
8+
"devDependencies": {
9+
"@cloudflare/workers-types": "^4.20250224.0",
10+
"wrangler": "workspace:*"
11+
},
12+
"volta": {
13+
"extends": "../../package.json"
14+
}
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export interface Env {
2+
SECRET: any;
3+
}
4+
5+
export default {
6+
async fetch(
7+
request: Request,
8+
env: Env,
9+
ctx: ExecutionContext
10+
): Promise<Response> {
11+
try {
12+
const value = await env.SECRET.get();
13+
return new Response(value);
14+
} catch (e) {
15+
return new Response(
16+
e instanceof Error ? e.message : "Something went wrong",
17+
{
18+
status: 404,
19+
}
20+
);
21+
}
22+
},
23+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2021",
4+
"lib": ["es2021"],
5+
"module": "es2022",
6+
"types": ["@cloudflare/workers-types/experimental"],
7+
"noEmit": true,
8+
"isolatedModules": true,
9+
"forceConsistentCasingInFileNames": true,
10+
"strict": true,
11+
"skipLibCheck": true
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "./node_modules/wrangler/config-schema.json",
3+
"compatibility_date": "2025-01-01",
4+
"main": "src/index.ts",
5+
"name": "secret-store",
6+
"secrets_store_secrets": [
7+
{
8+
"binding": "SECRET",
9+
"secret_name": "secret_name",
10+
"store_id": "a3fb907f446e4d94bd946d7ea6365b1c",
11+
},
12+
],
13+
}

fixtures/worker-ts/wrangler.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
name = "worker-ts"
22
main = "src/index.ts"
33
compatibility_date = "2023-05-04"
4+

packages/miniflare/src/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
QueuesError,
5757
R2_PLUGIN_NAME,
5858
ReplaceWorkersTypes,
59+
SECRET_STORE_PLUGIN_NAME,
5960
SERVICE_ENTRY,
6061
SharedOptions,
6162
SOCKET_ENTRY,
@@ -109,13 +110,15 @@ import {
109110
SharedHeaders,
110111
SiteBindings,
111112
} from "./workers";
113+
import { ADMIN_API } from "./workers/secrets-store/constants";
112114
import { formatZodError } from "./zod-format";
113115
import type {
114116
CacheStorage,
115117
D1Database,
116118
DurableObjectNamespace,
117119
Fetcher,
118120
KVNamespace,
121+
KVNamespaceListKey,
119122
Queue,
120123
R2Bucket,
121124
} from "@cloudflare/workers-types/experimental";
@@ -1912,6 +1915,34 @@ export class Miniflare {
19121915
): Promise<ReplaceWorkersTypes<KVNamespace>> {
19131916
return this.#getProxy(KV_PLUGIN_NAME, bindingName, workerName);
19141917
}
1918+
getSecretsStoreSecretAPI(
1919+
bindingName: string,
1920+
workerName?: string
1921+
): Promise<
1922+
() => {
1923+
create: (value: string) => Promise<string>;
1924+
update: (value: string, id: string) => Promise<string>;
1925+
duplicate: (id: string, newName: string) => Promise<string>;
1926+
delete: (id: string) => Promise<void>;
1927+
list: () => Promise<KVNamespaceListKey<{ uuid: string }, string>[]>;
1928+
get: (id: string) => Promise<string>;
1929+
}
1930+
> {
1931+
return this.#getProxy(
1932+
SECRET_STORE_PLUGIN_NAME,
1933+
bindingName,
1934+
workerName
1935+
).then((binding) => {
1936+
// @ts-expect-error We exposed an admin API on this key
1937+
return binding[ADMIN_API];
1938+
});
1939+
}
1940+
getSecretsStoreSecret(
1941+
bindingName: string,
1942+
workerName?: string
1943+
): Promise<ReplaceWorkersTypes<KVNamespace>> {
1944+
return this.#getProxy(SECRET_STORE_PLUGIN_NAME, bindingName, workerName);
1945+
}
19151946
getQueueProducer<Body = unknown>(
19161947
bindingName: string,
19171948
workerName?: string

packages/miniflare/src/plugins/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { PIPELINE_PLUGIN, PIPELINES_PLUGIN_NAME } from "./pipelines";
1212
import { QUEUES_PLUGIN, QUEUES_PLUGIN_NAME } from "./queues";
1313
import { R2_PLUGIN, R2_PLUGIN_NAME } from "./r2";
1414
import { RATELIMIT_PLUGIN, RATELIMIT_PLUGIN_NAME } from "./ratelimit";
15+
import { SECRET_STORE_PLUGIN, SECRET_STORE_PLUGIN_NAME } from "./secret-store";
1516
import { WORKFLOWS_PLUGIN, WORKFLOWS_PLUGIN_NAME } from "./workflows";
1617

1718
export const PLUGINS = {
@@ -27,6 +28,7 @@ export const PLUGINS = {
2728
[ASSETS_PLUGIN_NAME]: ASSETS_PLUGIN,
2829
[WORKFLOWS_PLUGIN_NAME]: WORKFLOWS_PLUGIN,
2930
[PIPELINES_PLUGIN_NAME]: PIPELINE_PLUGIN,
31+
[SECRET_STORE_PLUGIN_NAME]: SECRET_STORE_PLUGIN,
3032
};
3133
export type Plugins = typeof PLUGINS;
3234

@@ -76,15 +78,17 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
7678
z.input<typeof RATELIMIT_PLUGIN.options> &
7779
z.input<typeof ASSETS_PLUGIN.options> &
7880
z.input<typeof WORKFLOWS_PLUGIN.options> &
79-
z.input<typeof PIPELINE_PLUGIN.options>;
81+
z.input<typeof PIPELINE_PLUGIN.options> &
82+
z.input<typeof SECRET_STORE_PLUGIN.options>;
8083

8184
export type SharedOptions = z.input<typeof CORE_PLUGIN.sharedOptions> &
8285
z.input<typeof CACHE_PLUGIN.sharedOptions> &
8386
z.input<typeof D1_PLUGIN.sharedOptions> &
8487
z.input<typeof DURABLE_OBJECTS_PLUGIN.sharedOptions> &
8588
z.input<typeof KV_PLUGIN.sharedOptions> &
8689
z.input<typeof R2_PLUGIN.sharedOptions> &
87-
z.input<typeof WORKFLOWS_PLUGIN.sharedOptions>;
90+
z.input<typeof WORKFLOWS_PLUGIN.sharedOptions> &
91+
z.input<typeof SECRET_STORE_PLUGIN.sharedOptions>;
8892

8993
export const PLUGIN_ENTRIES = Object.entries(PLUGINS) as [
9094
keyof Plugins,
@@ -134,3 +138,4 @@ export * from "./assets";
134138
export * from "./assets/schema";
135139
export * from "./workflows";
136140
export * from "./pipelines";
141+
export * from "./secret-store";
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import SCRIPT_SECRETS_STORE_SECRET from "worker:secrets-store/secret";
2+
import { z } from "zod";
3+
import { ServiceDesignator, Worker_Binding } from "../../runtime";
4+
import { KV_PLUGIN, KVOptionsSchema } from "../kv";
5+
import { PersistenceSchema, Plugin, ProxyNodeBinding } from "../shared";
6+
7+
const SecretsStoreSecretsSchema = z.record(
8+
z.object({
9+
store_id: z.string(),
10+
secret_name: z.string(),
11+
})
12+
);
13+
14+
export const SecretsStoreSecretsOptionsSchema = z.object({
15+
secretsStoreSecrets: SecretsStoreSecretsSchema.optional(),
16+
});
17+
18+
export const SecretsStoreSecretsSharedOptionsSchema = z.object({
19+
secretsStorePersist: PersistenceSchema,
20+
});
21+
22+
export const SECRET_STORE_PLUGIN_NAME = "secrets-store";
23+
24+
function getkvNamespacesOptions(
25+
secretsStoreSecrets: z.input<typeof SecretsStoreSecretsSchema>
26+
): z.input<typeof KVOptionsSchema> {
27+
// Get unique store ids
28+
const storeIds = new Set(
29+
Object.values(secretsStoreSecrets).map((store) => store.store_id)
30+
);
31+
// Setup a KV Namespace per store id with store id as the binding name
32+
const storeIdKvNamespaceEntries = Array.from(storeIds).map((storeId) => [
33+
storeId,
34+
`${SECRET_STORE_PLUGIN_NAME}:${storeId}`,
35+
]);
36+
37+
return {
38+
kvNamespaces: Object.fromEntries(storeIdKvNamespaceEntries),
39+
};
40+
}
41+
42+
function isKvBinding(
43+
binding: Worker_Binding
44+
): binding is Worker_Binding & { kvNamespace: ServiceDesignator } {
45+
return "kvNamespace" in binding;
46+
}
47+
48+
export const SECRET_STORE_PLUGIN: Plugin<
49+
typeof SecretsStoreSecretsOptionsSchema,
50+
typeof SecretsStoreSecretsSharedOptionsSchema
51+
> = {
52+
options: SecretsStoreSecretsOptionsSchema,
53+
sharedOptions: SecretsStoreSecretsSharedOptionsSchema,
54+
async getBindings(options) {
55+
if (!options.secretsStoreSecrets) {
56+
return [];
57+
}
58+
59+
const bindings = Object.entries(
60+
options.secretsStoreSecrets
61+
).map<Worker_Binding>(([name, config]) => {
62+
return {
63+
name,
64+
service: {
65+
name: `${SECRET_STORE_PLUGIN_NAME}:${config.store_id}:${config.secret_name}`,
66+
entrypoint: "SecretsStoreSecret",
67+
},
68+
};
69+
});
70+
return bindings;
71+
},
72+
getNodeBindings(options: z.infer<typeof SecretsStoreSecretsOptionsSchema>) {
73+
if (!options.secretsStoreSecrets) {
74+
return {};
75+
}
76+
return Object.fromEntries(
77+
Object.keys(options.secretsStoreSecrets).map((name) => [
78+
name,
79+
new ProxyNodeBinding(),
80+
])
81+
);
82+
},
83+
async getServices({ options, sharedOptions, ...restOptions }) {
84+
if (!options.secretsStoreSecrets) {
85+
return [];
86+
}
87+
88+
const kvServices = await KV_PLUGIN.getServices({
89+
options: getkvNamespacesOptions(options.secretsStoreSecrets),
90+
sharedOptions: {
91+
kvPersist: sharedOptions.secretsStorePersist,
92+
},
93+
...restOptions,
94+
});
95+
96+
const kvBindings = await KV_PLUGIN.getBindings(
97+
getkvNamespacesOptions(options.secretsStoreSecrets),
98+
restOptions.workerIndex
99+
);
100+
101+
if (!kvBindings || !kvBindings.every(isKvBinding)) {
102+
throw new Error(
103+
"Expected KV plugin to return bindings with kvNamespace defined"
104+
);
105+
}
106+
107+
if (!Array.isArray(kvServices)) {
108+
throw new Error("Expected KV plugin to return an array of services");
109+
}
110+
111+
return [
112+
...kvServices,
113+
...Object.entries(options.secretsStoreSecrets).map<Worker_Binding>(
114+
([_, config]) => {
115+
return {
116+
name: `${SECRET_STORE_PLUGIN_NAME}:${config.store_id}:${config.secret_name}`,
117+
worker: {
118+
compatibilityDate: "2025-01-01",
119+
modules: [
120+
{
121+
name: "secret.worker.js",
122+
esModule: SCRIPT_SECRETS_STORE_SECRET(),
123+
},
124+
],
125+
bindings: [
126+
{
127+
name: "store",
128+
kvNamespace: kvBindings.find(
129+
// Look up the corresponding KV namespace for the store id
130+
(binding) => binding.name === config.store_id
131+
)?.kvNamespace,
132+
},
133+
{
134+
name: "secret_name",
135+
json: JSON.stringify(config.secret_name),
136+
},
137+
],
138+
},
139+
};
140+
}
141+
),
142+
];
143+
},
144+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const ADMIN_API = "SecretsStoreSecret::admin_api";

0 commit comments

Comments
 (0)