Skip to content

Commit aad674b

Browse files
committed
feat: Add Secrets Store integration with Container helper methods
- Add SecretsStoreBinding interface and secretsStoreBindings property to ContainerOptions - Implement Secrets Store helper methods following established KV/R2 patterns: - setupSecretsStoreBindingEnvironment(): Generate environment variables - getSecretsStoreBindingInfo(): Detailed binding information - validateSecretsStoreBindingEnvironment(): Validation with error reporting - getSecretsStoreBindingSummary(): Concise summary for logging - autoDetectSecretsStoreBindings(): Auto-detection from environment - Integrate Secrets Store environment variables into container start configuration - Add comprehensive test coverage with Jest configuration - Install @types/jest dependency and create proper test mocks - Add 17 comprehensive tests covering all Secrets Store functionality - Follow established UX patterns for minimal user code
1 parent feab875 commit aad674b

File tree

7 files changed

+638
-12
lines changed

7 files changed

+638
-12
lines changed

jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ module.exports = {
66
transform: {
77
'^.+\\.ts$': ['ts-jest', { tsconfig: 'tsconfig.json' }]
88
},
9+
moduleNameMapper: {
10+
'^cloudflare:workers$': '<rootDir>/src/tests/__mocks__/cloudflare-workers.js'
11+
},
12+
setupFilesAfterEnv: ['<rootDir>/src/tests/setup.ts'],
913
collectCoverage: true,
1014
coverageDirectory: 'coverage',
1115
coverageReporters: ['text', 'lcov'],

package-lock.json

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/container.ts

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
WaitOptions,
1010
CancellationOptions,
1111
StartAndWaitForPortsOptions,
12+
SecretsStoreBinding,
1213
} from '../types';
1314
import { generateId, parseTimeExpression } from './helpers';
1415
import { DurableObject } from 'cloudflare:workers';
@@ -232,6 +233,7 @@ export class Container<Env = unknown> extends DurableObject<Env> {
232233
envVars: ContainerStartOptions['env'] = {};
233234
entrypoint: ContainerStartOptions['entrypoint'];
234235
enableInternet: ContainerStartOptions['enableInternet'] = true;
236+
secretsStoreBindings: SecretsStoreBinding[] = [];
235237

236238
// =========================
237239
// PUBLIC INTERFACE
@@ -261,6 +263,12 @@ export class Container<Env = unknown> extends DurableObject<Env> {
261263
if (options) {
262264
if (options.defaultPort !== undefined) this.defaultPort = options.defaultPort;
263265
if (options.sleepAfter !== undefined) this.sleepAfter = options.sleepAfter;
266+
if (options.secretsStoreBindings) this.secretsStoreBindings = options.secretsStoreBindings;
267+
}
268+
269+
// Auto-detect Secrets Store bindings if not explicitly provided
270+
if (this.secretsStoreBindings.length === 0) {
271+
this.secretsStoreBindings = this.autoDetectSecretsStoreBindings(env);
264272
}
265273

266274
// Create schedules table if it doesn't exist
@@ -912,7 +920,11 @@ export class Container<Env = unknown> extends DurableObject<Env> {
912920
enableInternet,
913921
};
914922

915-
if (envVars && Object.keys(envVars).length > 0) startConfig.env = envVars;
923+
// Merge Secrets Store environment variables with existing environment variables
924+
const secretsStoreEnvVars = this.setupSecretsStoreBindingEnvironment();
925+
const mergedEnvVars = { ...secretsStoreEnvVars, ...envVars };
926+
927+
if (Object.keys(mergedEnvVars).length > 0) startConfig.env = mergedEnvVars;
916928
if (entrypoint) startConfig.entrypoint = entrypoint;
917929

918930
this.renewActivityTimeout();
@@ -1289,6 +1301,155 @@ export class Container<Env = unknown> extends DurableObject<Env> {
12891301
return this.toSchedule(schedule);
12901302
}
12911303

1304+
// ==========================
1305+
// SECRETS STORE BINDING HELPERS
1306+
// ==========================
1307+
1308+
/**
1309+
* Generate environment variables for Secrets Store bindings
1310+
* Creates SECRETS_{BINDING_NAME}_BINDING, SECRETS_{BINDING_NAME}_STORE_ID, and SECRETS_{BINDING_NAME}_SECRET_NAME environment variables
1311+
* @private
1312+
*/
1313+
private setupSecretsStoreBindingEnvironment(): Record<string, string> {
1314+
const secretsEnvVars: Record<string, string> = {};
1315+
for (const binding of this.secretsStoreBindings) {
1316+
const envPrefix = binding.binding.toUpperCase().replace(/[^A-Z0-9]/g, '_');
1317+
secretsEnvVars[`SECRETS_${envPrefix}_BINDING`] = binding.binding;
1318+
secretsEnvVars[`SECRETS_${envPrefix}_STORE_ID`] = binding.storeId;
1319+
secretsEnvVars[`SECRETS_${envPrefix}_SECRET_NAME`] = binding.secretName;
1320+
}
1321+
return secretsEnvVars;
1322+
}
1323+
1324+
/**
1325+
* Get detailed information about configured Secrets Store bindings
1326+
* @public
1327+
*/
1328+
public getSecretsStoreBindingInfo(): Record<string, { binding: string; storeId: string; secretName: string; envVars: Record<string, string> }> {
1329+
const bindingInfo: Record<string, any> = {};
1330+
for (const binding of this.secretsStoreBindings) {
1331+
const envPrefix = binding.binding.toUpperCase().replace(/[^A-Z0-9]/g, '_');
1332+
const envVars: Record<string, string> = {
1333+
[`SECRETS_${envPrefix}_BINDING`]: binding.binding,
1334+
[`SECRETS_${envPrefix}_STORE_ID`]: binding.storeId,
1335+
[`SECRETS_${envPrefix}_SECRET_NAME`]: binding.secretName
1336+
};
1337+
1338+
bindingInfo[binding.binding] = {
1339+
binding: binding.binding,
1340+
storeId: binding.storeId,
1341+
secretName: binding.secretName,
1342+
envVars
1343+
};
1344+
}
1345+
return bindingInfo;
1346+
}
1347+
1348+
/**
1349+
* Validate Secrets Store binding environment variables are properly set
1350+
* @public
1351+
*/
1352+
public validateSecretsStoreBindingEnvironment(): { valid: boolean; bindings: Record<string, any>; errors: string[] } {
1353+
const errors: string[] = [];
1354+
const bindings: Record<string, any> = {};
1355+
1356+
// Get all Secrets Store environment variables from process.env
1357+
const secretsVars: Record<string, string> = {};
1358+
for (const [key, value] of Object.entries(process.env)) {
1359+
if (key.startsWith('SECRETS_') && value) {
1360+
secretsVars[key] = value;
1361+
}
1362+
}
1363+
1364+
// Group Secrets Store variables by binding name
1365+
const groupedBindings: Record<string, Record<string, string>> = {};
1366+
for (const [key, value] of Object.entries(secretsVars)) {
1367+
const match = key.match(/^SECRETS_([A-Z0-9_]+)_(BINDING|STORE_ID|SECRET_NAME)$/);
1368+
if (match) {
1369+
const bindingName = match[1];
1370+
const varType = match[2];
1371+
if (!groupedBindings[bindingName]) {
1372+
groupedBindings[bindingName] = {};
1373+
}
1374+
groupedBindings[bindingName][varType] = value;
1375+
}
1376+
}
1377+
1378+
// Check each configured binding
1379+
for (const binding of this.secretsStoreBindings) {
1380+
const envPrefix = binding.binding.toUpperCase().replace(/[^A-Z0-9]/g, '_');
1381+
const foundBinding = groupedBindings[envPrefix];
1382+
1383+
if (!foundBinding) {
1384+
errors.push(`Secrets Store binding '${binding.binding}' not found in environment variables`);
1385+
continue;
1386+
}
1387+
1388+
if (!foundBinding.BINDING) {
1389+
errors.push(`SECRETS_${envPrefix}_BINDING environment variable missing`);
1390+
}
1391+
1392+
if (!foundBinding.STORE_ID) {
1393+
errors.push(`SECRETS_${envPrefix}_STORE_ID environment variable missing`);
1394+
}
1395+
1396+
if (!foundBinding.SECRET_NAME) {
1397+
errors.push(`SECRETS_${envPrefix}_SECRET_NAME environment variable missing`);
1398+
}
1399+
1400+
bindings[binding.binding] = {
1401+
configured: binding,
1402+
environment: foundBinding,
1403+
valid: foundBinding.BINDING && foundBinding.STORE_ID && foundBinding.SECRET_NAME
1404+
};
1405+
}
1406+
1407+
return {
1408+
valid: errors.length === 0,
1409+
bindings,
1410+
errors
1411+
};
1412+
}
1413+
1414+
/**
1415+
* Get a summary of Secrets Store bindings for logging and debugging
1416+
* @public
1417+
*/
1418+
public getSecretsStoreBindingSummary(): { configured: number; bindings: Array<{ name: string; storeId: string; secretName: string }> } {
1419+
return {
1420+
configured: this.secretsStoreBindings.length,
1421+
bindings: this.secretsStoreBindings.map(binding => ({
1422+
name: binding.binding,
1423+
storeId: binding.storeId,
1424+
secretName: binding.secretName
1425+
}))
1426+
};
1427+
}
1428+
1429+
/**
1430+
* Auto-detect Secrets Store bindings from the environment
1431+
* @private
1432+
*/
1433+
private autoDetectSecretsStoreBindings(env: any): SecretsStoreBinding[] {
1434+
const secretsStoreBindings: SecretsStoreBinding[] = [];
1435+
1436+
// Look for Secrets Store binding properties in the environment
1437+
for (const [key, value] of Object.entries(env)) {
1438+
// Check if this property looks like a Secrets Store binding
1439+
if (value && typeof value === 'object' && 'get' in value && typeof value.get === 'function') {
1440+
// This looks like a Secrets Store binding - we need store_id and secret_name
1441+
// For auto-detection, we'll use sensible defaults based on the binding name
1442+
secretsStoreBindings.push({
1443+
binding: key,
1444+
storeId: `auto-detected-store-${key.toLowerCase()}`,
1445+
secretName: key.toLowerCase().replace(/_/g, '-')
1446+
});
1447+
}
1448+
}
1449+
1450+
return secretsStoreBindings;
1451+
}
1452+
12921453
private isActivityExpired(): boolean {
12931454
return this.sleepAfterMs <= Date.now();
12941455
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Mock for cloudflare:workers module
2+
class DurableObject {
3+
constructor(ctx, env) {
4+
this.ctx = ctx;
5+
this.env = env;
6+
}
7+
8+
async fetch(request) {
9+
return new Response('Mock DurableObject response');
10+
}
11+
}
12+
13+
module.exports = { DurableObject };

0 commit comments

Comments
 (0)