@striae-org/striae 7.1.2 → 8.0.0

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 (47) hide show
  1. package/.env.example +18 -1
  2. package/app/components/actions/case-manage/operations.ts +2 -1
  3. package/app/routes/striae/utils/case-export.ts +1 -17
  4. package/app/utils/data/operations/case-export-loader.ts +17 -0
  5. package/app/utils/forensics/signature-utils.ts +110 -44
  6. package/functions/[[path]].ts +2 -1
  7. package/load-context.ts +13 -1
  8. package/package.json +23 -21
  9. package/public/_routes.json +1 -0
  10. package/react-router.config.ts +7 -0
  11. package/scripts/deploy-all.sh +28 -14
  12. package/scripts/deploy-config/modules/env-utils.sh +10 -1
  13. package/scripts/deploy-config/modules/keys.sh +33 -4
  14. package/scripts/deploy-config/modules/prompt.sh +2 -0
  15. package/scripts/deploy-config/modules/scaffolding.sh +32 -14
  16. package/scripts/deploy-config/modules/validation.sh +6 -1
  17. package/scripts/deploy-worker-secrets.sh +20 -50
  18. package/scripts/encrypt-registry.mjs +104 -0
  19. package/scripts/upload-registries.sh +146 -0
  20. package/shared/registry/r2-key-registry.ts +161 -0
  21. package/shared/registry/registry-encryption.ts +112 -0
  22. package/vite.config.ts +2 -1
  23. package/workers/audit-worker/package.json +2 -2
  24. package/workers/audit-worker/src/crypto/data-at-rest.ts +9 -72
  25. package/workers/audit-worker/src/types.ts +2 -0
  26. package/workers/audit-worker/wrangler.jsonc.example +5 -1
  27. package/workers/data-worker/package.json +2 -2
  28. package/workers/data-worker/src/handlers/decrypt-export.ts +1 -1
  29. package/workers/data-worker/src/handlers/signing.ts +5 -2
  30. package/workers/data-worker/src/registry/key-registry.ts +43 -101
  31. package/workers/data-worker/src/types.ts +6 -2
  32. package/workers/data-worker/wrangler.jsonc.example +5 -1
  33. package/workers/image-worker/package.json +2 -2
  34. package/workers/image-worker/src/security/key-registry.ts +13 -74
  35. package/workers/image-worker/src/types.ts +2 -0
  36. package/workers/image-worker/wrangler.jsonc.example +5 -1
  37. package/workers/lists-worker/package.json +2 -2
  38. package/workers/lists-worker/wrangler.jsonc.example +1 -1
  39. package/workers/pdf-worker/package.json +2 -2
  40. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  41. package/workers/user-worker/package.json +2 -2
  42. package/workers/user-worker/src/cleanup/account-deletion.ts +9 -74
  43. package/workers/user-worker/src/registry/user-kv.ts +8 -65
  44. package/workers/user-worker/src/storage/user-records.ts +1 -1
  45. package/workers/user-worker/src/types.ts +2 -0
  46. package/workers/user-worker/wrangler.jsonc.example +5 -1
  47. package/wrangler.toml.example +1 -1
package/.env.example CHANGED
@@ -47,6 +47,10 @@ USER_KV_ENCRYPTION_ACTIVE_KEY_ID=
47
47
  MANIFEST_SIGNING_PRIVATE_KEY=your_manifest_signing_private_key_here
48
48
  MANIFEST_SIGNING_KEY_ID=your_manifest_signing_key_id_here
49
49
  MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
50
+ # Optional key registry for rotation-safe manifest signing.
51
+ # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
52
+ MANIFEST_SIGNING_KEYS_JSON='{}'
53
+ MANIFEST_SIGNING_ACTIVE_KEY_ID=
50
54
 
51
55
  # DATA EXPORT ENCRYPTION CONFIGURATION
52
56
  EXPORT_ENCRYPTION_PRIVATE_KEY=your_export_encryption_private_key_here
@@ -62,11 +66,17 @@ DATA_AT_REST_ENCRYPTION_ENABLED=true
62
66
  DATA_AT_REST_ENCRYPTION_PRIVATE_KEY=your_data_at_rest_encryption_private_key_here
63
67
  DATA_AT_REST_ENCRYPTION_KEY_ID=your_data_at_rest_encryption_key_id_here
64
68
  DATA_AT_REST_ENCRYPTION_PUBLIC_KEY=your_data_at_rest_encryption_public_key_here
65
- # Optional key registry for data/files/audit decryption compatibility.
69
+ # Key registries are now stored in R2 at CONFIG_BUCKET_NAME/{scope}-keys.json.
70
+ # These env vars are only used for initial upload to R2 via scripts/upload-registries.sh.
66
71
  # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
