@striae-org/striae 3.1.0 → 3.2.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 (30) hide show
  1. package/.env.example +2 -0
  2. package/app/components/actions/generate-pdf.ts +4 -0
  3. package/app/components/icon/icons.svg +4 -71
  4. package/app/components/icon/manifest.json +3 -75
  5. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +171 -0
  6. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +223 -0
  7. package/app/components/sidebar/case-export/case-export.module.css +0 -158
  8. package/app/components/sidebar/case-export/case-export.tsx +8 -185
  9. package/app/config-example/inactivity.ts +1 -1
  10. package/app/services/audit-export.service.ts +1 -1
  11. package/app/services/firebase.ts +4 -0
  12. package/app/utils/auth.ts +5 -1
  13. package/package.json +6 -3
  14. package/public/_routes.json +1 -0
  15. package/public/icon-256.png +0 -0
  16. package/scripts/deploy-config.sh +226 -2
  17. package/scripts/deploy-pages.sh +2 -2
  18. package/scripts/deploy-worker-secrets.sh +6 -2
  19. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  20. package/workers/data-worker/wrangler.jsonc.example +1 -1
  21. package/workers/image-worker/wrangler.jsonc.example +1 -1
  22. package/workers/keys-worker/src/keys.example.ts +1 -0
  23. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  24. package/workers/pdf-worker/package.json +1 -0
  25. package/workers/pdf-worker/src/format-striae.ts +2 -1
  26. package/workers/pdf-worker/src/generated-assets.ts +117 -0
  27. package/workers/pdf-worker/src/pdf-worker.example.ts +12 -1
  28. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  29. package/workers/user-worker/wrangler.jsonc.example +1 -1
  30. package/wrangler.toml.example +1 -1
@@ -317,164 +317,6 @@
317
317
  border-color: color-mix(in lab, var(--primary) 55%, transparent);
318
318
  }
319
319
 
