@striae-org/striae 7.0.0 โ†’ 7.1.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 +8 -14
  2. package/app/components/canvas/canvas.module.css +12 -0
  3. package/app/components/canvas/canvas.tsx +26 -5
  4. package/functions/api/_shared/lists-client.ts +39 -0
  5. package/functions/api/_shared/registration-allowlist.ts +5 -4
  6. package/functions/api/auth/can-register.ts +7 -2
  7. package/functions/api/pdf/[[path]].ts +4 -1
  8. package/functions/api/user/[[path]].ts +11 -5
  9. package/package.json +10 -8
  10. package/scripts/deploy-all.sh +3 -3
  11. package/scripts/deploy-config/modules/prompt.sh +43 -7
  12. package/scripts/deploy-config/modules/scaffolding.sh +19 -0
  13. package/scripts/deploy-config/modules/validation.sh +3 -0
  14. package/scripts/deploy-config.sh +0 -33
  15. package/scripts/deploy-pages-secrets.sh +1 -10
  16. package/scripts/deploy-worker-secrets.sh +19 -1
  17. package/scripts/install-workers.sh +4 -3
  18. package/scripts/update-markdown-versions.cjs +1 -0
  19. package/workers/audit-worker/package.json +2 -2
  20. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  21. package/workers/data-worker/package.json +2 -2
  22. package/workers/data-worker/wrangler.jsonc.example +1 -1
  23. package/workers/image-worker/package.json +2 -2
  24. package/workers/image-worker/src/handlers/delete-image.ts +5 -5
  25. package/workers/image-worker/src/handlers/mint-signed-url.ts +5 -5
  26. package/workers/image-worker/src/handlers/serve-image.ts +7 -7
  27. package/workers/image-worker/src/handlers/upload-image.ts +4 -4
  28. package/workers/image-worker/src/image-worker.ts +4 -4
  29. package/workers/image-worker/src/router.ts +11 -11
  30. package/workers/image-worker/src/types.ts +1 -1
  31. package/workers/image-worker/wrangler.jsonc.example +1 -1
  32. package/workers/lists-worker/package.json +13 -0
  33. package/workers/lists-worker/src/lists-worker.ts +97 -0
  34. package/workers/lists-worker/src/types.ts +4 -0
  35. package/workers/lists-worker/wrangler.jsonc.example +23 -0
  36. package/workers/pdf-worker/package.json +2 -2
  37. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  38. package/workers/user-worker/package.json +2 -2
  39. package/workers/user-worker/src/handlers/user-routes.ts +26 -34
  40. package/workers/user-worker/src/types.ts +13 -0
  41. package/workers/user-worker/src/user-worker.ts +18 -24
  42. package/workers/user-worker/wrangler.jsonc.example +1 -1
  43. package/wrangler.toml.example +6 -2
  44. package/app/config-example/members.emails +0 -11
  45. package/app/config-example/primershear.emails +0 -6
  46. package/scripts/deploy-members-emails.sh +0 -102
  47. package/scripts/deploy-primershear-emails.sh +0 -101
package/.env.example CHANGED
@@ -110,17 +110,11 @@ PDF_WORKER_NAME=your_pdf_worker_name_here
110
110
  BROWSER_API_TOKEN=your_cloudflare_browser_rendering_api_token_here
111
111
 
112
112
  # ================================
113
- # PRIMERSHEAR PDF FORMAT
114
- # ================================
115
- # Comma-separated list of email addresses that will receive the primershear PDF format.
116
- # Leave empty to disable the feature. Never commit this value to source control.
117
- # Example: PRIMERSHEAR_EMAILS=analyst@org.com,user2@org.com
118
- PRIMERSHEAR_EMAILS=
119
-
120
- # ================================
121
- # REGISTRATION EMAIL ALLOWLIST CONFIGURATION
122
- # ================================
123
- # Comma-separated list of email addresses that may register an account.
124
- # Leave empty to disable the feature. Never commit this value to source control.
125
- # Example: REGISTRATION_EMAILS=analyst@org.com,user2@org.com
126
- REGISTRATION_EMAILS=
113
+ # LISTS WORKER ENVIRONMENT VARIABLES
114
+ # ================================
115
+ # The lists-worker manages registration and PDF format allowlists via KV.
116
+ # STRIAE_LISTS_KV_ID is the KV namespace ID backing both lists.
117
+ # LISTS_ADMIN_SECRET guards write endpoints (POST/DELETE); use a strong random value.
118
+ LISTS_WORKER_NAME=your_lists_worker_name_here
119
+ STRIAE_LISTS_KV_ID=your_striae_lists_kv_id_here
120
+ LISTS_ADMIN_SECRET=your_lists_admin_secret_here
@@ -309,6 +309,18 @@
309
309
  font-family: "Inter", sans-serif;
310
310
  }
311
311
 