67
72
  DATA_AT_REST_ENCRYPTION_KEYS_JSON='{}'
68
73
  DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID=
69
74
 
75
+ # REGISTRY ENCRYPTION KEY
76
+ # AES-256-GCM key (32 bytes, base64url-encoded) used to encrypt key registries at rest in R2.
77
+ # Generated automatically by deploy-config.sh. Shared across all workers that read registries.
78
+ REGISTRY_ENCRYPTION_KEY=your_registry_encryption_key_here
79
+
70
80
  # ================================
71
81
  # PAGES WORKER ENVIRONMENT VARIABLES
72
82
  # ================================
@@ -79,6 +89,13 @@ PAGES_CUSTOM_DOMAIN=your_custom_domain_here
79
89
  USER_WORKER_NAME=your_user_worker_name_here
80
90
  KV_STORE_ID=your_kv_store_id_here
81
91
 
92
+ # ================================
93
+ # SHARED CONFIG BUCKET (key registries)
94
+ # ================================
95
+ # All key registries are stored in R2 at this bucket as {scope}-keys.json.
96
+ # Dev: striae-dev-config | Prod: striae-config
97
+ CONFIG_BUCKET_NAME=your_config_bucket_name_here
98
+
82
99
  # ================================
83
100
  # DATA WORKER ENVIRONMENT VARIABLES
84
101
  # ================================
@@ -16,7 +16,7 @@ import {
16
16
  } from '~/utils/data';
17
17
  import { type CaseData, type CaseExportData, type ValidationAuditEntry } from '~/types';
18
18
  import { auditService } from '~/services/audit';
19
- import { exportCaseData, formatDateForFilename } from '~/components/actions/case-export';
19
+ import { loadCaseExportActions } from '~/utils/data/operations/case-export-loader';
20
20
  import { buildArchivePackage } from './archive-package-builder';
21
21
  import { deleteFileWithoutAudit } from './delete-helpers';
22
22
  import { isReadOnlyCaseData, sortCaseNumbers, validateCaseNumber } from './utils';
@@ -600,6 +600,7 @@ export const archiveCase = async (
600
600
  isReadOnly: false,
601
601
  } as CaseData;
602
602
 
603
+ const { exportCaseData, formatDateForFilename } = await loadCaseExportActions();
603
604
  const exportData = await exportCaseData(user, caseNumber, { includeMetadata: true });