320
- .publicKeyOverlay {
321
- position: fixed;
322
- inset: 0;
323
- background-color: color-mix(in lab, var(--background) 60%, transparent);
324
- display: flex;
325
- justify-content: center;
326
- align-items: center;
327
- z-index: var(--zIndex5);
328
- padding: var(--spaceL);
329
- }
330
-
331
- .publicKeyModal {
332
- width: 100%;
333
- max-width: 640px;
334
- max-height: 90vw;
335
- background: var(--backgroundLight);
336
- border-radius: var(--spaceXS);
337
- display: flex;
338
- flex-direction: column;
339
- box-shadow: 0 var(--spaceXS) var(--spaceL)
340
- color-mix(in lab, var(--black) 18%, transparent);
341
- overflow: hidden;
342
- cursor: default;
343
- }
344
-
345
- .publicKeyHeader {
346
- display: flex;
347
- justify-content: space-between;
348
- align-items: center;
349
- padding: var(--spaceL);
350
- border-bottom: 1px solid color-mix(in lab, var(--text) 10%, transparent);
351
- }
352
-
353
- .publicKeyTitle {
354
- margin: 0;
355
- font-size: var(--fontSizeBodyL);
356
- font-weight: 600;
357
- color: var(--textTitle);
358
- }
359
-
360
- .publicKeyContent {
361
- padding: var(--spaceL);
362
- flex: 1 1 auto;
363
- min-height: 0;
364
- display: flex;
365
- flex-direction: column;
366
- gap: var(--spaceM);
367
- overflow-y: auto;
368
- overflow-x: hidden;
369
- }
370
-
371
- .publicKeyDescription {
372
- margin: 0;
373
- font-size: var(--fontSizeBodyS);
374
- color: var(--textBody);
375
- }
376
-
377
- .publicKeyMeta {
378
- margin: 0;
379
- font-size: var(--fontSizeBodyS);
380
- color: var(--textTitle);
381
- }
382
-
383
- .publicKeyMeta span {
384
- font-weight: var(--fontWeightMedium);
385
- }
386
-
387
- .publicKeyLabel {
388
- font-size: var(--fontSizeBodyXS);
389
- font-weight: var(--fontWeightMedium);
390
- color: var(--textTitle);
391
- }
392
-
393
- .publicKeyField {
394
- width: 100%;
395
- max-width: 100%;
396
- box-sizing: border-box;
397
- min-height: 180px;
398
- padding: var(--spaceM);
399
- border: 1px solid color-mix(in lab, var(--text) 10%, transparent);
400
- border-radius: var(--spaceXS);
401
- background: color-mix(in lab, var(--background) 96%, transparent);
402
- color: var(--textBody);
403
- font-size: var(--fontSizeBodyXS);
404
- line-height: 1.4;
405
- font-family: Consolas, "Courier New", monospace;
406
- resize: vertical;
407
- }
408
-
409
- .publicKeyHowToTitle {
410
- margin: 0;
411
- font-size: var(--fontSizeBodyS);
412
- font-weight: var(--fontWeightMedium);
413
- color: var(--textTitle);
414
- }
415
-
416
- .publicKeyHowToList {
417
- margin: 0;
418
- padding-left: var(--spaceL);
419
- display: flex;
420
- flex-direction: column;
421
- gap: var(--spaceXS);
422
- color: var(--textBody);
423
- font-size: var(--fontSizeBodyS);
424
- }
425
-
426
- .publicKeyActions {
427
- display: flex;
428
- justify-content: flex-end;
429
- gap: var(--spaceS);
430
- }
431
-
432
- .publicKeyStatus {
433
- margin: 0;
434
- font-size: var(--fontSizeBodyXS);
435
- color: var(--textBody);
436
- }
437
-
438
- .publicKeyCopyButton {
439
- background: transparent;
440
- color: var(--primary);
441
- border: 1px solid color-mix(in lab, var(--primary) 35%, transparent);
442
- border-radius: var(--spaceXS);
443
- padding: var(--spaceS) var(--spaceL);
444
- font-size: var(--fontSizeBodyS);
445
- font-weight: var(--fontWeightMedium);
446
- cursor: pointer;
447
- transition: all var(--durationS) var(--bezierFastoutSlowin);
448
- }
449
-
450
- .publicKeyCopyButton:hover:not(:disabled) {
451
- background: color-mix(in lab, var(--primary) 10%, transparent);
452
- border-color: color-mix(in lab, var(--primary) 55%, transparent);
453
- }
454
-
455
- .publicKeyCopyButton:disabled {
456
- background: color-mix(in lab, var(--background) 95%, transparent);
457
- color: var(--textLight);
458
- border-color: color-mix(in lab, var(--text) 10%, transparent);
459
- cursor: not-allowed;
460
- }
461
-
462
- .publicKeyCloseButton {
463
- background: var(--primary);
464
- color: white;
465
- border: none;
466
- border-radius: var(--spaceXS);
467
- padding: var(--spaceS) var(--spaceL);
468
- font-size: var(--fontSizeBodyS);
469
- font-weight: var(--fontWeightMedium);
470
- cursor: pointer;
471
- transition: all var(--durationS) var(--bezierFastoutSlowin);
472
- }
473
-
474
- .publicKeyCloseButton:hover {
475
- background: color-mix(in lab, var(--primary) 85%, var(--black));
476
- }
477
-
478
320
  .divider {
479
321
  margin: var(--spaceL) 0;
480
322
  text-align: center;
@@ -2,6 +2,7 @@ import { useState, useEffect, useContext } from 'react';
2
2
  import styles from './case-export.module.css';
3
3
  import config from '~/config/config.json';
4
4
  import { AuthContext } from '~/contexts/auth.context';
5
+ import { PublicSigningKeyModal } from '~/components/public-signing-key-modal/public-signing-key-modal';
5
6
  import { getVerificationPublicKey } from '~/utils/signature-utils';
6
7
  import { getCaseConfirmations, exportConfirmationData } from '../../actions/confirm-export';
7
8
 
@@ -86,8 +87,6 @@ export const CaseExport = ({
86
87
  const [includeImages, setIncludeImages] = useState(false);
87
88
  const [hasConfirmationData, setHasConfirmationData] = useState(false);
88
89
  const [isPublicKeyModalOpen, setIsPublicKeyModalOpen] = useState(false);
89
- const [isCopyingPublicKey, setIsCopyingPublicKey] = useState(false);
90
- const [publicKeyCopyMessage, setPublicKeyCopyMessage] = useState('');
91
90
  const { keyId: publicSigningKeyId, publicKeyPem } = getPublicSigningKeyDetails();
92
91
 
93
92
  // Update caseNumber when currentCaseNumber prop changes
@@ -153,22 +152,10 @@ export const CaseExport = ({
153
152
  }
154
153
  }, [isOpen]);
155
154
 
156
- useEffect(() => {
157
- if (!isPublicKeyModalOpen) {
158
- setIsCopyingPublicKey(false);
159
- setPublicKeyCopyMessage('');
160
- }
161
- }, [isPublicKeyModalOpen]);
162
-
163
155
  // Handle Escape key to close modal
164
156
  useEffect(() => {
165
157
  const handleEscapeKey = (event: KeyboardEvent) => {
166
- if (event.key === 'Escape' && isOpen) {
167
- if (isPublicKeyModalOpen) {
168
- setIsPublicKeyModalOpen(false);
169
- return;
170
- }
171
-
158
+ if (event.key === 'Escape' && isOpen && !isPublicKeyModalOpen) {
172
159
  onClose();
173
160
  }
174
161
  };
@@ -261,81 +248,6 @@ export const CaseExport = ({
261
248
  }
262
249
  };
263
250
 
264
- const handlePublicKeyOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
265
- if (e.target === e.currentTarget) {
266
- setIsPublicKeyModalOpen(false);
267
- }
268
- };
269
-
270
- const handlePublicKeyOverlayKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
271
- if (e.target !== e.currentTarget) {
272
- return;
273
- }
274
-
275
- if (e.key === 'Enter' || e.key === ' ') {
276
- e.preventDefault();
277
- setIsPublicKeyModalOpen(false);
278
- }
279
- };
280
-
281
- const copyTextWithExecCommand = (text: string): boolean => {
282
- const tempTextarea = document.createElement('textarea');
283
- tempTextarea.value = text;
284
- tempTextarea.setAttribute('readonly', '');
285
- tempTextarea.style.position = 'fixed';
286
- tempTextarea.style.opacity = '0';
287
- tempTextarea.style.pointerEvents = 'none';
288
-
289
- document.body.appendChild(tempTextarea);
290
- tempTextarea.select();
291
-
292
- let copied = false;
293
- try {
294
- copied = document.execCommand('copy');
295
- } finally {
296
- document.body.removeChild(tempTextarea);
297
- }
298
-
299
- return copied;
300
- };
301
-
302
- const handleCopyPublicKey = async () => {
303
- if (!publicKeyPem) {
304
- setPublicKeyCopyMessage('No public signing key is configured for this environment.');
305
- return;
306
- }
307
-
308
- setIsCopyingPublicKey(true);
309
- setPublicKeyCopyMessage('');
310
-
311
- try {
312
- if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
313
- await navigator.clipboard.writeText(publicKeyPem);
314
- setPublicKeyCopyMessage('Public key copied to clipboard.');
315
- } else {
316
- const copied = copyTextWithExecCommand(publicKeyPem);
317
- setPublicKeyCopyMessage(
318
- copied
319
- ? 'Public key copied to clipboard.'
320
- : 'Copy failed. Select and copy the key manually.'
321
- );
322
- }
323
- } catch (copyError) {
324
- const copied = copyTextWithExecCommand(publicKeyPem);
325
- setPublicKeyCopyMessage(
326
- copied
327
- ? 'Public key copied to clipboard.'
328
- : 'Copy failed. Select and copy the key manually.'
329
- );
330
-
331
- if (!copied) {
332
- console.error('Failed to copy public signing key:', copyError);
333
- }
334
- } finally {
335
- setIsCopyingPublicKey(false);
336
- }
337
- };
338
-
339
251
  return (
340
252
  <div
341
253
  className={styles.overlay}
@@ -497,101 +409,12 @@ export const CaseExport = ({
497
409
  </div>
498
410
  </div>
499
411
 
500
- {isPublicKeyModalOpen && (
501
- <div
502
- className={styles.publicKeyOverlay}
503
- onMouseDown={handlePublicKeyOverlayMouseDown}
504
- onKeyDown={handlePublicKeyOverlayKeyDown}
505
- role="button"
506
- tabIndex={0}
507
- aria-label="Close public signing key dialog"
508
- >
509
- <div
510
- className={styles.publicKeyModal}
511
- role="dialog"
512
- aria-modal="true"
513
- aria-labelledby="publicSigningKeyTitle"
514
- >
515
- <div className={styles.publicKeyHeader}>
516
- <h3 id="publicSigningKeyTitle" className={styles.publicKeyTitle}>
517
- Striae Public Signing Key
518
- </h3>
519
- <button
520
- type="button"
521
- className={styles.closeButton}
522
- onClick={() => setIsPublicKeyModalOpen(false)}
523
- aria-label="Close public signing key dialog"
524
- >
525
- ×
526
- </button>
527
- </div>
528
-
529
- <div className={styles.publicKeyContent}>
530
- <p className={styles.publicKeyDescription}>
531
- This key verifies digital signatures attached to Striae exports. It is safe to share for
532
- independent verification.
533
- </p>
534
-
535
- {publicSigningKeyId && (
536
- <p className={styles.publicKeyMeta}>
537
- Key ID: <span>{publicSigningKeyId}</span>
538
- </p>
539
- )}
540
-
541
- <label htmlFor="publicSigningKey" className={styles.publicKeyLabel}>
542
- Public signing key (PEM)
543
- </label>
544
- <textarea
545
- id="publicSigningKey"
546
- className={styles.publicKeyField}
547
- value={publicKeyPem || 'No public signing key is configured for this environment.'}
548
- readOnly
549
- rows={10}
550
- />
551
-
552
- <p className={styles.publicKeyHowToTitle}>How to verify Striae exports</p>
553
- <ol className={styles.publicKeyHowToList}>
554
- <li>
555
- Locate signature metadata in the export (for case ZIP exports, see FORENSIC_MANIFEST.json;
556
- for confirmation exports, see metadata.signature).
557
- </li>
558
- <li>
559
- Use this public key with your signature verification workflow (for example OpenSSL or an
560
- internal verifier) to validate the signed payload.
561
- </li>
562
- <li>
563
- Trust the export only when signature verification succeeds and the key ID matches the export
564
- metadata.
565
- </li>
566
- </ol>
567
-
568
- {publicKeyCopyMessage && (
569
- <p className={styles.publicKeyStatus} role="status" aria-live="polite">
570
- {publicKeyCopyMessage}
571
- </p>
572
- )}
573
-
574
- <div className={styles.publicKeyActions}>
575
- <button
576
- type="button"
577
- className={styles.publicKeyCopyButton}
578
- onClick={handleCopyPublicKey}
579
- disabled={isCopyingPublicKey || !publicKeyPem}
580
- >
581
- {isCopyingPublicKey ? 'Copying...' : 'Copy Key'}
582
- </button>
583
- <button
584
- type="button"
585
- className={styles.publicKeyCloseButton}
586
- onClick={() => setIsPublicKeyModalOpen(false)}
587
- >
588
- Close
589
- </button>
590
- </div>
591
- </div>
592
- </div>
593
- </div>
594
- )}
412
+ <PublicSigningKeyModal
413
+ isOpen={isPublicKeyModalOpen}
414
+ onClose={() => setIsPublicKeyModalOpen(false)}
415
+ publicSigningKeyId={publicSigningKeyId}
416
+ publicKeyPem={publicKeyPem}
417
+ />
595
418
  </div>
596
419
  );
597
420
  };
@@ -1,5 +1,5 @@
1
1
  export const INACTIVITY_CONFIG = {
2
- TIMEOUT_MINUTES: 60,
2
+ TIMEOUT_MINUTES: 30,
3
3
  WARNING_MINUTES: 5,
4
4
  TRACKED_ACTIVITIES: [
5
5
  'mousedown',
@@ -557,7 +557,7 @@ Audit Signature: ${JSON.stringify(signaturePayload.signature)}
557
557
  Verification Instructions:
558
558
  1. Copy the entire report content above the "INTEGRITY VERIFICATION" section
559
559
  2. Calculate SHA256 hash of that content (excluding this verification section)
560
- 3. Validate audit signature metadata and signature with the Striae Hash Utility
560
+ 3. Validate audit signature metadata and signature with your signature verification workflow (for example OpenSSL or an internal verifier)
561
561
  4. Confirm both hash and signature validation pass before relying on this report
562
562
 
563
563
  This report requires both hash and signature validation for tamper detection.
@@ -1,6 +1,8 @@
1
1
  import { initializeApp } from 'firebase/app';
2
2
  import {
3
3
  getAuth,
4
+ setPersistence,
5
+ browserSessionPersistence,
4
6
  //connectAuthEmulator,
5
7
  } from 'firebase/auth';
6
8
  import firebaseConfig from '~/config/firebase';
@@ -9,6 +11,8 @@ import { getAppVersion } from '~/utils/version';
9
11
  export const app = initializeApp(firebaseConfig, "Striae");
10
12
  export const auth = getAuth(app);
11
13
 
14
+ setPersistence(auth, browserSessionPersistence);
15
+
12
16
  console.log(`Welcome to ${app.name} v${getAppVersion()}`);
13
17
 
14
18
  //Connect to the Firebase Auth emulator if running locally
package/app/utils/auth.ts CHANGED
@@ -3,7 +3,7 @@ import paths from '~/config/config.json';
3
3
  const KEYS_URL = paths.keys_url;
4
4
  const KEYS_AUTH = paths.keys_auth;
5
5
 
6
- type KeyType = 'USER_DB_AUTH' | 'R2_KEY_SECRET' | 'IMAGES_API_TOKEN' | 'ACCOUNT_HASH';
6
+ type KeyType = 'USER_DB_AUTH' | 'R2_KEY_SECRET' | 'IMAGES_API_TOKEN' | 'ACCOUNT_HASH' | 'PDF_WORKER_AUTH';
7
7
 
8
8
  async function getApiKey(keyType: KeyType): Promise<string> {
9
9
  const keyResponse = await fetch(`${KEYS_URL}/${keyType}`, {
@@ -31,4 +31,8 @@ export async function getImageApiKey(): Promise<string> {
31
31
 
32
32
  export async function getAccountHash(): Promise<string> {
33
33
  return getApiKey('ACCOUNT_HASH');
34
+ }
35
+
36
+ export async function getPdfApiKey(): Promise<string> {
37
+ return getApiKey('PDF_WORKER_AUTH');
34
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "private": false,
5
5
  "description": "Cloud-native forensic annotation application for firearms identification (Remix + Cloudflare Workers).",
6
6
  "license": "Apache-2.0",
@@ -77,9 +77,9 @@
77
77
  "publish:github": "npm publish --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
78
78
  "publish:github:dry-run": "npm publish --dry-run --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
79
79
  "publish:all": "npm run publish:npm && npm run publish:github",
80
- "publish:all:dry-run": "npm run publish:npm:dry-run && npm run publish:github:dry-run",
80
+ "publish:all:dry-run": "npm run publish:npm:dry-run && npm run publish:github:dry-run",
81
81
  "lint": "node ./scripts/run-eslint.cjs",
82
- "start": "node ./scripts/dev.cjs && wrangler pages dev ./build/client",
82
+ "start": "node ./scripts/dev.cjs && wrangler pages dev",
83
83
  "typecheck": "tsc",
84
84
  "typegen": "wrangler types",
85
85
  "preview": "npm run build && wrangler pages dev",
@@ -130,6 +130,9 @@
130
130
  "vite-tsconfig-paths": "^6.1.1",
131
131
  "wrangler": "^3.114.17"
132
132
  },
133
+ "overrides": {
134
+ "tar": "7.5.11"
135
+ },
133
136
  "engines": {
134
137
  "node": ">=20.0.0"
135
138
  },
@@ -8,6 +8,7 @@
8
8
  "/*.css",
9
9
  "/*.js",
10
10
  "/*.pdf",
11
+ "/*.png",
11
12
  "/*.zip"
12
13
  ]
13
14
  }
Binary file