312
+ .noteSection {
313
+ flex: 1;
314
+ display: flex;
315
+ flex-direction: column;
316
+ overflow: hidden;
317
+ min-height: 0;
318
+ }
319
+
320
+ .noteSection + .noteSection {
321
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
322
+ }
323
+
312
324
  .additionalNotesBox {
313
325
  flex: 1;
314
326
  overflow-y: auto;
@@ -424,12 +424,33 @@ export const Canvas = ({
424
424
  </div>{/* end imageContainer */}
425
425
 
426
426
  {/* Additional Notes - Right Panel */}
427
- {activeAnnotations?.has('notes') && annotationData?.additionalNotes && (
427
+ {activeAnnotations?.has('notes') &&
428
+ (annotationData?.leftAdditionalNotes || annotationData?.rightAdditionalNotes || annotationData?.additionalNotes) && (
428
429
  <aside className={styles.notesPanel} aria-label="Additional notes">
429
- <div className={styles.notesPanelHeader}>Notes</div>
430
- <div className={styles.additionalNotesBox}>
431
- {annotationData.additionalNotes}
432
- </div>
430
+ {annotationData?.leftAdditionalNotes && (
431
+ <div className={styles.noteSection}>
432
+ <div className={styles.notesPanelHeader}>Left Item</div>
433
+ <div className={styles.additionalNotesBox}>
434
+ {annotationData.leftAdditionalNotes}
435
+ </div>
436
+ </div>
437
+ )}
438
+ {annotationData?.rightAdditionalNotes && (
439
+ <div className={styles.noteSection}>
440
+ <div className={styles.notesPanelHeader}>Right Item</div>
441
+ <div className={styles.additionalNotesBox}>
442
+ {annotationData.rightAdditionalNotes}
443
+ </div>
444
+ </div>
445
+ )}
446
+ {annotationData?.additionalNotes && (
447
+ <div className={styles.noteSection}>
448
+ <div className={styles.notesPanelHeader}>Notes</div>
449
+ <div className={styles.additionalNotesBox}>
450
+ {annotationData.additionalNotes}
451
+ </div>
452
+ </div>
453
+ )}
433
454
  </aside>
434
455
  )}
435
456
  </div>
@@ -0,0 +1,39 @@
1
+ export type ListResult =
2
+ | { ok: true; list: string }
3
+ | { ok: false; error: string };
4
+
5
+ /**
6
+ * Client helper for reading email lists from the lists-worker via service binding.
7
+ * Returns a ListResult so callers can apply fail-open or fail-closed logic
8
+ * appropriate to their security context.
9
+ *
10
+ * - ok: true โ†’ list fetched successfully (may be empty string if list is empty)
11
+ * - ok: false โ†’ worker unreachable, auth failure, or unexpected response shape
12
+ */
13
+ export async function fetchListFromWorker(
14
+ binding: Fetcher,
15
+ list: 'members' | 'primershear',
16
+ secret: string
17
+ ): Promise<ListResult> {
18
+ try {
19
+ const response = await binding.fetch(`https://worker/${list}`, {
20
+ headers: { 'Authorization': `Bearer ${secret}` },
21
+ });
22
+ if (!response.ok) {
23
+ const msg = `lists-client: GET /${list} returned ${response.status}`;
24
+ console.error(msg);
25
+ return { ok: false, error: msg };
26
+ }
27
+ const data = await response.json() as { list?: unknown };
28
+ if (typeof data.list !== 'string') {
29
+ const msg = `lists-client: unexpected response shape for /${list}`;
30
+ console.error(msg);
31
+ return { ok: false, error: msg };
32
+ }
33
+ return { ok: true, list: data.list };
34
+ } catch (err) {
35
+ const msg = `lists-client: failed to fetch /${list}`;
36
+ console.error(msg, err);
37
+ return { ok: false, error: msg };
38
+ }
39
+ }
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * Checks whether the given email is permitted to register based on the
3
- * REGISTRATION_EMAILS secret (a comma-separated list of allowed entries).
3
+ * registration allowlist (a comma-separated list of allowed entries) sourced
4
+ * from the lists-worker KV (key: "allow").
4
5
  *
5
6
  * Each entry may be:
6
7
  * - An exact email address: user@example.com
7
8
  * - A domain wildcard: @example.com (matches any email from that domain)
8
9
  *
9
- * If registrationEmails is empty or unset, all registrations are allowed
10
- * (backward-compatible โ€” deploys without a members.emails file are unrestricted).
10
+ * If registrationEmails is empty or unset, registration is denied (fail closed).
11
+ * An empty list indicates the allowlist has not been populated, not that all are allowed.
11
12
  */
12
13
  export function isEmailAllowed(email: string, registrationEmails: string): boolean {
13
14
  if (!registrationEmails || registrationEmails.trim().length === 0) {
14
- return true;
15
+ return false;
15
16
  }
16
17
 
17
18
  const normalizedEmail = email.toLowerCase().trim();
@@ -1,4 +1,5 @@
1
1
  import { isEmailAllowed } from '../_shared/registration-allowlist';
2
+ import { fetchListFromWorker } from '../_shared/lists-client';
2
3
 
3
4
  interface CanRegisterContext {
4
5
  request: Request;
@@ -49,9 +50,13 @@ export const onRequest = async ({ request, env }: CanRegisterContext): Promise<R
49
50
  return textResponse('Missing required parameter: email', 400);
50
51
  }
51
52
 
52
- const registrationEmails = env.REGISTRATION_EMAILS ?? '';
53
+ const listResult = await fetchListFromWorker(env.LISTS_WORKER, 'members', env.LISTS_ADMIN_SECRET);
54
+ if (!listResult.ok) {
55
+ // Fail closed: cannot verify allowlist, deny to prevent bypass.
56
+ return textResponse('Unable to verify registration eligibility', 503);
57
+ }
53
58
 
54
- if (isEmailAllowed(email, registrationEmails)) {
59
+ if (isEmailAllowed(email, listResult.list)) {
55
60
  return jsonResponse({ allowed: true });
56
61
  }
57
62
 
@@ -1,4 +1,5 @@
1
1
  import { verifyFirebaseIdentityFromRequest } from '../_shared/firebase-auth';
2
+ import { fetchListFromWorker } from '../_shared/lists-client';
2
3
 
3
4
  interface PdfProxyContext {
4
5
  request: Request;
@@ -97,9 +98,11 @@ export const onRequest = async ({ request, env }: PdfProxyContext): Promise<Resp
97
98
 
98
99
  // Resolve the report format server-side based on the verified user email.
99
100
  // This prevents email lists from ever being exposed in the client bundle.
101
+ // Fail-open: if the lists-worker is unavailable, fall back to the default format.
102
+ const primershearResult = await fetchListFromWorker(env.LISTS_WORKER, 'primershear', env.LISTS_ADMIN_SECRET);
100
103
  const reportFormat = resolveReportFormat(
101
104
  identity.email,
102
- env.PRIMERSHEAR_EMAILS ?? ''
105
+ primershearResult.ok ? primershearResult.list : ''
103
106
  );
104
107
 
105
108
  let upstreamBody: BodyInit;
@@ -1,5 +1,6 @@
1
1
  import { verifyFirebaseIdentityFromRequest } from '../_shared/firebase-auth';
2
2
  import { isEmailAllowed } from '../_shared/registration-allowlist';
3
+ import { fetchListFromWorker } from '../_shared/lists-client';
3
4
 
4
5
  interface UserProxyContext {
5
6
  request: Request;
@@ -142,9 +143,14 @@ export const onRequest = async ({ request, env }: UserProxyContext): Promise<Res
142
143
  }
143
144
 
144
145
  // Registration gateway: for PUT requests, check if this is a new user creation.
145
- // If REGISTRATION_EMAILS is set and the user record does not yet exist, enforce the allowlist.
146
+ // Always enforce the allowlist for new users โ€” isEmailAllowed fails closed for empty lists.
146
147
  // This is defense-in-depth โ€” the primary check runs client-side in the login flow.
147
- if (request.method === 'PUT' && env.REGISTRATION_EMAILS && env.REGISTRATION_EMAILS.trim().length > 0) {
148
+ if (request.method === 'PUT') {
149
+ const listResult = await fetchListFromWorker(env.LISTS_WORKER, 'members', env.LISTS_ADMIN_SECRET);
150
+ if (!listResult.ok) {
151
+ // Fail closed: cannot verify allowlist, reject to prevent bypass.
152
+ return textResponse('Unable to verify registration eligibility', 503);
153
+ }
148
154
  try {
149
155
  const existenceResponse = await env.USER_WORKER.fetch(
150
156
  `https://worker/${encodeURIComponent(requestedUserId)}`,
@@ -158,8 +164,8 @@ export const onRequest = async ({ request, env }: UserProxyContext): Promise<Res
158
164
 
159
165
  if (existenceResponse.status === 404) {
160
166
  // User does not exist yet โ€” this is a registration PUT.
161
- // Enforce the email allowlist.
162
- if (!isEmailAllowed(identity.email ?? '', env.REGISTRATION_EMAILS)) {
167
+ // Enforce the email allowlist (isEmailAllowed returns false for empty list).
168
+ if (!isEmailAllowed(identity.email ?? '', listResult.list)) {
163
169
  return textResponse('Registration is not permitted for this email address', 403);
164
170
  }
165
171
  } else if (!existenceResponse.ok) {
@@ -169,7 +175,7 @@ export const onRequest = async ({ request, env }: UserProxyContext): Promise<Res
169
175
  }
170
176
  // If user already exists (200), proceed normally.
171
177
  } catch {
172
- // Fail closed: on network error with allowlist active, reject the request.
178
+ // Fail closed: on network error, reject the request.
173
179
  return textResponse('Unable to verify registration eligibility', 502);
174
180
  }
175
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "7.0.0",
3
+ "version": "7.1.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",
@@ -89,16 +89,15 @@
89
89
  "deploy-config": "bash ./scripts/deploy-config.sh",
90
90
  "update-env": "bash ./scripts/deploy-config.sh --update-env",
91
91
  "install-workers": "bash ./scripts/install-workers.sh",
92
- "deploy-workers": "npm run deploy-workers:audit && npm run deploy-workers:data && npm run deploy-workers:image && npm run deploy-workers:pdf && npm run deploy-workers:user",
92
+ "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",
93
93
  "deploy-workers:secrets": "bash ./scripts/deploy-worker-secrets.sh",
94
94
  "deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh",
95
95
  "deploy-pages": "bash ./scripts/deploy-pages.sh",
96
- "deploy-primershear": "bash ./scripts/deploy-primershear-emails.sh",
97
- "deploy-members": "bash ./scripts/deploy-members-emails.sh",
98
96
  "deploy-workers:audit": "cd workers/audit-worker && npm run deploy",
99
97
  "deploy-workers:data": "cd workers/data-worker && npm run deploy",
100
98
  "deploy-workers:image": "cd workers/image-worker && npm run deploy",
101
99
  "deploy-workers:pdf": "cd workers/pdf-worker && npm run deploy",
100
+ "deploy-workers:lists": "cd workers/lists-worker && npm run deploy",
102
101
  "deploy-workers:user": "cd workers/user-worker && npm run deploy"
103
102
  },
104
103
  "dependencies": {
@@ -112,7 +111,7 @@
112
111
  "react-router": "^7.14.2"
113
112
  },
114
113
  "devDependencies": {
115
- "@cloudflare/vitest-pool-workers": "^0.14.9",
114
+ "@cloudflare/vitest-pool-workers": "^0.15.0",
116
115
  "@react-router/dev": "^7.14.2",
117
116
  "@react-router/fs-routes": "^7.14.2",
118
117
  "@types/qrcode": "^1.5.6",
@@ -130,12 +129,15 @@
130
129
  "firebase-admin": "^13.8.0",
131
130
  "modern-normalize": "^3.0.1",
132
131
  "typescript": "^6.0.3",
133
- "vite": "^8.0.9",
132
+ "vite": "^8.0.10",
134
133
  "vitest": "^4.1.5",
135
- "wrangler": "^4.84.1"
134
+ "wrangler": "^4.85.0"
136
135
  },
137
136
  "overrides": {
138
- "@tootallnate/once": "3.0.1"
137
+ "@tootallnate/once": "3.0.1",
138
+ "firebase-admin": {
139
+ "uuid": "^14.0.0"
140
+ }
139
141
  },
140
142
  "engines": {
141
143
  "node": ">=20.19.0"
@@ -110,7 +110,7 @@ if ! npx wrangler types; then
110
110
  echo -e "${RED}โŒ Root wrangler types generation failed!${NC}"
111
111
  exit 1
112
112
  fi
113
- for WORKER in audit-worker data-worker image-worker pdf-worker user-worker; do
113
+ for WORKER in audit-worker data-worker image-worker lists-worker pdf-worker user-worker; do
114
114
  echo -e "${YELLOW} โ†’ Generating types for ${WORKER}...${NC}"
115
115
  if ! (cd "workers/$WORKER" && npx wrangler types); then
116
116
  echo -e "${RED}โŒ wrangler types failed for ${WORKER}!${NC}"
@@ -123,7 +123,7 @@ echo ""
123
123
  # Step 4: Deploy Workers
124
124
  echo -e "${PURPLE}Step 4/7: Deploying Workers${NC}"
125
125
  echo "----------------------------"
126
- echo -e "${YELLOW}๐Ÿ”ง Deploying all 5 Cloudflare Workers...${NC}"
126
+ echo -e "${YELLOW}๐Ÿ”ง Deploying all 6 Cloudflare Workers...${NC}"
127
127
  if ! npm run deploy-workers; then
128
128
  echo -e "${RED}โŒ Worker deployment failed!${NC}"
129
129
  exit 1
@@ -172,7 +172,7 @@ echo ""
172
172
  echo -e "${BLUE}Deployed Components:${NC}"
173
173
  echo " โœ… Worker dependencies (npm install)"
174
174
  echo " โœ… Wrangler types (root + all workers)"
175
- echo " โœ… 5 Cloudflare Workers"
175
+ echo " โœ… 6 Cloudflare Workers"
176
176
  echo " โœ… Worker environment variables"
177
177
  echo " โœ… Pages environment variables"
178
178
  echo " โœ… Cloudflare Pages frontend"
@@ -23,7 +23,7 @@ prompt_for_secrets() {
23
23
  is_auto_generated_secret_var() {
24
24
  local var_name=$1
25
25
  case "$var_name" in
26
- IMAGE_SIGNED_URL_SECRET)
26
+ IMAGE_SIGNED_URL_SECRET|LISTS_ADMIN_SECRET)
27
27
  return 0
28
28
  ;;
29
29
  *)
@@ -39,6 +39,9 @@ prompt_for_secrets() {
39
39
  IMAGE_SIGNED_URL_SECRET)
40
40
  [ "$value" = "your_image_signed_url_secret_here" ]
41
41
  ;;
42
+ LISTS_ADMIN_SECRET)
43
+ [ "$value" = "your_lists_admin_secret_here" ]
44
+ ;;
42
45
  *)
43
46
  return 1
44
47
  ;;
@@ -223,13 +226,44 @@ prompt_for_secrets() {
223
226
 
224
227
  echo -e "${BLUE}๐Ÿ”‘ WORKER NAMES${NC}"
225
228
  echo "==============="
226
- echo -e "${YELLOW}Worker names are lowercased automatically and must use only letters, numbers, and dashes.${NC}"
229
+ echo -e "${YELLOW}Worker names are auto-generated and lowercased. Press Enter to keep the generated value or type a custom name.${NC}"
230
+
231
+ # Auto-generate each worker name if not yet set or still a placeholder.
232
+ _gen_worker_name() {
233
+ local var_name=$1
234
+ local current="${!var_name:-}"
235
+ if is_placeholder "$current" || [ -z "$current" ]; then
236
+ local suffix
237
+ suffix=$(openssl rand -base64 16 2>/dev/null | tr -dc 'a-z0-9' | head -c 10 || true)
238
+ if [ -n "$suffix" ]; then
239
+ printf '%s' "striae-dev-${suffix}"
240
+ fi
241
+ fi
242
+ }
243
+
244
+ _new=$( _gen_worker_name "USER_WORKER_NAME")
245
+ [ -n "$_new" ] && { USER_WORKER_NAME="$_new"; export USER_WORKER_NAME; }
246
+ prompt_for_var "USER_WORKER_NAME" "User worker name (auto-generated; change only if using an existing worker)"
247
+
248
+ _new=$(_gen_worker_name "DATA_WORKER_NAME")
249
+ [ -n "$_new" ] && { DATA_WORKER_NAME="$_new"; export DATA_WORKER_NAME; }
250
+ prompt_for_var "DATA_WORKER_NAME" "Data worker name (auto-generated; change only if using an existing worker)"
251
+
252
+ _new=$(_gen_worker_name "AUDIT_WORKER_NAME")
253
+ [ -n "$_new" ] && { AUDIT_WORKER_NAME="$_new"; export AUDIT_WORKER_NAME; }
254
+ prompt_for_var "AUDIT_WORKER_NAME" "Audit worker name (auto-generated; change only if using an existing worker)"
255
+
256
+ _new=$(_gen_worker_name "IMAGES_WORKER_NAME")
257
+ [ -n "$_new" ] && { IMAGES_WORKER_NAME="$_new"; export IMAGES_WORKER_NAME; }
258
+ prompt_for_var "IMAGES_WORKER_NAME" "Images worker name (auto-generated; change only if using an existing worker)"
259
+
260
+ _new=$(_gen_worker_name "PDF_WORKER_NAME")
261
+ [ -n "$_new" ] && { PDF_WORKER_NAME="$_new"; export PDF_WORKER_NAME; }
262
+ prompt_for_var "PDF_WORKER_NAME" "PDF worker name (auto-generated; change only if using an existing worker)"
227
263
 
228
- prompt_for_var "USER_WORKER_NAME" "User worker name"
229
- prompt_for_var "DATA_WORKER_NAME" "Data worker name"
230
- prompt_for_var "AUDIT_WORKER_NAME" "Audit worker name"
231
- prompt_for_var "IMAGES_WORKER_NAME" "Images worker name"
232
- prompt_for_var "PDF_WORKER_NAME" "PDF worker name"
264
+ _new=$(_gen_worker_name "LISTS_WORKER_NAME")
265
+ [ -n "$_new" ] && { LISTS_WORKER_NAME="$_new"; export LISTS_WORKER_NAME; }
266
+ prompt_for_var "LISTS_WORKER_NAME" "Lists worker name (auto-generated; change only if using an existing worker)"
233
267
  echo ""
234
268
 
235
269
  echo -e "${BLUE}๐Ÿ—„๏ธ STORAGE CONFIGURATION${NC}"
@@ -238,6 +272,7 @@ prompt_for_secrets() {
238
272
  prompt_for_var "AUDIT_BUCKET_NAME" "Your R2 bucket name for audit logs (separate from data bucket)"
239
273
  prompt_for_var "FILES_BUCKET_NAME" "Your R2 bucket name for encrypted files storage"
240
274
  prompt_for_var "KV_STORE_ID" "Your KV namespace ID (UUID format)"
275
+ prompt_for_var "STRIAE_LISTS_KV_ID" "KV namespace ID for the lists-worker (UUID format; backs registration and primershear allowlists)"
241
276
 
242
277
  echo -e "${BLUE}๐Ÿ” SERVICE-SPECIFIC SECRETS${NC}"
243
278
  echo "============================"
@@ -255,6 +290,7 @@ prompt_for_secrets() {
255
290
  prompt_for_var "IMAGE_SIGNED_URL_BASE_URL" "Signed URL delivery base URL โ€” routes signed image delivery through the Pages proxy (leave as-is unless using a non-standard domain)"
256
291
 
257
292
  prompt_for_var "BROWSER_API_TOKEN" "Cloudflare Browser Rendering API token (for PDF Worker)"
293
+ prompt_for_var "LISTS_ADMIN_SECRET" "Lists worker admin secret โ€” guards write endpoints (auto-generated; guards POST/DELETE on the lists-worker)"
258
294
 
259
295
  configure_manifest_signing_credentials
260
296
  configure_export_encryption_credentials
@@ -106,6 +106,14 @@ copy_example_configs() {
106
106
  echo -e "${YELLOW} โš ๏ธ pdf-worker: wrangler.jsonc already exists, skipping copy${NC}"
107
107
  fi
108
108
 
109
+ cd ../lists-worker
110
+ if [ -f "wrangler.jsonc.example" ] && { [ "$update_env" = "true" ] || [ ! -f "wrangler.jsonc" ]; }; then
111
+ cp wrangler.jsonc.example wrangler.jsonc
112
+ echo -e "${GREEN} โœ… lists-worker: wrangler.jsonc created from example${NC}"
113
+ elif [ -f "wrangler.jsonc" ]; then
114
+ echo -e "${YELLOW} โš ๏ธ lists-worker: wrangler.jsonc already exists, skipping copy${NC}"
115
+ fi
116
+
109
117
  # Return to project root
110
118
  cd ../..
111
119
 
@@ -172,6 +180,16 @@ update_wrangler_configs() {
172
180
  echo -e "${GREEN} โœ… pdf-worker configuration updated${NC}"
173
181
  fi
174
182
 
183
+ if [ -f "workers/lists-worker/wrangler.jsonc" ]; then
184
+ echo -e "${YELLOW} Updating lists-worker/wrangler.jsonc...${NC}"
185
+ local escaped_striae_lists_kv_id
186
+ escaped_striae_lists_kv_id=$(escape_for_sed_replacement "$STRIAE_LISTS_KV_ID")
187
+ sed -i "s/\"LISTS_WORKER_NAME\"/\"$LISTS_WORKER_NAME\"/g" workers/lists-worker/wrangler.jsonc
188
+ sed -i "s/\"ACCOUNT_ID\"/\"$escaped_account_id\"/g" workers/lists-worker/wrangler.jsonc
189
+ sed -i "s/\"STRIAE_LISTS_KV_ID\"/\"$escaped_striae_lists_kv_id\"/g" workers/lists-worker/wrangler.jsonc
190
+ echo -e "${GREEN} โœ… lists-worker configuration updated${NC}"
191
+ fi
192
+
175
193
  if [ -f "workers/user-worker/wrangler.jsonc" ]; then
176
194
  echo -e "${YELLOW} Updating user-worker/wrangler.jsonc...${NC}"
177
195
  sed -i "s/\"USER_WORKER_NAME\"/\"$USER_WORKER_NAME\"/g" workers/user-worker/wrangler.jsonc
@@ -190,6 +208,7 @@ update_wrangler_configs() {
190
208
  sed -i "s/AUDIT_WORKER_NAME/$AUDIT_WORKER_NAME/g" wrangler.toml
191
209
  sed -i "s/IMAGES_WORKER_NAME/$IMAGES_WORKER_NAME/g" wrangler.toml
192
210
  sed -i "s/PDF_WORKER_NAME/$PDF_WORKER_NAME/g" wrangler.toml
211
+ sed -i "s/LISTS_WORKER_NAME/$LISTS_WORKER_NAME/g" wrangler.toml
193
212
  echo -e "${GREEN} โœ… main wrangler.toml configuration updated${NC}"
194
213
  fi
195
214
 
@@ -96,12 +96,14 @@ required_vars=(
96
96
  "AUDIT_WORKER_NAME"
97
97
  "IMAGES_WORKER_NAME"
98
98
  "PDF_WORKER_NAME"
99
+ "LISTS_WORKER_NAME"
99
100
 
100
101
  # Storage Configuration (required for config replacement)
101
102
  "DATA_BUCKET_NAME"
102
103
  "AUDIT_BUCKET_NAME"
103
104
  "FILES_BUCKET_NAME"
104
105
  "KV_STORE_ID"
106
+ "STRIAE_LISTS_KV_ID"
105
107
 
106
108
  # Worker-Specific Secrets (required for deployment)
107
109
  "IMAGE_SIGNED_URL_SECRET"
@@ -112,6 +114,7 @@ required_vars=(
112
114
  "EXPORT_ENCRYPTION_PRIVATE_KEY"
113
115
  "EXPORT_ENCRYPTION_KEY_ID"
114
116
  "EXPORT_ENCRYPTION_PUBLIC_KEY"
117
+ "LISTS_ADMIN_SECRET"
115
118
  )
116
119
 
117
120
  validate_required_vars() {
@@ -200,36 +200,6 @@ source "$DEPLOY_CONFIG_VALIDATION_MODULE"
200
200
  source "$DEPLOY_CONFIG_SCAFFOLDING_MODULE"
201
201
  source "$DEPLOY_CONFIG_PROMPT_MODULE"
202
202
 
203
- EMAIL_LIST_CONFIG_DIR="app/config"
204
- MEMBERS_EMAILS_FILE="$EMAIL_LIST_CONFIG_DIR/members.emails"
205
- PRIMERSHEAR_EMAILS_FILE="$EMAIL_LIST_CONFIG_DIR/primershear.emails"
206
-
207
- sync_env_var_from_email_list_file() {
208
- local env_var_name=$1
209
- local file_path=$2
210
- local loaded_values=""
211
-
212
- if [ ! -f "$file_path" ]; then
213
- echo -e "${YELLOW}โš ๏ธ $file_path not found; keeping existing $env_var_name value in .env${NC}"
214
- return 0
215
- fi
216
-
217
- loaded_values=$(grep -v '^[[:space:]]*#' "$file_path" | grep -v '^[[:space:]]*$' | sed -e 's/\r$//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | paste -sd ',' - || true)
218
-
219
- write_env_var "$env_var_name" "$loaded_values"
220
- export "$env_var_name=$loaded_values"
221
-
222
- local loaded_count
223
- loaded_count=$(echo "$loaded_values" | tr ',' '\n' | grep -c '[^[:space:]]' || true)
224
- echo -e "${GREEN}โœ… Synced $env_var_name from $file_path ($loaded_count entry/entries)${NC}"
225
- }
226
-
227
- sync_email_list_env_vars_from_config() {
228
- echo -e "${YELLOW}๐Ÿ“ง Syncing optional email list env vars from app/config...${NC}"
229
- sync_env_var_from_email_list_file "REGISTRATION_EMAILS" "$MEMBERS_EMAILS_FILE"
230
- sync_env_var_from_email_list_file "PRIMERSHEAR_EMAILS" "$PRIMERSHEAR_EMAILS_FILE"
231
- }
232
-
233
203
  if [ "$validate_only" = "true" ]; then
234
204
  echo -e "\n${BLUE}๐Ÿงช Validate-only mode enabled${NC}"
235
205
  run_validation_checkpoint
@@ -247,9 +217,6 @@ load_admin_service_credentials
247
217
  # Always prompt for secrets to ensure configuration
248
218
  prompt_for_secrets
249
219
 
250
- # Keep optional email list env vars aligned with app/config source files.
251
- sync_email_list_env_vars_from_config
252
-
253
220
  # Validate after secrets have been configured
254
221
  validate_required_vars
255
222
 
@@ -117,16 +117,6 @@ deploy_pages_secrets() {
117
117
  printf '%s' "$secret_value" | wrangler pages secret put "$secret" --project-name "$PAGES_PROJECT_NAME"
118
118
  done
119
119
 
120
- local optional_primershear_emails
121
- optional_primershear_emails=$(get_optional_value "PRIMERSHEAR_EMAILS")
122
- echo -e "${YELLOW} Setting PRIMERSHEAR_EMAILS...${NC}"
123
- printf '%s' "$optional_primershear_emails" | wrangler pages secret put "PRIMERSHEAR_EMAILS" --project-name "$PAGES_PROJECT_NAME"
124
-
125
- local optional_registration_emails
126
- optional_registration_emails=$(get_optional_value "REGISTRATION_EMAILS")
127
- echo -e "${YELLOW} Setting REGISTRATION_EMAILS...${NC}"
128
- printf '%s' "$optional_registration_emails" | wrangler pages secret put "REGISTRATION_EMAILS" --project-name "$PAGES_PROJECT_NAME"
129
-
130
120
  echo -e "${GREEN}โœ… Pages secrets deployed to production${NC}"
131
121
  }
132
122
 
@@ -152,6 +142,7 @@ fi
152
142
 
153
143
  required_pages_secrets=(
154
144
  "PROJECT_ID"
145
+ "LISTS_ADMIN_SECRET"
155
146
  )
156
147
 
157
148
  echo -e "${YELLOW}๐Ÿ” Validating required Pages secret values...${NC}"
@@ -267,6 +267,13 @@ build_data_worker_secret_list() {
267
267
  printf '%s\n' "${secrets[@]}"
268
268
  }
269
269
 
270
+ build_lists_worker_secret_list() {
271
+ local secrets=(
272
+ "LISTS_ADMIN_SECRET"
273
+ )
274
+ printf '%s\n' "${secrets[@]}"
275
+ }
276
+
270
277
  build_images_worker_secret_list() {
271
278
  local secrets=(
272
279
  "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY"
@@ -299,7 +306,7 @@ echo -e "\n${BLUE}๐Ÿ” Deploying secrets to workers...${NC}"
299
306
  # Check if workers are configured
300
307
  echo -e "${YELLOW}๐Ÿ” Checking worker configurations...${NC}"
301
308
  workers_configured=0
302
- total_workers=5
309
+ total_workers=6
303
310
 
304
311
  for worker_dir in workers/*/; do
305
312
  if [ -f "$worker_dir/wrangler.jsonc" ] || [ -f "$worker_dir/wrangler.toml" ]; then
@@ -363,6 +370,16 @@ if ! set_worker_secrets "PDF Worker" "workers/pdf-worker" \
363
370
  echo -e "${YELLOW}โš ๏ธ Skipping PDF Worker (not configured)${NC}"
364
371
  fi
365
372
 
373
+ # Lists Worker
374
+ lists_worker_secrets=()
375
+ while IFS= read -r secret; do
376
+ lists_worker_secrets+=("$secret")
377
+ done < <(build_lists_worker_secret_list)
378
+
379
+ if ! set_worker_secrets "Lists Worker" "workers/lists-worker" "${lists_worker_secrets[@]}"; then
380
+ echo -e "${YELLOW}โš ๏ธ Skipping Lists Worker (not configured)${NC}"
381
+ fi
382
+
366
383
  echo -e "\n${GREEN}๐ŸŽ‰ Worker secrets deployment completed!${NC}"
367
384
 
368
385
  echo -e "\n${YELLOW}โš ๏ธ WORKER CONFIGURATION REMINDERS:${NC}"
@@ -371,6 +388,7 @@ echo " - Configure KV namespace ID in workers/user-worker/wrangler.jsonc"
371
388
  echo " - Configure R2 bucket name in workers/data-worker/wrangler.jsonc"
372
389
  echo " - Configure R2 bucket name in workers/audit-worker/wrangler.jsonc"
373
390
  echo " - Configure R2 bucket name in workers/image-worker/wrangler.jsonc"
391
+ echo " - Configure KV namespace ID in workers/lists-worker/wrangler.jsonc"
374
392
  echo " - Update ACCOUNT_ID and custom domains in all worker configurations"
375
393
 
376
394
  echo -e "\n${BLUE}๐Ÿ“ For manual deployment, use these commands:${NC}"
@@ -7,8 +7,9 @@
7
7
  # 1. audit-worker
8
8
  # 2. data-worker
9
9
  # 3. image-worker
10
- # 4. pdf-worker
11
- # 5. user-worker
10
+ # 4. lists-worker
11
+ # 5. pdf-worker
12
+ # 6. user-worker
12
13
 
13
14
  # Colors for output
14
15
  RED='\033[0;31m'
@@ -34,7 +35,7 @@ if [ ! -d "$WORKERS_DIR" ]; then
34
35
  fi
35
36
 
36
37
  # List of workers
37
- WORKERS=("audit-worker" "data-worker" "image-worker" "pdf-worker" "user-worker")
38
+ WORKERS=("audit-worker" "data-worker" "image-worker" "lists-worker" "pdf-worker" "user-worker")
38
39
 
39
40
  echo -e "${PURPLE}Installing npm dependencies for all workers...${NC}"
40
41
  echo ""
@@ -11,6 +11,7 @@ const workerDirs = [
11
11
  'workers/audit-worker',
12
12
  'workers/data-worker',
13
13
  'workers/image-worker',
14
+ 'workers/lists-worker',
14
15
  'workers/pdf-worker',
15
16
  'workers/user-worker',
16
17
  ];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audit-worker",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -8,6 +8,6 @@
8
8
  "start": "wrangler dev"
9
9
  },
10
10
  "devDependencies": {
11
- "wrangler": "^4.84.1"
11
+ "wrangler": "^4.85.0"
12
12
  }
13
13
  }
@@ -3,7 +3,7 @@
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/audit-worker.ts",
5
5
  "workers_dev": false,
6
- "compatibility_date": "2026-04-21",
6
+ "compatibility_date": "2026-04-25",
7
7
  "compatibility_flags": [
8
8
  "nodejs_compat"
9
9
  ],