604
605
  const archivedExportData: CaseExportData = {
605
606
  ...exportData,
@@ -1,20 +1,4 @@
1
- import type * as CaseExportActions from '~/components/actions/case-export';
2
-
3
- export type CaseExportActionsModule = typeof CaseExportActions;
4
-
5
- let caseExportActionsPromise: Promise<CaseExportActionsModule> | null = null;
6
-
7
- export const loadCaseExportActions = (): Promise<CaseExportActionsModule> => {
8
- if (!caseExportActionsPromise) {
9
- caseExportActionsPromise = import('~/components/actions/case-export').catch((error: unknown) => {
10
- // Clear cached failures so transient chunk/network errors can recover on retry.
11
- caseExportActionsPromise = null;
12
- throw error;
13
- });
14
- }
15
-
16
- return caseExportActionsPromise;
17
- };
1
+ export { loadCaseExportActions, type CaseExportActionsModule } from '~/utils/data/operations/case-export-loader';
18
2
 
19
3
  export const getExportProgressLabel = (progress: number): string => {
20
4
  if (progress < 30) {
@@ -0,0 +1,17 @@
1
+ import type * as CaseExportActions from '~/components/actions/case-export';
2
+
3
+ export type CaseExportActionsModule = typeof CaseExportActions;
4
+
5
+ let caseExportActionsPromise: Promise<CaseExportActionsModule> | null = null;
6
+
7
+ export const loadCaseExportActions = (): Promise<CaseExportActionsModule> => {
8
+ if (!caseExportActionsPromise) {
9
+ caseExportActionsPromise = import('~/components/actions/case-export').catch((error: unknown) => {
10
+ // Clear cached failures so transient chunk/network errors can recover on retry.
11
+ caseExportActionsPromise = null;
12
+ throw error;
13
+ });
14
+ }
15
+
16
+ return caseExportActionsPromise;
17
+ };
@@ -29,6 +29,11 @@ export interface PublicSigningKeyDetails {
29
29
  publicKeyPem: string | null;
30
30
  }
31
31
 
32
+ interface VerificationKeyCandidate {
33
+ keyId: string;
34
+ publicKeyPem: string;
35
+ }
36
+
32
37
  const RSA_PSS_SALT_LENGTH = 32;
33
38
 
34
39
  type ManifestSigningConfig = {
@@ -155,6 +160,80 @@ export function getVerificationPublicKey(keyId: string): string | null {
155
160
  return null;
156
161
  }
157
162
 
163
+ function getVerificationPublicKeyCandidates(signatureKeyId: string): VerificationKeyCandidate[] {
164
+ const config = paths as unknown as ManifestSigningConfig;
165
+ const keyMap = config.manifest_signing_public_keys;
166
+ const candidates: VerificationKeyCandidate[] = [];
167
+ const seenKeyIds = new Set<string>();
168
+
169
+ const appendCandidate = (keyId: string | null, publicKeyPem: string | null): void => {
170
+ if (!keyId || !publicKeyPem || seenKeyIds.has(keyId)) {
171
+ return;
172
+ }
173
+
174
+ seenKeyIds.add(keyId);
175
+ candidates.push({ keyId, publicKeyPem });
176
+ };
177
+
178
+ appendCandidate(signatureKeyId, getVerificationPublicKey(signatureKeyId));
179
+
180
+ const configuredKeyId =
181
+ typeof config.manifest_signing_key_id === 'string' && config.manifest_signing_key_id.trim().length > 0
182
+ ? config.manifest_signing_key_id.trim()
183
+ : null;
184
+ const legacyPublicKeyPem = normalizePemOrNull(config.manifest_signing_public_key);
185
+
186
+ appendCandidate(configuredKeyId, configuredKeyId ? getVerificationPublicKey(configuredKeyId) : legacyPublicKeyPem);
187
+
188
+ if (keyMap && typeof keyMap === 'object') {
189
+ const orderedEntries = Object.entries(keyMap)
190
+ .filter(([, value]) => typeof value === 'string' && value.trim().length > 0)
191
+ .sort(([leftKeyId], [rightKeyId]) => leftKeyId.localeCompare(rightKeyId));
192
+
193
+ for (const [keyId, pemValue] of orderedEntries) {
194
+ appendCandidate(keyId, normalizePemPublicKey(pemValue));
195
+ }
196
+ }
197
+
198
+ if (candidates.length === 0 && legacyPublicKeyPem) {
199
+ appendCandidate(configuredKeyId ?? signatureKeyId, legacyPublicKeyPem);
200
+ }
201
+
202
+ return candidates;
203
+ }
204
+
205
+ async function verifyWithPublicKey(
206
+ payload: string,
207
+ signatureValue: string,
208
+ publicKeyPem: string,
209
+ invalidPublicKeyError: string
210
+ ): Promise<boolean> {
211
+ const key = await crypto.subtle.importKey(
212
+ 'spki',
213
+ publicKeyPemToArrayBuffer(publicKeyPem, invalidPublicKeyError),
214
+ {
215
+ name: 'RSA-PSS',
216
+ hash: 'SHA-256'
217
+ },
218
+ false,
219
+ ['verify']
220
+ );
221
+
222
+ const signatureBytes = base64UrlToUint8Array(signatureValue);
223
+ const signatureBuffer = new Uint8Array(signatureBytes.byteLength);
224
+ signatureBuffer.set(signatureBytes);
225
+
226
+ return crypto.subtle.verify(
227
+ {
228
+ name: 'RSA-PSS',
229
+ saltLength: RSA_PSS_SALT_LENGTH
230
+ },
231
+ key,
232
+ signatureBuffer,
233
+ new TextEncoder().encode(payload)
234
+ );
235
+ }
236
+
158
237
  export async function verifySignaturePayload(
159
238
  payload: string,
160
239
  signature: SignatureEnvelope,
@@ -177,59 +256,46 @@ export async function verifySignaturePayload(
177
256
  };
178
257
  }
179
258
 
180
- const publicKeyPem =
181
- typeof options.verificationPublicKeyPem === 'string' && options.verificationPublicKeyPem.trim().length > 0
182
- ? options.verificationPublicKeyPem
183
- : getVerificationPublicKey(signature.keyId);
184
- if (!publicKeyPem) {
185
- return {
186
- isValid: false,
187
- keyId: signature.keyId,
188
- error: `${messages.noVerificationKeyPrefix || 'No verification key configured for key ID'}: ${signature.keyId}`
189
- };
190
- }
191
-
192
259
  const verificationFailedError = messages.verificationFailedError || 'Signature verification failed';
193
260
  const invalidPublicKeyError =
194
261
  messages.invalidPublicKeyError ||
195
262
  `${verificationFailedError}: invalid public key`;
196
263
 
197
- try {
198
- const key = await crypto.subtle.importKey(
199
- 'spki',
200
- publicKeyPemToArrayBuffer(publicKeyPem, invalidPublicKeyError),
201
- {
202
- name: 'RSA-PSS',
203
- hash: 'SHA-256'
204
- },
205
- false,
206
- ['verify']
207
- );
208
-
209
- const signatureBytes = base64UrlToUint8Array(signature.value);
210
- const signatureBuffer = new Uint8Array(signatureBytes.byteLength);
211
- signatureBuffer.set(signatureBytes);
212
-
213
- const verified = await crypto.subtle.verify(
214
- {
215
- name: 'RSA-PSS',
216
- saltLength: RSA_PSS_SALT_LENGTH
217
- },
218
- key,
219
- signatureBuffer,
220
- new TextEncoder().encode(payload)
221
- );
264
+ const explicitVerificationKey =
265
+ typeof options.verificationPublicKeyPem === 'string' && options.verificationPublicKeyPem.trim().length > 0
266
+ ? options.verificationPublicKeyPem
267
+ : null;
268
+ const keyCandidates = explicitVerificationKey
269
+ ? [{ keyId: signature.keyId, publicKeyPem: explicitVerificationKey }]
270
+ : getVerificationPublicKeyCandidates(signature.keyId);
222
271
 
223
- return {
224
- isValid: verified,
225
- keyId: signature.keyId,
226
- error: verified ? undefined : verificationFailedError
227
- };
228
- } catch (error) {
272
+ if (keyCandidates.length === 0) {
229
273
  return {
230
274
  isValid: false,
231
275
  keyId: signature.keyId,
232
- error: error instanceof Error ? error.message : verificationFailedError
276
+ error: `${messages.noVerificationKeyPrefix || 'No verification key configured for key ID'}: ${signature.keyId}`
233
277
  };
234
278
  }
279
+
280
+ let lastError: unknown;
281
+
282
+ for (const candidate of keyCandidates) {
283
+ try {
284
+ const verified = await verifyWithPublicKey(payload, signature.value, candidate.publicKeyPem, invalidPublicKeyError);
285
+ if (verified) {
286
+ return {
287
+ isValid: true,
288
+ keyId: signature.keyId
289
+ };
290
+ }
291
+ } catch (error) {
292
+ lastError = error;
293
+ }
294
+ }
295
+
296
+ return {
297
+ isValid: false,
298
+ keyId: signature.keyId,
299
+ error: lastError instanceof Error ? lastError.message : verificationFailedError
300
+ };
235
301
  }
@@ -1,4 +1,5 @@
1
1
  import { createPagesFunctionHandler } from "@react-router/cloudflare";
2
+ import { getLoadContext } from "../load-context";
2
3
 
3
4
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4
5
  // @ts-ignore - the server build file is generated by `react-router build`
@@ -6,4 +7,4 @@ import * as build from "../build/server";
6
7
 
7
8
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
8
9
  // @ts-ignore - temporary workaround for crossOrigin type issue
9
- export const onRequest = createPagesFunctionHandler({ build });
10
+ export const onRequest = createPagesFunctionHandler({ build, getLoadContext });
package/load-context.ts CHANGED
@@ -1,9 +1,21 @@
1
1
  import { type PlatformProxy } from "wrangler";
2
+ import { RouterContextProvider } from "react-router";
2
3
 
3
4
  type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
4
5
 
5
6
  declare module "react-router" {
6
- interface AppLoadContext {
7
+ interface RouterContextProvider {
7
8
  cloudflare: Cloudflare;
8
9
  }
10
+ }
11
+
12
+ export function getLoadContext({
13
+ context,
14
+ }: {
15
+ request: Request;
16
+ context: { cloudflare: Cloudflare };
17
+ }) {
18
+ const provider = new RouterContextProvider();
19
+ Object.assign(provider, context);
20
+ return provider;
9
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "7.1.2",
3
+ "version": "8.0.0",
4
4
  "private": false,
5
5
  "description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
6
6
  "license": "Apache-2.0",
@@ -88,6 +88,8 @@
88
88
  "update-versions": "node ./scripts/update-markdown-versions.cjs",
89
89
  "update-compatibility-dates": "node ./scripts/update-compatibility-dates.cjs",
90
90
  "deploy-config": "bash ./scripts/deploy-config.sh",
91
+ "deploy-config:rotate-keys": "bash ./scripts/deploy-config.sh --force-rotate-keys",
92
+ "upload-registries": "bash ./scripts/upload-registries.sh",
91
93
  "update-env": "bash ./scripts/deploy-config.sh --update-env",
92
94
  "install-workers": "bash ./scripts/install-workers.sh",
93
95
  "deploy-workers": "npm run deploy-workers:audit && npm run deploy-workers:data && npm run deploy-workers:image && npm run deploy-workers:lists && npm run deploy-workers:pdf && npm run deploy-workers:user",
@@ -102,44 +104,44 @@
102
104
  "deploy-workers:user": "cd workers/user-worker && npm run deploy"
103
105
  },
104
106
  "dependencies": {
105
- "@react-router/cloudflare": "^7.15.0",
106
- "firebase": "^12.13.0",
107
- "isbot": "^5.1.40",
107
+ "@react-router/cloudflare": "^7.17.0",
108
+ "firebase": "^12.14.0",
109
+ "isbot": "^5.1.41",
108
110
  "jszip": "^3.10.1",
109
111
  "qrcode": "^1.5.4",
110
- "react": "^19.2.6",
111
- "react-dom": "^19.2.6",
112
- "react-router": "^7.15.0"
112
+ "react": "^19.2.7",
113
+ "react-dom": "^19.2.7",
114
+ "react-router": "^7.17.0"
113
115
  },
114
116
  "devDependencies": {
115
- "@cloudflare/vitest-pool-workers": "^0.15.2",
116
- "@react-router/dev": "^7.15.0",
117
- "@react-router/fs-routes": "^7.15.0",
117
+ "@cloudflare/vitest-pool-workers": "^0.16.9",
118
+ "@react-router/dev": "^7.17.0",
119
+ "@react-router/fs-routes": "^7.17.0",
118
120
  "@types/qrcode": "^1.5.6",
119
- "@types/react": "^19.2.14",
121
+ "@types/react": "^19.2.17",
120
122
  "@types/react-dom": "^19.2.3",
121
- "@typescript-eslint/eslint-plugin": "^8.59.2",
122
- "@typescript-eslint/parser": "^8.59.2",
123
- "@vitest/coverage-v8": "^4.1.5",
123
+ "@typescript-eslint/eslint-plugin": "^8.60.1",
124
+ "@typescript-eslint/parser": "^8.60.1",
125
+ "@vitest/coverage-v8": "^4.1.8",
124
126
  "eslint": "^9.39.4",
125
- "eslint-import-resolver-typescript": "^4.4.4",
127
+ "eslint-import-resolver-typescript": "^4.4.5",
126
128
  "eslint-plugin-import": "^2.32.0",
127
129
  "eslint-plugin-jsx-a11y": "^6.10.2",
128
130
  "eslint-plugin-react": "^7.37.5",
129
131
  "eslint-plugin-react-hooks": "^7.1.1",
130
- "firebase-admin": "^13.8.0",
132
+ "firebase-admin": "^13.10.0",
131
133
  "modern-normalize": "^3.0.1",
132
134
  "typescript": "^6.0.3",
133
- "vite": "^8.0.11",
134
- "vitest": "^4.1.5",
135
- "wrangler": "^4.88.0"
135
+ "vite": "^8.0.16",
136
+ "vitest": "^4.1.8",
137
+ "wrangler": "^4.98.0"
136
138
  },
137
139
  "overrides": {
138
140
  "@tootallnate/once": "3.0.1",
139
141
  "uuid": "^14.0.0"
140
142
  },
141
143
  "engines": {
142
- "node": ">=20.19.0"
144
+ "node": ">=22.0.0"
143
145
  },
144
146
  "packageManager": "npm@11.13.0"
145
- }
147
+ }
@@ -5,6 +5,7 @@
5
5
  "/assets/*",
6
6
  "/build/*",
7
7
  "/*.css",
8
+ "/*.ico",
8
9
  "/*.js",
9
10
  "/*.pdf",
10
11
  "/*.png",
@@ -2,4 +2,11 @@ import type { Config } from "@react-router/dev/config";
2
2
 
3
3
  export default {
4
4
  ssr: true,
5
+ future: {
6
+ v8_middleware: true,
7
+ v8_splitRouteModules: true,
8
+ v8_viteEnvironmentApi: true,
9
+ v8_passThroughRequests: true,
10
+ v8_trailingSlashAwareDataRequests: true,
11
+ },
5
12
  } satisfies Config;
@@ -6,10 +6,12 @@
6
6
  # This script deploys the entire Striae application:
7
7
  # 1. Configuration setup (copy configs, replace placeholders)
8
8
  # 2. Worker dependencies installation
9
- # 3. Workers (all 5 workers)
10
- # 4. Worker secrets/environment variables
11
- # 5. Pages secrets/environment variables
12
- # 6. Pages (frontend)
9
+ # 3. Wrangler types generation
10
+ # 4. Workers (all 6 workers)
11
+ # 5. Key registries (upload to R2 config bucket)
12
+ # 6. Worker secrets/environment variables
13
+ # 7. Pages secrets/environment variables
14
+ # 8. Pages (frontend)
13
15
 
14
16
  set -e
15
17
  set -o pipefail
@@ -67,6 +69,7 @@ require_command wrangler
67
69
  assert_file_exists "$SCRIPT_DIR/deploy-config.sh"
68
70
  assert_file_exists "$SCRIPT_DIR/install-workers.sh"
69
71
  assert_file_exists "$SCRIPT_DIR/deploy-worker-secrets.sh"
72
+ assert_file_exists "$SCRIPT_DIR/upload-registries.sh"
70
73
  assert_file_exists "$SCRIPT_DIR/deploy-pages-secrets.sh"
71
74
  assert_file_exists "package.json"
72
75
 
@@ -79,7 +82,7 @@ echo -e "${GREEN}✅ Preflight checks passed${NC}"
79
82
  echo ""
80
83
 
81
84
  # Step 1: Configuration Setup
82
- echo -e "${PURPLE}Step 1/7: Configuration Setup${NC}"
85
+ echo -e "${PURPLE}Step 1/8: Configuration Setup${NC}"
83
86
  echo "------------------------------"
84
87
  echo -e "${YELLOW}⚙️ Setting up configuration files and replacing placeholders...${NC}"
85
88
  if ! bash "$SCRIPT_DIR/deploy-config.sh"; then
@@ -92,7 +95,7 @@ run_config_checkpoint
92
95
  echo ""
93
96
 
94
97
  # Step 2: Install Worker Dependencies
95
- echo -e "${PURPLE}Step 2/7: Installing Worker Dependencies${NC}"
98
+ echo -e "${PURPLE}Step 2/8: Installing Worker Dependencies${NC}"
96
99
  echo "----------------------------------------"
97
100
  echo -e "${YELLOW}📦 Installing npm dependencies for all workers...${NC}"
98
101
  if ! bash "$SCRIPT_DIR/install-workers.sh"; then
@@ -103,7 +106,7 @@ echo -e "${GREEN}✅ All worker dependencies installed successfully${NC}"
103
106
  echo ""
104
107
 
105
108
  # Step 3: Generate Wrangler Types
106
- echo -e "${PURPLE}Step 3/7: Generating Wrangler Types${NC}"
109
+ echo -e "${PURPLE}Step 3/8: Generating Wrangler Types${NC}"
107
110
  echo "-------------------------------------"
108
111
  echo -e "${YELLOW}📝 Running wrangler types in root and all worker directories...${NC}"
109
112
  if ! npx wrangler types; then
@@ -121,7 +124,7 @@ echo -e "${GREEN}✅ Wrangler types generated successfully${NC}"
121
124
  echo ""
122
125
 
123
126
  # Step 4: Deploy Workers
124
- echo -e "${PURPLE}Step 4/7: Deploying Workers${NC}"
127
+ echo -e "${PURPLE}Step 4/8: Deploying Workers${NC}"
125
128
  echo "----------------------------"
126
129
  echo -e "${YELLOW}🔧 Deploying all 6 Cloudflare Workers...${NC}"
127
130
  if ! npm run deploy-workers; then
@@ -131,8 +134,19 @@ fi
131
134
  echo -e "${GREEN}✅ All workers deployed successfully${NC}"
132
135
  echo ""
133
136
 
134
- # Step 5: Deploy Worker Secrets
135
- echo -e "${PURPLE}Step 5/7: Deploying Worker Secrets${NC}"
137
+ # Step 5: Upload Key Registries to R2
138
+ echo -e "${PURPLE}Step 5/8: Uploading Key Registries to R2${NC}"
139
+ echo "-----------------------------------------"
140
+ echo -e "${YELLOW}📦 Uploading key registries to config bucket...${NC}"
141
+ if ! bash "$SCRIPT_DIR/upload-registries.sh"; then
142
+ echo -e "${RED}❌ Key registry upload failed!${NC}"
143
+ exit 1
144
+ fi
145
+ echo -e "${GREEN}✅ Key registries uploaded successfully${NC}"
146
+ echo ""
147
+
148
+ # Step 6: Deploy Worker Secrets
149
+ echo -e "${PURPLE}Step 6/8: Deploying Worker Secrets${NC}"
136
150
  echo "-----------------------------------"
137
151
  echo -e "${YELLOW}🔐 Deploying worker environment variables...${NC}"
138
152
  if ! bash "$SCRIPT_DIR/deploy-worker-secrets.sh"; then
@@ -142,8 +156,8 @@ fi
142
156
  echo -e "${GREEN}✅ Worker secrets deployed successfully${NC}"
143
157
  echo ""
144
158
 
145
- # Step 6: Deploy Pages Secrets
146
- echo -e "${PURPLE}Step 6/7: Deploying Pages Secrets${NC}"
159
+ # Step 7: Deploy Pages Secrets
160
+ echo -e "${PURPLE}Step 7/8: Deploying Pages Secrets${NC}"
147
161
  echo "----------------------------------"
148
162
  echo -e "${YELLOW}🔐 Deploying Pages environment variables...${NC}"
149
163
  if ! bash "$SCRIPT_DIR/deploy-pages-secrets.sh"; then
@@ -153,8 +167,8 @@ fi
153
167
  echo -e "${GREEN}✅ Pages secrets deployed successfully${NC}"
154
168
  echo ""
155
169
 
156
- # Step 7: Deploy Pages
157
- echo -e "${PURPLE}Step 7/7: Deploying Pages${NC}"
170
+ # Step 8: Deploy Pages
171
+ echo -e "${PURPLE}Step 8/8: Deploying Pages${NC}"
158
172
  echo "--------------------------"
159
173
  echo -e "${YELLOW}🌐 Building and deploying Pages...${NC}"
160
174
  if ! npm run deploy-pages; then
@@ -187,7 +187,7 @@ write_env_var() {
187
187
  var_value=$(strip_carriage_returns "$var_value")
188
188
  env_file_value="$var_value"
189
189
 
190
- if [ "$var_name" = "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "USER_KV_ENCRYPTION_KEYS_JSON" ]; then
190
+ if [ "$var_name" = "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_KEYS_JSON" ] || [ "$var_name" = "EXPORT_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "USER_KV_ENCRYPTION_KEYS_JSON" ]; then
191
191
  # Store as a quoted string so sourced .env preserves escaped newline markers (\n)
192
192
  env_file_value=${env_file_value//\"/\\\"}
193
193
  env_file_value="\"$env_file_value\""
@@ -252,3 +252,12 @@ update_private_key_registry() {
252
252
  escape_for_sed_replacement() {
253
253
  printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g'
254
254
  }
255
+
256
+ generate_10_char_id() {
257
+ local raw label
258
+ raw=$(openssl rand -base64 32 2>/dev/null | tr -dc 'a-z0-9' || true)
259
+ label="${raw:0:10}"
260
+ if [ -n "$label" ] && [ ${#label} -eq 10 ]; then
261
+ printf '%s' "$label"
262
+ fi
263
+ }
@@ -108,6 +108,8 @@ configure_manifest_signing_credentials() {
108
108
  restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_PRIVATE_KEY"
109
109
  restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_PUBLIC_KEY"
110
110
  restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_KEY_ID"
111
+ restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_KEYS_JSON"
112
+ restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_ACTIVE_KEY_ID"
111
113
 
112
114
  if [ -z "$MANIFEST_SIGNING_PRIVATE_KEY" ] || is_placeholder "$MANIFEST_SIGNING_PRIVATE_KEY" || [ -z "$MANIFEST_SIGNING_PUBLIC_KEY" ] || is_placeholder "$MANIFEST_SIGNING_PUBLIC_KEY"; then
113
115
  should_generate="true"
@@ -131,7 +133,7 @@ configure_manifest_signing_credentials() {
131
133
 
132
134
  if [ -z "$MANIFEST_SIGNING_KEY_ID" ] || is_placeholder "$MANIFEST_SIGNING_KEY_ID" || [ "$should_generate" = "true" ]; then
133
135
  local generated_key_id
134
- generated_key_id=$(generate_worker_subdomain_label)
136
+ generated_key_id=$(generate_10_char_id)
135
137
  if [ -z "$generated_key_id" ] || [ ${#generated_key_id} -ne 10 ]; then
136
138
  echo -e "${RED}❌ Error: Failed to generate MANIFEST_SIGNING_KEY_ID${NC}"
137
139
  exit 1
@@ -144,6 +146,8 @@ configure_manifest_signing_credentials() {
144
146
  echo -e "${GREEN}✅ MANIFEST_SIGNING_KEY_ID: $MANIFEST_SIGNING_KEY_ID${NC}"
145
147
  fi
146
148
 
149
+ update_private_key_registry "MANIFEST_SIGNING_KEYS_JSON" "MANIFEST_SIGNING_ACTIVE_KEY_ID" "$MANIFEST_SIGNING_KEY_ID" "$MANIFEST_SIGNING_PRIVATE_KEY" "manifest signing"
150
+
147
151
  echo ""
148
152
  }
149
153
 
@@ -212,7 +216,7 @@ configure_export_encryption_credentials() {
212
216
 
213
217
  if [ -z "$EXPORT_ENCRYPTION_KEY_ID" ] || is_placeholder "$EXPORT_ENCRYPTION_KEY_ID" || [ "$should_generate" = "true" ]; then
214
218
  local generated_key_id
215
- generated_key_id=$(generate_worker_subdomain_label)
219
+ generated_key_id=$(generate_10_char_id)
216
220
  if [ -z "$generated_key_id" ] || [ ${#generated_key_id} -ne 10 ]; then
217
221
  echo -e "${RED}❌ Error: Failed to generate EXPORT_ENCRYPTION_KEY_ID${NC}"
218
222
  exit 1
@@ -327,7 +331,7 @@ configure_user_kv_encryption_credentials() {
327
331
 
328
332
  if [ -z "$USER_KV_ENCRYPTION_KEY_ID" ] || is_placeholder "$USER_KV_ENCRYPTION_KEY_ID" || [ "$should_generate" = "true" ]; then
329
333
  local generated_key_id
330
- generated_key_id=$(generate_worker_subdomain_label)
334
+ generated_key_id=$(generate_10_char_id)
331
335
  if [ -z "$generated_key_id" ] || [ ${#generated_key_id} -ne 10 ]; then
332
336
  echo -e "${RED}❌ Error: Failed to generate USER_KV_ENCRYPTION_KEY_ID${NC}"
333
337
  exit 1
@@ -385,7 +389,7 @@ configure_data_at_rest_encryption_credentials() {
385
389
 
386
390
  if [ -z "$DATA_AT_REST_ENCRYPTION_KEY_ID" ] || is_placeholder "$DATA_AT_REST_ENCRYPTION_KEY_ID" || [ "$should_generate" = "true" ]; then
387
391
  local generated_key_id
388
- generated_key_id=$(generate_worker_subdomain_label)
392
+ generated_key_id=$(generate_10_char_id)
389
393
  if [ -z "$generated_key_id" ] || [ ${#generated_key_id} -ne 10 ]; then
390
394
  echo -e "${RED}❌ Error: Failed to generate DATA_AT_REST_ENCRYPTION_KEY_ID${NC}"
391
395
  exit 1
@@ -402,3 +406,28 @@ configure_data_at_rest_encryption_credentials() {
402
406
 
403
407
  echo ""
404
408
  }
409
+
410
+ configure_registry_encryption_key() {
411
+ echo -e "${BLUE}🔒 REGISTRY ENCRYPTION KEY CONFIGURATION${NC}"
412
+ echo "========================================="
413
+
414
+ restore_env_var_from_backup_if_missing "REGISTRY_ENCRYPTION_KEY"
415
+
416
+ if [ -n "$REGISTRY_ENCRYPTION_KEY" ] && ! is_placeholder "$REGISTRY_ENCRYPTION_KEY"; then
417
+ echo -e "${GREEN}✅ REGISTRY_ENCRYPTION_KEY already configured${NC}"
418
+ else
419
+ echo -e "${YELLOW}Generating REGISTRY_ENCRYPTION_KEY (32 random bytes, base64url)...${NC}"
420
+ local key_value
421
+ key_value=$(node -e "const { randomBytes } = require('crypto'); const buf = randomBytes(32); process.stdout.write(buf.toString('base64url'));")
422
+ if [ -z "$key_value" ] || [ ${#key_value} -lt 20 ]; then
423
+ echo -e "${RED}❌ Error: Failed to generate REGISTRY_ENCRYPTION_KEY${NC}"
424
+ exit 1
425
+ fi
426
+ REGISTRY_ENCRYPTION_KEY="$key_value"
427
+ export REGISTRY_ENCRYPTION_KEY
428
+ write_env_var "REGISTRY_ENCRYPTION_KEY" "$REGISTRY_ENCRYPTION_KEY"
429
+ echo -e "${GREEN}✅ REGISTRY_ENCRYPTION_KEY generated${NC}"
430
+ fi
431
+
432
+ echo ""
433
+ }
@@ -271,6 +271,7 @@ prompt_for_secrets() {
271
271
  prompt_for_var "DATA_BUCKET_NAME" "Your R2 bucket name for case data storage"
272
272
  prompt_for_var "AUDIT_BUCKET_NAME" "Your R2 bucket name for audit logs (separate from data bucket)"
273
273
  prompt_for_var "FILES_BUCKET_NAME" "Your R2 bucket name for encrypted files storage"
274
+ prompt_for_var "CONFIG_BUCKET_NAME" "Your R2 bucket name for config/key registries (shared across workers)"
274
275
  prompt_for_var "KV_STORE_ID" "Your KV namespace ID (UUID format)"
275
276
  prompt_for_var "STRIAE_LISTS_KV_ID" "KV namespace ID for the lists-worker (UUID format; backs registration and primershear allowlists)"
276
277
 
@@ -296,6 +297,7 @@ prompt_for_secrets() {
296
297
  configure_export_encryption_credentials
297
298
  configure_user_kv_encryption_credentials
298
299
  configure_data_at_rest_encryption_credentials
300
+ configure_registry_encryption_key
299
301
 
300
302
  # Reload the updated .env file
301
303
  source .env