@striae-org/striae 5.5.2 → 6.0.1

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 (41) hide show
  1. package/README.md +3 -1
  2. package/app/components/actions/case-export/download-handlers.ts +130 -62
  3. package/app/components/actions/case-manage/archive-package-builder.ts +299 -0
  4. package/app/components/actions/case-manage/delete-helpers.ts +61 -0
  5. package/app/components/actions/case-manage/index.ts +2 -0
  6. package/app/components/actions/case-manage/operations.ts +714 -0
  7. package/app/components/actions/case-manage/types.ts +21 -0
  8. package/app/components/actions/case-manage/utils.ts +34 -0
  9. package/app/components/actions/case-manage.ts +1 -1079
  10. package/app/components/navbar/case-import/case-import.module.css +2 -2
  11. package/app/components/navbar/case-import/case-import.tsx +0 -8
  12. package/app/components/navbar/case-import/components/CasePreviewSection.tsx +1 -1
  13. package/app/components/navbar/case-modals/all-cases-modal.tsx +13 -1
  14. package/app/components/navbar/navbar.tsx +8 -5
  15. package/app/components/sidebar/cases/case-sidebar.tsx +3 -2
  16. package/app/components/sidebar/sidebar-container.tsx +7 -0
  17. package/{members.emails.example → app/config-example/members.emails} +1 -1
  18. package/{primershear.emails.example → app/config-example/primershear.emails} +1 -1
  19. package/app/routes/striae/striae.tsx +36 -11
  20. package/app/types/export.ts +1 -0
  21. package/app/utils/forensics/SHA256.ts +2 -2
  22. package/app/utils/forensics/audit-export-signature.ts +1 -1
  23. package/app/utils/forensics/confirmation-signature.ts +1 -1
  24. package/app/utils/forensics/signature-utils.ts +7 -2
  25. package/package.json +2 -4
  26. package/scripts/deploy-config.sh +33 -0
  27. package/scripts/deploy-members-emails.sh +4 -4
  28. package/scripts/deploy-primershear-emails.sh +3 -3
  29. package/workers/audit-worker/package.json +1 -1
  30. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  31. package/workers/data-worker/package.json +1 -1
  32. package/workers/data-worker/src/signature-utils.ts +7 -2
  33. package/workers/data-worker/src/signing-payload-utils.ts +4 -4
  34. package/workers/data-worker/wrangler.jsonc.example +1 -1
  35. package/workers/image-worker/package.json +1 -1
  36. package/workers/image-worker/wrangler.jsonc.example +1 -1
  37. package/workers/pdf-worker/package.json +1 -1
  38. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  39. package/workers/user-worker/package.json +1 -1
  40. package/workers/user-worker/wrangler.jsonc.example +1 -1
  41. package/wrangler.toml.example +1 -1
@@ -433,7 +433,7 @@
433
433
  }
434
434
 
435
435
  .archivedImportNote {
436
- margin-bottom: var(--spaceM);
436
+ margin: var(--spaceM) 0;
437
437
  padding: var(--spaceS) var(--spaceM);
438
438
  border-radius: var(--spaceXS);
439
439
  background: color-mix(in lab, var(--success) 10%, transparent);
@@ -444,7 +444,7 @@
444
444
  }
445
445
 
