mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# Cloudflare Secrets Store
|
||||
|
||||
Account-level encrypted secret management for Workers and AI Gateway.
|
||||
|
||||
## Overview
|
||||
|
||||
**Secrets Store**: Centralized, account-level secrets, reusable across Workers
|
||||
**Worker Secrets**: Per-Worker secrets (`wrangler secret put`)
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Store**: Container (1/account in beta)
|
||||
- **Secret**: String ≤1024 bytes
|
||||
- **Scopes**: Permission boundaries controlling access
|
||||
- `workers`: For Workers runtime access
|
||||
- `ai-gateway`: For AI Gateway access
|
||||
- Secrets must have correct scope for binding to work
|
||||
- **Bindings**: Connect secrets via `env` object
|
||||
|
||||
**Regional Availability**: Global except China Network (unavailable)
|
||||
|
||||
### Access Control
|
||||
|
||||
- **Super Admin**: Full access
|
||||
- **Admin**: Create/edit/delete secrets, view metadata
|
||||
- **Deployer**: View metadata + bindings
|
||||
- **Reporter**: View metadata only
|
||||
|
||||
API Token permissions: `Account Secrets Store Edit/Read`
|
||||
|
||||
### Limits (Beta)
|
||||
|
||||
- 100 secrets/account
|
||||
- 1 store/account
|
||||
- 1024 bytes max/secret
|
||||
- Production secrets count toward limit
|
||||
|
||||
## When to Use
|
||||
|
||||
**Use Secrets Store when:**
|
||||
- Multiple Workers share same credential
|
||||
- Centralized management needed
|
||||
- Compliance requires audit trail
|
||||
- Team collaboration on secrets
|
||||
|
||||
**Use Worker Secrets when:**
|
||||
- Secret unique to one Worker
|
||||
- Simple single-Worker project
|
||||
- No cross-Worker sharing needed
|
||||
|
||||
## In This Reference
|
||||
|
||||
### Reading Order by Task
|
||||
|
||||
| Task | Start Here | Then Read |
|
||||
|------|------------|-----------|
|
||||
| Quick overview | README.md | - |
|
||||
| First-time setup | README.md → configuration.md | api.md |
|
||||
| Add secret to Worker | configuration.md | api.md |
|
||||
| Implement access pattern | api.md | patterns.md |
|
||||
| Debug errors | gotchas.md | api.md |
|
||||
| Secret rotation | patterns.md | configuration.md |
|
||||
| Best practices | gotchas.md | patterns.md |
|
||||
|
||||
### Files
|
||||
|
||||
- [configuration.md](./configuration.md) - Wrangler commands, binding config
|
||||
- [api.md](./api.md) - Binding API, get/put/delete operations
|
||||
- [patterns.md](./patterns.md) - Rotation, encryption, access control
|
||||
- [gotchas.md](./gotchas.md) - Security issues, limits, best practices
|
||||
|
||||
## See Also
|
||||
- [workers](../workers/) - Worker bindings integration
|
||||
- [wrangler](../wrangler/) - CLI secret management commands
|
||||
200
.agents/skills/cloudflare-deploy/references/secrets-store/api.md
Normal file
200
.agents/skills/cloudflare-deploy/references/secrets-store/api.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# API Reference
|
||||
|
||||
## Binding API
|
||||
|
||||
### Basic Access
|
||||
|
||||
**CRITICAL**: Async `.get()` required - secrets NOT directly available.
|
||||
|
||||
**`.get()` throws on error** - does NOT return null. Always use try/catch.
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
API_KEY: { get(): Promise<string> };
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const apiKey = await env.API_KEY.get();
|
||||
return fetch("https://api.example.com", {
|
||||
headers: { "Authorization": `Bearer ${apiKey}` }
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
try {
|
||||
const apiKey = await env.API_KEY.get();
|
||||
return fetch("https://api.example.com", {
|
||||
headers: { "Authorization": `Bearer ${apiKey}` }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Secret access failed:", error);
|
||||
return new Response("Configuration error", { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Secrets & Patterns
|
||||
|
||||
```typescript
|
||||
// Parallel fetch
|
||||
const [stripeKey, sendgridKey] = await Promise.all([
|
||||
env.STRIPE_KEY.get(),
|
||||
env.SENDGRID_KEY.get()
|
||||
]);
|
||||
|
||||
// ❌ Missing .get()
|
||||
const key = env.API_KEY;
|
||||
|
||||
// ❌ Module-level cache
|
||||
const CACHED_KEY = await env.API_KEY.get(); // Fails
|
||||
|
||||
// ✅ Request-scope cache
|
||||
const key = await env.API_KEY.get(); // OK - reuse within request
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
Base: `https://api.cloudflare.com/client/v4`
|
||||
|
||||
### Auth
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $CF_TOKEN" \
|
||||
https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/secrets_store/stores
|
||||
```
|
||||
|
||||
### Store Operations
|
||||
|
||||
```bash
|
||||
# List
|
||||
GET /accounts/{account_id}/secrets_store/stores
|
||||
|
||||
# Create
|
||||
POST /accounts/{account_id}/secrets_store/stores
|
||||
{"name": "my-store"}
|
||||
|
||||
# Delete
|
||||
DELETE /accounts/{account_id}/secrets_store/stores/{store_id}
|
||||
```
|
||||
|
||||
### Secret Operations
|
||||
|
||||
```bash
|
||||
# List
|
||||
GET /accounts/{account_id}/secrets_store/stores/{store_id}/secrets
|
||||
|
||||
# Create (single)
|
||||
POST /accounts/{account_id}/secrets_store/stores/{store_id}/secrets
|
||||
{
|
||||
"name": "my_secret",
|
||||
"value": "secret_value",
|
||||
"scopes": ["workers"],
|
||||
"comment": "Optional"
|
||||
}
|
||||
|
||||
# Create (batch)
|
||||
POST /accounts/{account_id}/secrets_store/stores/{store_id}/secrets
|
||||
[
|
||||
{"name": "secret_one", "value": "val1", "scopes": ["workers"]},
|
||||
{"name": "secret_two", "value": "val2", "scopes": ["workers", "ai-gateway"]}
|
||||
]
|
||||
|
||||
# Get metadata
|
||||
GET /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id}
|
||||
|
||||
# Update
|
||||
PATCH /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id}
|
||||
{"value": "new_value", "comment": "Updated"}
|
||||
|
||||
# Delete (single)
|
||||
DELETE /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id}
|
||||
|
||||
# Delete (batch)
|
||||
DELETE /accounts/{account_id}/secrets_store/stores/{store_id}/secrets
|
||||
{"secret_ids": ["id-1", "id-2"]}
|
||||
|
||||
# Duplicate
|
||||
POST /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id}/duplicate
|
||||
{"name": "new_name"}
|
||||
|
||||
# Quota
|
||||
GET /accounts/{account_id}/secrets_store/quota
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
Success:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {
|
||||
"id": "secret-id-123",
|
||||
"name": "my_secret",
|
||||
"created": "2025-01-11T12:00:00Z",
|
||||
"scopes": ["workers"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Error:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"errors": [{"code": 10000, "message": "Name exists"}]
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Helpers
|
||||
|
||||
Official types available via `@cloudflare/workers-types`:
|
||||
|
||||
```typescript
|
||||
import type { SecretsStoreSecret } from "@cloudflare/workers-types";
|
||||
|
||||
interface Env {
|
||||
STRIPE_API_KEY: SecretsStoreSecret;
|
||||
DATABASE_URL: SecretsStoreSecret;
|
||||
WORKER_SECRET: string; // Regular Worker secret (direct access)
|
||||
}
|
||||
```
|
||||
|
||||
Custom helper type:
|
||||
|
||||
```typescript
|
||||
interface SecretsStoreBinding {
|
||||
get(): Promise<string>;
|
||||
}
|
||||
|
||||
// Fallback helper
|
||||
async function getSecretWithFallback(
|
||||
primary: SecretsStoreBinding,
|
||||
fallback?: SecretsStoreBinding
|
||||
): Promise<string> {
|
||||
try {
|
||||
return await primary.get();
|
||||
} catch (error) {
|
||||
if (fallback) return await fallback.get();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Batch helper
|
||||
async function getAllSecrets(
|
||||
secrets: Record<string, SecretsStoreBinding>
|
||||
): Promise<Record<string, string>> {
|
||||
const entries = await Promise.all(
|
||||
Object.entries(secrets).map(async ([k, v]) => [k, await v.get()])
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
```
|
||||
|
||||
See: [configuration.md](./configuration.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)
|
||||
@@ -0,0 +1,185 @@
|
||||
# Configuration
|
||||
|
||||
## Wrangler Config
|
||||
|
||||
### Basic Binding
|
||||
|
||||
**wrangler.jsonc**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"secrets_store_secrets": [
|
||||
{
|
||||
"binding": "API_KEY",
|
||||
"store_id": "abc123",
|
||||
"secret_name": "stripe_api_key"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**wrangler.toml** (alternative):
|
||||
|
||||
```toml
|
||||
[[secrets_store_secrets]]
|
||||
binding = "API_KEY"
|
||||
store_id = "abc123"
|
||||
secret_name = "stripe_api_key"
|
||||
```
|
||||
|
||||
Fields:
|
||||
- `binding`: Variable name for `env` access
|
||||
- `store_id`: From `wrangler secrets-store store list`
|
||||
- `secret_name`: Identifier (no spaces)
|
||||
|
||||
### Environment-Specific
|
||||
|
||||
**wrangler.jsonc**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"env": {
|
||||
"production": {
|
||||
"secrets_store_secrets": [
|
||||
{
|
||||
"binding": "API_KEY",
|
||||
"store_id": "prod-store",
|
||||
"secret_name": "prod_api_key"
|
||||
}
|
||||
]
|
||||
},
|
||||
"staging": {
|
||||
"secrets_store_secrets": [
|
||||
{
|
||||
"binding": "API_KEY",
|
||||
"store_id": "staging-store",
|
||||
"secret_name": "staging_api_key"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**wrangler.toml** (alternative):
|
||||
|
||||
```toml
|
||||
[env.production]
|
||||
[[env.production.secrets_store_secrets]]
|
||||
binding = "API_KEY"
|
||||
store_id = "prod-store"
|
||||
secret_name = "prod_api_key"
|
||||
|
||||
[env.staging]
|
||||
[[env.staging.secrets_store_secrets]]
|
||||
binding = "API_KEY"
|
||||
store_id = "staging-store"
|
||||
secret_name = "staging_api_key"
|
||||
```
|
||||
|
||||
## Wrangler Commands
|
||||
|
||||
### Store Management
|
||||
|
||||
```bash
|
||||
wrangler secrets-store store list
|
||||
wrangler secrets-store store create my-store --remote
|
||||
wrangler secrets-store store delete <store-id> --remote
|
||||
```
|
||||
|
||||
### Secret Management (Production)
|
||||
|
||||
```bash
|
||||
# Create (interactive)
|
||||
wrangler secrets-store secret create <store-id> \
|
||||
--name MY_SECRET --scopes workers --remote
|
||||
|
||||
# Create (piped)
|
||||
cat secret.txt | wrangler secrets-store secret create <store-id> \
|
||||
--name MY_SECRET --scopes workers --remote
|
||||
|
||||
# List/get/update/delete
|
||||
wrangler secrets-store secret list <store-id> --remote
|
||||
wrangler secrets-store secret get <store-id> --name MY_SECRET --remote
|
||||
wrangler secrets-store secret update <store-id> --name MY_SECRET --new-value "val" --remote
|
||||
wrangler secrets-store secret delete <store-id> --name MY_SECRET --remote
|
||||
|
||||
# Duplicate
|
||||
wrangler secrets-store secret duplicate <store-id> \
|
||||
--name ORIG --new-name COPY --remote
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
**CRITICAL**: Production secrets (`--remote`) NOT accessible in local dev.
|
||||
|
||||
```bash
|
||||
# Create local-only (no --remote)
|
||||
wrangler secrets-store secret create <store-id> --name DEV_KEY --scopes workers
|
||||
|
||||
wrangler dev # Uses local secrets
|
||||
wrangler deploy # Uses production secrets
|
||||
```
|
||||
|
||||
Best practice: Separate names for local/prod:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"env": {
|
||||
"development": {
|
||||
"secrets_store_secrets": [
|
||||
{ "binding": "API_KEY", "store_id": "store", "secret_name": "dev_api_key" }
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"secrets_store_secrets": [
|
||||
{ "binding": "API_KEY", "store_id": "store", "secret_name": "prod_api_key" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dashboard
|
||||
|
||||
### Creating Secrets
|
||||
|
||||
1. **Secrets Store** → **Create secret**
|
||||
2. Fill: Name (no spaces), Value, Scope (`Workers`), Comment
|
||||
3. **Save** (value hidden after)
|
||||
|
||||
### Adding Bindings
|
||||
|
||||
**Method 1**: Worker → Settings → Bindings → Add → Secrets Store
|
||||
**Method 2**: Create secret directly from Worker settings dropdown
|
||||
|
||||
Deploy options:
|
||||
- **Deploy**: Immediate 100%
|
||||
- **Save version**: Gradual rollout
|
||||
|
||||
## CI/CD
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
- name: Create secret
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_TOKEN }}
|
||||
run: |
|
||||
echo "${{ secrets.API_KEY }}" | \
|
||||
npx wrangler secrets-store secret create $STORE_ID \
|
||||
--name API_KEY --scopes workers --remote
|
||||
|
||||
- name: Deploy
|
||||
run: npx wrangler deploy
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
||||
```yaml
|
||||
script:
|
||||
- echo "$API_KEY_VALUE" | npx wrangler secrets-store secret create $STORE_ID --name API_KEY --scopes workers --remote
|
||||
- npx wrangler deploy
|
||||
```
|
||||
|
||||
See: [api.md](./api.md), [patterns.md](./patterns.md)
|
||||
@@ -0,0 +1,97 @@
|
||||
# Gotchas
|
||||
|
||||
## Common Errors
|
||||
|
||||
### ".get() Throws on Error"
|
||||
|
||||
**Cause:** Assuming `.get()` returns null on failure instead of throwing
|
||||
**Solution:** Always wrap `.get()` calls in try/catch blocks to handle errors gracefully
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const key = await env.API_KEY.get();
|
||||
} catch (error) {
|
||||
return new Response("Configuration error", { status: 500 });
|
||||
}
|
||||
```
|
||||
|
||||
### "Logging Secret Values"
|
||||
|
||||
**Cause:** Accidentally logging secret values in console or error messages
|
||||
**Solution:** Only log metadata (e.g., "Retrieved API_KEY") never the actual secret value
|
||||
|
||||
### "Module-Level Secret Access"
|
||||
|
||||
**Cause:** Attempting to access secrets during module initialization before env is available
|
||||
**Solution:** Cache secrets in request scope only, not at module level
|
||||
|
||||
### "Secret not found in store"
|
||||
|
||||
**Cause:** Secret name doesn't exist, case mismatch, missing workers scope, or incorrect store_id
|
||||
**Solution:** Verify secret exists with `wrangler secrets-store secret list <store-id> --remote`, check name matches exactly (case-sensitive), ensure secret has `workers` scope, and verify correct store_id
|
||||
|
||||
### "Scope Mismatch"
|
||||
|
||||
**Cause:** Secret exists but missing `workers` scope (only has `ai-gateway` scope)
|
||||
**Solution:** Update secret scopes: `wrangler secrets-store secret update <store-id> --name SECRET --scopes workers --remote` or add via Dashboard
|
||||
|
||||
### "JSON Parsing Failure"
|
||||
|
||||
**Cause:** Storing invalid JSON in secret, then failing to parse during runtime
|
||||
**Solution:** Validate JSON before storing:
|
||||
|
||||
```bash
|
||||
# Validate before storing
|
||||
echo '{"key":"value"}' | jq . && \
|
||||
echo '{"key":"value"}' | wrangler secrets-store secret create <store-id> \
|
||||
--name CONFIG --scopes workers --remote
|
||||
```
|
||||
|
||||
Runtime parsing with error handling:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const configStr = await env.CONFIG.get();
|
||||
const config = JSON.parse(configStr);
|
||||
} catch (error) {
|
||||
console.error("Invalid config JSON:", error);
|
||||
return new Response("Invalid configuration", { status: 500 });
|
||||
}
|
||||
```
|
||||
|
||||
### "Cannot access secret in local dev"
|
||||
|
||||
**Cause:** Attempting to access production secrets in local development environment
|
||||
**Solution:** Create local-only secrets (without `--remote` flag) for development: `wrangler secrets-store secret create <store-id> --name API_KEY --scopes workers`
|
||||
|
||||
### "Property 'get' does not exist"
|
||||
|
||||
**Cause:** Missing TypeScript type definition for secret binding
|
||||
**Solution:** Define interface with get method: `interface Env { API_KEY: { get(): Promise<string> }; }`
|
||||
|
||||
### "Binding already exists"
|
||||
|
||||
**Cause:** Duplicate binding in dashboard or conflict between wrangler.jsonc and dashboard
|
||||
**Solution:** Remove duplicate from dashboard Settings → Bindings, check for conflicts, or delete old Worker secret with `wrangler secret delete API_KEY`
|
||||
|
||||
### "Account secret quota exceeded"
|
||||
|
||||
**Cause:** Account has reached 100 secret limit (beta)
|
||||
**Solution:** Check quota with `wrangler secrets-store quota --remote`, delete unused secrets, consolidate duplicates, or contact Cloudflare for increase
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Value | Notes |
|
||||
|-------|-------|-------|
|
||||
| Max secrets per account | 100 | Beta limit |
|
||||
| Max stores per account | 1 | Beta limit |
|
||||
| Max secret size | 1024 bytes | Per secret |
|
||||
| Local secrets | Don't count toward limit | Only production secrets count |
|
||||
| Scopes available | `workers`, `ai-gateway` | Must have correct scope for access |
|
||||
| Scope | Account-level | Can be reused across multiple Workers |
|
||||
| Access method | `await env.BINDING.get()` | Async only, throws on error |
|
||||
| Management | Centralized | Via secrets-store commands |
|
||||
| Local dev | Separate local secrets | Use without `--remote` flag |
|
||||
| Regional availability | Global except China Network | Unavailable in China Network |
|
||||
|
||||
See: [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md)
|
||||
@@ -0,0 +1,207 @@
|
||||
# Patterns
|
||||
|
||||
## Secret Rotation
|
||||
|
||||
Zero-downtime rotation with versioned naming (`api_key_v1`, `api_key_v2`):
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
PRIMARY_KEY: { get(): Promise<string> };
|
||||
FALLBACK_KEY?: { get(): Promise<string> };
|
||||
}
|
||||
|
||||
async function fetchWithAuth(url: string, key: string) {
|
||||
return fetch(url, { headers: { "Authorization": `Bearer ${key}` } });
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
let resp = await fetchWithAuth("https://api.example.com", await env.PRIMARY_KEY.get());
|
||||
|
||||
// Fallback during rotation
|
||||
if (!resp.ok && env.FALLBACK_KEY) {
|
||||
resp = await fetchWithAuth("https://api.example.com", await env.FALLBACK_KEY.get());
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Workflow: Create `api_key_v2` → add fallback binding → deploy → swap primary → deploy → remove `v1`
|
||||
|
||||
## Encryption with KV
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
CACHE: KVNamespace;
|
||||
ENCRYPTION_KEY: { get(): Promise<string> };
|
||||
}
|
||||
|
||||
async function encryptValue(value: string, key: string): Promise<string> {
|
||||
const enc = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw", enc.encode(key), { name: "AES-GCM" }, false, ["encrypt"]
|
||||
);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv }, keyMaterial, enc.encode(value)
|
||||
);
|
||||
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
combined.set(iv);
|
||||
combined.set(new Uint8Array(encrypted), iv.length);
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const key = await env.ENCRYPTION_KEY.get();
|
||||
const encrypted = await encryptValue("sensitive-data", key);
|
||||
await env.CACHE.put("user:123:data", encrypted);
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## HMAC Signing
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
HMAC_SECRET: { get(): Promise<string> };
|
||||
}
|
||||
|
||||
async function signRequest(data: string, secret: string): Promise<string> {
|
||||
const enc = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
|
||||
);
|
||||
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data));
|
||||
return btoa(String.fromCharCode(...new Uint8Array(sig)));
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const secret = await env.HMAC_SECRET.get();
|
||||
const payload = await request.text();
|
||||
const signature = await signRequest(payload, secret);
|
||||
return Response.json({ signature });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Audit & Monitoring
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const apiKey = await env.API_KEY.get();
|
||||
const resp = await fetch("https://api.example.com", {
|
||||
headers: { "Authorization": `Bearer ${apiKey}` }
|
||||
});
|
||||
|
||||
ctx.waitUntil(
|
||||
fetch("https://log.example.com/log", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
event: "secret_used",
|
||||
secret_name: "API_KEY",
|
||||
timestamp: new Date().toISOString(),
|
||||
duration_ms: Date.now() - startTime,
|
||||
success: resp.ok
|
||||
})
|
||||
})
|
||||
);
|
||||
return resp;
|
||||
} catch (error) {
|
||||
ctx.waitUntil(
|
||||
fetch("https://log.example.com/log", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
event: "secret_access_failed",
|
||||
secret_name: "API_KEY",
|
||||
error: error instanceof Error ? error.message : "Unknown"
|
||||
})
|
||||
})
|
||||
);
|
||||
return new Response("Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from Worker Secrets
|
||||
|
||||
Change `env.SECRET` (direct) to `await env.SECRET.get()` (async).
|
||||
|
||||
Steps:
|
||||
1. Create in Secrets Store: `wrangler secrets-store secret create <store-id> --name API_KEY --scopes workers --remote`
|
||||
2. Add binding to `wrangler.jsonc`: `{"binding": "API_KEY", "store_id": "abc123", "secret_name": "api_key"}`
|
||||
3. Update code: `const key = await env.API_KEY.get();`
|
||||
4. Test staging, deploy
|
||||
5. Remove old: `wrangler secret delete API_KEY`
|
||||
|
||||
## Sharing Across Workers
|
||||
|
||||
Same secret, different binding names:
|
||||
|
||||
```jsonc
|
||||
// worker-1: binding="SHARED_DB", secret_name="postgres_url"
|
||||
// worker-2: binding="DB_CONN", secret_name="postgres_url"
|
||||
```
|
||||
|
||||
## JSON Secret Parsing
|
||||
|
||||
Store structured config as JSON secrets:
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
DB_CONFIG: { get(): Promise<string> };
|
||||
}
|
||||
|
||||
interface DbConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
try {
|
||||
const configStr = await env.DB_CONFIG.get();
|
||||
const config: DbConfig = JSON.parse(configStr);
|
||||
|
||||
// Use parsed config
|
||||
const dbUrl = `postgres://${config.username}:${config.password}@${config.host}:${config.port}`;
|
||||
|
||||
return Response.json({ connected: true });
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return new Response("Invalid config JSON", { status: 500 });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Store JSON secret:
|
||||
|
||||
```bash
|
||||
echo '{"host":"db.example.com","port":5432,"username":"app","password":"secret"}' | \
|
||||
wrangler secrets-store secret create <store-id> \
|
||||
--name DB_CONFIG --scopes workers --remote
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
### Service Bindings
|
||||
|
||||
Auth Worker signs JWT with Secrets Store; API Worker verifies via service binding.
|
||||
|
||||
See: [workers](../workers/) for service binding patterns.
|
||||
|
||||
See: [api.md](./api.md), [gotchas.md](./gotchas.md)
|
||||
Reference in New Issue
Block a user