446
446
  .archivedRegularCaseRiskNote {
447
- margin-bottom: var(--spaceM);
447
+ margin: var(--spaceM) 0;
448
448
  padding: var(--spaceS) var(--spaceM);
449
449
  border-radius: var(--spaceXS);
450
450
  background: color-mix(in lab, var(--warning) 12%, transparent);
@@ -239,7 +239,6 @@ export const CaseImport = ({
239
239
  if (!user || !importState.selectedFile || !importState.importType) return;
240
240
 
241
241
  if (importState.importType === 'case' && isArchivedRegularCaseImportBlocked) {
242
- setError(ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE);
243
242
  return;
244
243
  }
245
244
 
@@ -260,7 +259,6 @@ export const CaseImport = ({
260
259
  casePreview,
261
260
  updateImportState,
262
261
  executeImport,
263
- setError,
264
262
  ]);
265
263
 
266
264
  const handleCancelImport = useCallback(() => {
@@ -333,7 +331,6 @@ export const CaseImport = ({
333
331
  // Handle confirmation import
334
332
  const handleConfirmImport = useCallback(() => {
335
333
  if (isArchivedRegularCaseImportBlocked) {
336
- setError(ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE);
337
334
  return;
338
335
  }
339
336
 
@@ -343,7 +340,6 @@ export const CaseImport = ({
343
340
  isArchivedRegularCaseImportBlocked,
344
341
  executeImport,
345
342
  updateImportState,
346
- setError,
347
343
  ]);
348
344
 
349
345
  if (!isOpen) return null;
@@ -414,10 +410,6 @@ export const CaseImport = ({
414
410
  {/* Import progress */}
415
411
  <ProgressSection importProgress={importProgress} />
416
412
 
417
- {isArchivedRegularCaseImportBlocked && (
418
- <div className={styles.error}>{ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE}</div>
419
- )}
420
-
421
413
  {/* Success message */}
422
414
  {messages.success && (
423
415
  <div className={styles.success}>
@@ -82,7 +82,7 @@ export const CasePreviewSection = ({
82
82
  </div>
83
83
  )}
84
84
  </div>
85
- {casePreview.archived && (
85
+ {casePreview.archived && !isArchivedRegularCaseImportBlocked && (
86
86
  <div className={styles.archivedImportNote}>
87
87
  {ARCHIVED_SELF_IMPORT_NOTE}
88
88
  </div>
@@ -225,6 +225,14 @@ export const CasesModal = ({
225
225
  selectedCase && selectedCase.caseNumber !== currentCase && !selectedCase.isReadOnly
226
226
  );
227
227
 
228
+ const deleteSelectedCaseTitle = !selectedCase
229
+ ? 'Select a case to delete.'
230
+ : selectedCase.caseNumber === currentCase
231
+ ? 'Open a different case before deleting this one.'
232
+ : selectedCase.isReadOnly
233
+ ? 'Read-only review cases cannot be deleted. Use Clear RO Case under Case Management first.'
234
+ : undefined;
235
+
228
236
  useEffect(() => {
229
237
  setCurrentPage(0);
230
238
  }, [preferences.sortBy, preferences.confirmationFilter, preferences.showArchivedOnly]);
@@ -484,12 +492,15 @@ export const CasesModal = ({
484
492
  const handleDeleteSelectedCase = async () => {
485
493
  if (!selectedCase || !canDeleteSelectedCase) {
486
494
  const isCurrentCaseSelection = selectedCase?.caseNumber === currentCase;
495
+ const isReadOnlyReviewSelection = selectedCase?.isReadOnly === true;
487
496
 
488
497
  setActionNotice({
489
498
  type: 'warning',
490
499
  message: isCurrentCaseSelection
491
500
  ? 'Open a different case before deleting this one.'
492
- : 'Selected case cannot be deleted.',
501
+ : isReadOnlyReviewSelection
502
+ ? 'Read-only review cases cannot be deleted. Use Clear RO Case under Case Management first.'
503
+ : 'Selected case cannot be deleted.',
493
504
  });
494
505
  return;
495
506
  }
@@ -785,6 +796,7 @@ export const CasesModal = ({
785
796
  className={`${styles.secondaryActionButton} ${styles.deleteActionButton}`}
786
797
  onClick={handleDeleteSelectedCase}
787
798
  disabled={!canDeleteSelectedCase || isRunningAction}
799
+ title={deleteSelectedCaseTitle}
788
800
  >
789
801
  Delete Selected
790
802
  </button>
@@ -121,6 +121,11 @@ export const Navbar = ({
121
121
  const isImageNotesActive = canOpenImageNotes;
122
122
  const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
123
123
  const isArchivedRegularReadOnly = Boolean(isReadOnly && archiveDetails?.archived && !isReviewOnlyCase);
124
+ const caseExportLabel = isArchivedRegularReadOnly
125
+ ? 'Export Archive'
126
+ : isReadOnly
127
+ ? 'Export Confirmations'
128
+ : 'Export Case Package';
124
129
 
125
130
  return (
126
131
  <>
@@ -179,11 +184,9 @@ export const Navbar = ({
179
184
  type="button"
180
185
  role="menuitem"
181
186
  className={`${styles.caseMenuItem} ${styles.caseMenuItemExport}`}
182
- disabled={!hasLoadedCase || disableLongRunningCaseActions || isArchivedRegularReadOnly}
187
+ disabled={!hasLoadedCase || disableLongRunningCaseActions}
183
188
  title={
184
- isArchivedRegularReadOnly
185
- ? 'Export is unavailable for archived cases loaded from your regular case list'
186
- : !hasLoadedCase
189
+ !hasLoadedCase
187
190
  ? 'Load a case to export case data'
188
191
  : disableLongRunningCaseActions
189
192
  ? 'Export is unavailable while files are uploading'
@@ -194,7 +197,7 @@ export const Navbar = ({
194
197
  setIsCaseMenuOpen(false);
195
198
  }}
196
199
  >
197
- {isReadOnly ? 'Export Confirmations' : 'Export Case Package'}
200
+ {caseExportLabel}
198
201
  </button>
199
202
  <button
200
203
  type="button"
@@ -258,7 +258,8 @@ const handleImageSelect = (file: FileData) => {
258
258
  ? 'Select an image first'
259
259
  : undefined;
260
260
 
261
- const showCaseExportButton = Boolean(currentCase && isReadOnly && !isArchivedCase);
261
+ const showCaseExportButton = Boolean(currentCase && isReadOnly);
262
+ const caseExportButtonLabel = isArchivedCase ? 'Export Archive' : 'Export Confirmations';
262
263
 
263
264
  const exportCaseTitle = isUploading
264
265
  ? 'Cannot export while uploading'
@@ -391,7 +392,7 @@ return (
391
392
  disabled={isUploading || !currentCase}
392
393
  title={exportCaseTitle}
393
394
  >
394
- Export Confirmations
395
+ {caseExportButtonLabel}
395
396
  </button>
396
397
  ) : (
397
398
  <button
@@ -109,6 +109,13 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
109
109
  rel="noopener noreferrer"
110
110
  className={styles.footerModalLink}>
111
111
  Security Policy
112
+ </Link>
113
+ <Link
114
+ to="https://community.striae.org"
115
+ target="_blank"
116
+ rel="noopener noreferrer"
117
+ className={styles.footerModalLink}>
118
+ Striae Community
112
119
  </Link>
113
120
  </div>
114
121
 
@@ -8,4 +8,4 @@
8
8
  #
9
9
  # Examples:
10
10
  # analyst@organization.com
11
- # @striae.org
11
+ # @striae.org
@@ -3,4 +3,4 @@
3
3
  # This file is untracked. Run: npm run deploy-primershear to push changes.
4
4
  #
5
5
  # Example:
6
- # analyst@organization.com
6
+ # analyst@organization.com
@@ -19,7 +19,7 @@ import { FilesModal } from '~/components/sidebar/files/files-modal';
19
19
  import { NotesEditorModal } from '~/components/sidebar/notes/notes-editor-modal';
20
20
  import { UserAuditViewer } from '~/components/audit/user-audit-viewer';
21
21
  import { fetchUserApi } from '~/utils/api';
22
- import { type AnnotationData, type FileData } from '~/types';
22
+ import { type AnnotationData, type FileData, type ExportOptions } from '~/types';
23
23
  import { validateCaseNumber, renameCase, deleteCase, checkExistingCase, createNewCase, archiveCase, getCaseArchiveDetails } from '~/components/actions/case-manage';
24
24
  import { checkReadOnlyCaseExists, deleteReadOnlyCase } from '~/components/actions/case-review';
25
25
  import { canCreateCase, getCaseConfirmationSummary } from '~/utils/data';
@@ -77,15 +77,21 @@ export const Striae = ({ user }: StriaePage) => {
77
77
 
78
78
  // PDF generation states
79
79
  const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
80
+
81
+ // Toast notification states
80
82
  const [showToast, setShowToast] = useState(false);
81
83
  const [toastMessage, setToastMessage] = useState('');
82
84
  const [toastType, setToastType] = useState<ToastType>('success');
83
85
  const [toastDuration, setToastDuration] = useState(4000);
86
+
87
+ // Modal and sidebar states
84
88
  const [isAuditTrailOpen, setIsAuditTrailOpen] = useState(false);
85
89
  const [isRenameCaseModalOpen, setIsRenameCaseModalOpen] = useState(false);
86
90
  const [isOpenCaseModalOpen, setIsOpenCaseModalOpen] = useState(false);
87
91
  const [isListCasesModalOpen, setIsListCasesModalOpen] = useState(false);
88
92
  const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
93
+
94
+ // Case management action states
89
95
  const [isRenamingCase, setIsRenamingCase] = useState(false);
90
96
  const [isDeletingCase, setIsDeletingCase] = useState(false);
91
97
  const [isArchivingCase, setIsArchivingCase] = useState(false);
@@ -93,6 +99,8 @@ export const Striae = ({ user }: StriaePage) => {
93
99
  const [isOpeningCase, setIsOpeningCase] = useState(false);
94
100
  const [openCaseHelperText, setOpenCaseHelperText] = useState('');
95
101
  const [isArchiveCaseModalOpen, setIsArchiveCaseModalOpen] = useState(false);
102
+
103
+ // Export states
96
104
  const [isExportCaseModalOpen, setIsExportCaseModalOpen] = useState(false);
97
105
  const [isExportingCase, setIsExportingCase] = useState(false);
98
106
  const [isExportConfirmationsModalOpen, setIsExportConfirmationsModalOpen] = useState(false);
@@ -287,7 +295,8 @@ export const Striae = ({ user }: StriaePage) => {
287
295
  const handleExport = async (
288
296
  exportCaseNumber: string,
289
297
  designatedReviewerEmail?: string,
290
- onProgress?: (progress: number, label: string) => void
298
+ onProgress?: (progress: number, label: string) => void,
299
+ exportOptions?: ExportOptions
291
300
  ) => {
292
301
  if (!exportCaseNumber) {
293
302
  showNotification('Select a case before exporting.', 'error');
@@ -311,7 +320,7 @@ export const Striae = ({ user }: StriaePage) => {
311
320
  setShowToast(true);
312
321
  onProgress?.(roundedProgress, label);
313
322
  },
314
- { designatedReviewerEmail }
323
+ { ...exportOptions, designatedReviewerEmail }
315
324
  );
316
325
 
317
326
  showNotification(`Case ${exportCaseNumber} exported successfully.`, 'success');
@@ -364,6 +373,28 @@ export const Striae = ({ user }: StriaePage) => {
364
373
  }
365
374
  };
366
375
 
376
+ const handleOpenCaseExport = () => {
377
+ if (!currentCase) {
378
+ return;
379
+ }
380
+
381
+ if (isReadOnlyCase) {
382
+ if (isReviewOnlyCase) {
383
+ void handleOpenExportConfirmationsModal();
384
+ return;
385
+ }
386
+
387
+ if (archiveDetails.archived) {
388
+ void handleExport(currentCase, undefined, undefined, {
389
+ archivePackageMode: true,
390
+ });
391
+ return;
392
+ }
393
+ }
394
+
395
+ setIsExportCaseModalOpen(true);
396
+ };
397
+
367
398
  const handleRenameCaseSubmit = async (newCaseName: string) => {
368
399
  if (!currentCase) {
369
400
  showNotification('Select a case before renaming.', 'error');
@@ -809,13 +840,7 @@ export const Striae = ({ user }: StriaePage) => {
809
840
  void handleOpenCaseModal();
810
841
  }}
811
842
  onOpenListAllCases={() => setIsListCasesModalOpen(true)}
812
- onOpenCaseExport={() => {
813
- if (isReadOnlyCase) {
814
- void handleOpenExportConfirmationsModal();
815
- } else {
816
- setIsExportCaseModalOpen(true);
817
- }
818
- }}
843
+ onOpenCaseExport={handleOpenCaseExport}
819
844
  onOpenAuditTrail={() => setIsAuditTrailOpen(true)}
820
845
  onOpenRenameCase={() => setIsRenameCaseModalOpen(true)}
821
846
  onDeleteCase={() => {
@@ -838,7 +863,7 @@ export const Striae = ({ user }: StriaePage) => {
838
863
  onOpenCase={() => {
839
864
  void handleOpenCaseModal();
840
865
  }}
841
- onOpenCaseExport={() => void handleOpenExportConfirmationsModal()}
866
+ onOpenCaseExport={handleOpenCaseExport}
842
867
  imageId={imageId}
843
868
  currentCase={currentCase}
844
869
  imageLoaded={imageLoaded}
@@ -5,4 +5,5 @@ export interface ExportOptions {
5
5
  includeUserInfo?: boolean;
6
6
  protectForensicData?: boolean;
7
7
  designatedReviewerEmail?: string;
8
+ archivePackageMode?: boolean;
8
9
  }
@@ -6,8 +6,8 @@
6
6
 
7
7
  import { verifySignaturePayload } from './signature-utils';
8
8
 
9
- export const FORENSIC_MANIFEST_VERSION = '2.0';
10
- export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-PKCS1-v1_5-SHA-256';
9
+ export const FORENSIC_MANIFEST_VERSION = '3.0';
10
+ export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-PSS-SHA-256';
11
11
 
12
12
  export interface ForensicManifestData {
13
13
  dataHash: string;
@@ -5,7 +5,7 @@ import {
5
5
  } from './SHA256';
6
6
  import { verifySignaturePayload } from './signature-utils';
7
7
 
8
- export const AUDIT_EXPORT_SIGNATURE_VERSION = '1.0';
8
+ export const AUDIT_EXPORT_SIGNATURE_VERSION = '2.0';
9
9
 
10
10
  const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
11
11
 
@@ -6,7 +6,7 @@ import {
6
6
  } from './SHA256';
7
7
  import { verifySignaturePayload } from './signature-utils';
8
8
 
9
- export const CONFIRMATION_SIGNATURE_VERSION = '2.0';
9
+ export const CONFIRMATION_SIGNATURE_VERSION = '3.0';
10
10
 
11
11
  const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
12
12
 
@@ -29,6 +29,8 @@ export interface PublicSigningKeyDetails {
29
29
  publicKeyPem: string | null;
30
30
  }
31
31
 
32
+ const RSA_PSS_SALT_LENGTH = 32;
33
+
32
34
  type ManifestSigningConfig = {
33
35
  manifest_signing_public_keys?: Record<string, string>;
34
36
  manifest_signing_public_key?: string;
@@ -197,7 +199,7 @@ export async function verifySignaturePayload(
197
199
  'spki',
198
200
  publicKeyPemToArrayBuffer(publicKeyPem, invalidPublicKeyError),
199
201
  {
200
- name: 'RSASSA-PKCS1-v1_5',
202
+ name: 'RSA-PSS',
201
203
  hash: 'SHA-256'
202
204
  },
203
205
  false,
@@ -209,7 +211,10 @@ export async function verifySignaturePayload(
209
211
  signatureBuffer.set(signatureBytes);
210
212
 
211
213
  const verified = await crypto.subtle.verify(
212
- { name: 'RSASSA-PKCS1-v1_5' },
214
+ {
215
+ name: 'RSA-PSS',
216
+ saltLength: RSA_PSS_SALT_LENGTH
217
+ },
213
218
  key,
214
219
  signatureBuffer,
215
220
  new TextEncoder().encode(payload)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "5.5.2",
3
+ "version": "6.0.1",
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",
@@ -50,9 +50,7 @@
50
50
  "workers/pdf-worker/src/assets/generated-assets.example.ts",
51
51
  "!workers/pdf-worker/src/formats/**/*",
52
52
  "workers/pdf-worker/src/formats/format-striae.ts",
53
- ".env.example",
54
- "primershear.emails.example",
55
- "members.emails.example",
53
+ ".env.example",
56
54
  "firebase.json",
57
55
  "tsconfig.json",
58
56
  "vite.config.ts",
@@ -200,6 +200,36 @@ 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
+
203
233
  if [ "$validate_only" = "true" ]; then
204
234
  echo -e "\n${BLUE}🧪 Validate-only mode enabled${NC}"
205
235
  run_validation_checkpoint
@@ -217,6 +247,9 @@ load_admin_service_credentials
217
247
  # Always prompt for secrets to ensure configuration
218
248
  prompt_for_secrets
219
249
 
250
+ # Keep optional email list env vars aligned with app/config source files.
251
+ sync_email_list_env_vars_from_config
252
+
220
253
  # Validate after secrets have been configured
221
254
  validate_required_vars
222
255
 
@@ -3,7 +3,7 @@
3
3
  # ============================================
4
4
  # MEMBERS EMAIL LIST DEPLOYMENT SCRIPT
5
5
  # ============================================
6
- # Reads members.emails, updates REGISTRATION_EMAILS in .env,
6
+ # Reads app/config/members.emails, updates REGISTRATION_EMAILS in .env,
7
7
  # then deploys that secret directly to Cloudflare Pages (production).
8
8
 
9
9
  set -e
@@ -26,12 +26,12 @@ trap 'echo -e "\n${RED}❌ deploy-members-emails.sh failed near line ${LINENO}${
26
26
 
27
27
  # ── Read emails file ──────────────────────────────────────────────────────────
28
28
 
29
- EMAILS_FILE="$PROJECT_ROOT/members.emails"
29
+ EMAILS_FILE="$PROJECT_ROOT/app/config/members.emails"
30
30
 
31
31
  if [ ! -f "$EMAILS_FILE" ]; then
32
32
  echo -e "${RED}❌ members.emails not found at: $EMAILS_FILE${NC}"
33
33
  echo -e "${YELLOW} Create it with one email address or @domain.com wildcard per line.${NC}"
34
- echo -e "${YELLOW} See members.emails.example for the format.${NC}"
34
+ echo -e "${YELLOW} See app/config-example/members.emails for the format.${NC}"
35
35
  exit 1
36
36
  fi
37
37
 
@@ -45,7 +45,7 @@ if [ -z "$REGISTRATION_EMAILS" ]; then
45
45
  fi
46
46
 
47
47
  ENTRY_COUNT=$(echo "$REGISTRATION_EMAILS" | tr ',' '\n' | grep -c '[^[:space:]]' || true)
48
- echo -e "${GREEN}✅ Loaded $ENTRY_COUNT entry(ies) from members.emails${NC}"
48
+ echo -e "${GREEN}✅ Loaded $ENTRY_COUNT entry(ies) from app/config/members.emails${NC}"
49
49
 
50
50
  # ── Update .env ───────────────────────────────────────────────────────────────
51
51
 
@@ -3,7 +3,7 @@
3
3
  # ============================================
4
4
  # PRIMERSHEAR EMAIL LIST DEPLOYMENT SCRIPT
5
5
  # ============================================
6
- # Reads primershear.emails, updates PRIMERSHEAR_EMAILS in .env,
6
+ # Reads app/config/primershear.emails, updates PRIMERSHEAR_EMAILS in .env,
7
7
  # then deploys that secret directly to Cloudflare Pages (production).
8
8
 
9
9
  set -e
@@ -26,7 +26,7 @@ trap 'echo -e "\n${RED}❌ deploy-primershear-emails.sh failed near line ${LINEN
26
26
 
27
27
  # ── Read emails file ──────────────────────────────────────────────────────────
28
28
 
29
- EMAILS_FILE="$PROJECT_ROOT/primershear.emails"
29
+ EMAILS_FILE="$PROJECT_ROOT/app/config/primershear.emails"
30
30
 
31
31
  if [ ! -f "$EMAILS_FILE" ]; then
32
32
  echo -e "${RED}❌ primershear.emails not found at: $EMAILS_FILE${NC}"
@@ -44,7 +44,7 @@ if [ -z "$PRIMERSHEAR_EMAILS" ]; then
44
44
  fi
45
45
 
46
46
  EMAIL_COUNT=$(echo "$PRIMERSHEAR_EMAILS" | tr ',' '\n' | grep -c '[^[:space:]]' || true)
47
- echo -e "${GREEN}✅ Loaded $EMAIL_COUNT email address(es) from primershear.emails${NC}"
47
+ echo -e "${GREEN}✅ Loaded $EMAIL_COUNT email address(es) from app/config/primershear.emails${NC}"
48
48
 
49
49
  # ── Update .env ───────────────────────────────────────────────────────────────
50
50
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audit-worker",
3
- "version": "5.5.2",
3
+ "version": "6.0.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -7,7 +7,7 @@
7
7
  "name": "AUDIT_WORKER_NAME",
8
8
  "account_id": "ACCOUNT_ID",
9
9
  "main": "src/audit-worker.ts",
10
- "compatibility_date": "2026-04-10",
10
+ "compatibility_date": "2026-04-12",
11
11
  "compatibility_flags": [
12
12
  "nodejs_compat"
13
13
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "data-worker",
3
- "version": "5.5.2",
3
+ "version": "6.0.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -5,6 +5,8 @@ export interface WorkerSignatureEnvelope {
5
5
  value: string;
6
6
  }
7
7
 
8
+ const RSA_PSS_SALT_LENGTH = 32;
9
+
8
10
  function base64UrlEncode(value: Uint8Array): string {
9
11
  let binary = '';
10
12
  for (const byte of value) {
@@ -57,7 +59,7 @@ export async function signPayload(
57
59
  'pkcs8',
58
60
  parsePkcs8PrivateKey(privateKey),
59
61
  {
60
- name: 'RSASSA-PKCS1-v1_5',
62
+ name: 'RSA-PSS',
61
63
  hash: 'SHA-256'
62
64
  },
63
65
  false,
@@ -65,7 +67,10 @@ export async function signPayload(
65
67
  );
66
68
 
67
69
  const signature = await crypto.subtle.sign(
68
- { name: 'RSASSA-PKCS1-v1_5' },
70
+ {
71
+ name: 'RSA-PSS',
72
+ saltLength: RSA_PSS_SALT_LENGTH
73
+ },
69
74
  signingKey,
70
75
  new TextEncoder().encode(payload)
71
76
  );
@@ -52,10 +52,10 @@ export interface AuditExportSigningPayload {
52
52
  hash: string;
53
53
  }
54
54
 
55
- export const FORENSIC_MANIFEST_VERSION = '2.0';
56
- export const CONFIRMATION_SIGNATURE_VERSION = '2.0';
57
- export const AUDIT_EXPORT_SIGNATURE_VERSION = '1.0';
58
- export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-PKCS1-v1_5-SHA-256';
55
+ export const FORENSIC_MANIFEST_VERSION = '3.0';
56
+ export const CONFIRMATION_SIGNATURE_VERSION = '3.0';
57
+ export const AUDIT_EXPORT_SIGNATURE_VERSION = '2.0';
58
+ export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-PSS-SHA-256';
59
59
 
60
60
  const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
61
61
 
@@ -5,7 +5,7 @@
5
5
  "name": "DATA_WORKER_NAME",
6
6
  "account_id": "ACCOUNT_ID",
7
7
  "main": "src/data-worker.ts",
8
- "compatibility_date": "2026-04-10",
8
+ "compatibility_date": "2026-04-12",
9
9
  "compatibility_flags": [
10
10
  "nodejs_compat"
11
11
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-worker",
3
- "version": "5.5.2",
3
+ "version": "6.0.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -2,7 +2,7 @@
2
2
  "name": "IMAGES_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/image-worker.ts",
5
- "compatibility_date": "2026-04-10",
5
+ "compatibility_date": "2026-04-12",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-worker",
3
- "version": "5.5.2",
3
+ "version": "6.0.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "generate:assets": "node scripts/generate-assets.js",
@@ -2,7 +2,7 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-04-10",
5
+ "compatibility_date": "2026-04-12",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "user-worker",
3
- "version": "5.5.2",
3
+ "version": "6.0.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -2,7 +2,7 @@
2
2
  "name": "USER_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/user-worker.ts",
5
- "compatibility_date": "2026-04-10",
5
+ "compatibility_date": "2026-04-12",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  #:schema node_modules/wrangler/config-schema.json
2
2
  name = "PAGES_PROJECT_NAME"
3
- compatibility_date = "2026-04-10"
3
+ compatibility_date = "2026-04-12"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6