@striae-org/striae 3.2.2 → 3.3.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 (37) hide show
  1. package/app/components/actions/case-export/download-handlers.ts +51 -3
  2. package/app/components/actions/case-import/confirmation-import.ts +41 -17
  3. package/app/components/actions/case-import/confirmation-package.ts +86 -0
  4. package/app/components/actions/case-import/index.ts +1 -0
  5. package/app/components/actions/case-import/orchestrator.ts +12 -2
  6. package/app/components/actions/case-import/validation.ts +5 -98
  7. package/app/components/actions/case-import/zip-processing.ts +44 -2
  8. package/app/components/actions/confirm-export.ts +44 -13
  9. package/app/components/form/form-button.tsx +1 -1
  10. package/app/components/form/form.module.css +9 -0
  11. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
  12. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
  13. package/app/components/sidebar/case-export/case-export.tsx +2 -54
  14. package/app/components/sidebar/case-import/case-import.tsx +18 -6
  15. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +6 -4
  16. package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
  17. package/app/components/sidebar/cases/case-sidebar.tsx +101 -46
  18. package/app/components/sidebar/cases/cases.module.css +101 -18
  19. package/app/components/sidebar/notes/notes.module.css +33 -13
  20. package/app/components/user/manage-profile.tsx +1 -1
  21. package/app/components/user/mfa-phone-update.tsx +15 -12
  22. package/app/root.tsx +2 -2
  23. package/app/routes/auth/login.tsx +129 -6
  24. package/app/utils/SHA256.ts +5 -1
  25. package/app/utils/confirmation-signature.ts +5 -1
  26. package/app/utils/export-verification.ts +353 -0
  27. package/app/utils/signature-utils.ts +74 -4
  28. package/package.json +7 -4
  29. package/public/favicon.ico +0 -0
  30. package/public/icon-256.png +0 -0
  31. package/public/icon-512.png +0 -0
  32. package/public/manifest.json +39 -0
  33. package/public/shortcut.png +0 -0
  34. package/public/social-image.png +0 -0
  35. package/react-router.config.ts +5 -0
  36. package/workers/pdf-worker/scripts/generate-assets.js +94 -0
  37. package/public/favicon.svg +0 -9
@@ -20,6 +20,15 @@ export interface SignatureVerificationMessages {
20
20
  verificationFailedError?: string;
21
21
  }
22
22
 
23
+ export interface SignatureVerificationOptions {
24
+ verificationPublicKeyPem?: string;
25
+ }
26
+
27
+ export interface PublicSigningKeyDetails {
28
+ keyId: string | null;
29
+ publicKeyPem: string | null;
30
+ }
31
+
23
32
  type ManifestSigningConfig = {
24
33
  manifest_signing_public_keys?: Record<string, string>;
25
34
  manifest_signing_public_key?: string;
@@ -30,6 +39,63 @@ function normalizePemPublicKey(pem: string): string {
30
39
  return pem.replace(/\\n/g, '\n').trim();
31
40
  }
32
41
 
42
+ function normalizePemOrNull(pem: unknown): string | null {
43
+ if (typeof pem !== 'string' || pem.trim().length === 0) {
44
+ return null;
45
+ }
46
+
47
+ return normalizePemPublicKey(pem);
48
+ }
49
+
50
+ function sanitizeKeyIdForFileName(keyId: string): string {
51
+ return keyId.trim().replace(/[^a-z0-9_-]+/gi, '-');
52
+ }
53
+
54
+ export function createPublicSigningKeyFileName(keyId?: string | null): string {
55
+ if (typeof keyId === 'string' && keyId.trim().length > 0) {
56
+ return `striae-public-signing-key-${sanitizeKeyIdForFileName(keyId)}.pem`;
57
+ }
58
+
59
+ return 'striae-public-signing-key.pem';
60
+ }
61
+
62
+ export function getCurrentPublicSigningKeyDetails(): PublicSigningKeyDetails {
63
+ const config = paths as unknown as ManifestSigningConfig;
64
+ const configuredKeyId =
65
+ typeof config.manifest_signing_key_id === 'string' && config.manifest_signing_key_id.trim().length > 0
66
+ ? config.manifest_signing_key_id
67
+ : null;
68
+
69
+ if (configuredKeyId) {
70
+ const configuredKey = getVerificationPublicKey(configuredKeyId);
71
+ if (configuredKey) {
72
+ return {
73
+ keyId: configuredKeyId,
74
+ publicKeyPem: configuredKey
75
+ };
76
+ }
77
+ }
78
+
79
+ const keyMap = config.manifest_signing_public_keys;
80
+ if (keyMap && typeof keyMap === 'object') {
81
+ const firstConfiguredEntry = Object.entries(keyMap).find(
82
+ ([, value]) => typeof value === 'string' && value.trim().length > 0
83
+ );
84
+
85
+ if (firstConfiguredEntry) {
86
+ return {
87
+ keyId: firstConfiguredEntry[0],
88
+ publicKeyPem: normalizePemPublicKey(firstConfiguredEntry[1])
89
+ };
90
+ }
91
+ }
92
+
93
+ return {
94
+ keyId: null,
95
+ publicKeyPem: normalizePemOrNull(config.manifest_signing_public_key)
96
+ };
97
+ }
98
+
33
99
  function publicKeyPemToArrayBuffer(publicKeyPem: string, invalidPublicKeyError: string): ArrayBuffer {
34
100
  const normalized = normalizePemPublicKey(publicKeyPem);
35
101
  const pemBody = normalized
@@ -71,7 +137,7 @@ export function getVerificationPublicKey(keyId: string): string | null {
71
137
  if (keyMap && typeof keyMap === 'object') {
72
138
  const mappedKey = keyMap[keyId];
73
139
  if (typeof mappedKey === 'string' && mappedKey.trim().length > 0) {
74
- return mappedKey;
140
+ return normalizePemPublicKey(mappedKey);
75
141
  }
76
142
  }
77
143
 
@@ -81,7 +147,7 @@ export function getVerificationPublicKey(keyId: string): string | null {
81
147
  typeof config.manifest_signing_public_key === 'string' &&
82
148
  config.manifest_signing_public_key.trim().length > 0
83
149
  ) {
84
- return config.manifest_signing_public_key;
150
+ return normalizePemPublicKey(config.manifest_signing_public_key);
85
151
  }
86
152
 
87
153
  return null;
@@ -91,7 +157,8 @@ export async function verifySignaturePayload(
91
157
  payload: string,
92
158
  signature: SignatureEnvelope,
93
159
  expectedAlgorithm: string,
94
- messages: SignatureVerificationMessages = {}
160
+ messages: SignatureVerificationMessages = {},
161
+ options: SignatureVerificationOptions = {}
95
162
  ): Promise<SignatureVerificationResult> {
96
163
  if (signature.algorithm !== expectedAlgorithm) {
97
164
  return {
@@ -108,7 +175,10 @@ export async function verifySignaturePayload(
108
175
  };
109
176
  }
110
177
 
111
- const publicKeyPem = getVerificationPublicKey(signature.keyId);
178
+ const publicKeyPem =
179
+ typeof options.verificationPublicKeyPem === 'string' && options.verificationPublicKeyPem.trim().length > 0
180
+ ? options.verificationPublicKeyPem
181
+ : getVerificationPublicKey(signature.keyId);
112
182
  if (!publicKeyPem) {
113
183
  return {
114
184
  isValid: false,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "3.2.2",
3
+ "version": "3.3.0",
4
4
  "private": false,
5
- "description": "Striae is a cloud-native forensic annotation application for firearms identification, built with React Router and Cloudflare Workers.",
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",
7
7
  "homepage": "https://www.striae.org",
8
8
  "repository": {
@@ -24,6 +24,8 @@
24
24
  "annotation",
25
25
  "react",
26
26
  "cloudflare-workers",
27
+ "authenticated",
28
+ "confirmations",
27
29
  "chain-of-custody",
28
30
  "audit-trail"
29
31
  ],
@@ -44,6 +46,7 @@
44
46
  "app/entry.server.tsx",
45
47
  "app/root.tsx",
46
48
  "app/tailwind.css",
49
+ "react-router.config.ts",
47
50
  "functions/",
48
51
  "public/",
49
52
  "scripts/",
@@ -51,8 +54,8 @@
51
54
  "workers/*/src/*.example.ts",
52
55
  "workers/*/src/*.example.js",
53
56
  "workers/*/src/*.ts",
54
- "workers/*/src/assets/*.ts",
55
- "workers/*/src/formats/*.ts",
57
+ "workers/pdf-worker/scripts/*.js",
58
+ "workers/pdf-worker/src/assets/icon-256.png",
56
59
  "!workers/*/src/*worker.ts",
57
60
  "workers/pdf-worker/src/assets/generated-assets.ts",
58
61
  "workers/pdf-worker/src/formats/format-striae.ts",
Binary file
Binary file
Binary file
@@ -0,0 +1,39 @@
1
+ {
2
+ "id": "/",
3
+ "name": "Striae: A Firearms Examiner's Comparison Companion",
4
+ "short_name": "Striae",
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
+ "start_url": "/",
7
+ "scope": "/",
8
+ "display": "standalone",
9
+ "orientation": "any",
10
+ "lang": "en-US",
11
+ "dir": "ltr",
12
+ "theme_color": "#377087",
13
+ "background_color": "#f5f5f5",
14
+ "categories": [
15
+ "business",
16
+ "productivity",
17
+ "utilities"
18
+ ],
19
+ "icons": [
20
+ {
21
+ "src": "shortcut.png",
22
+ "sizes": "64x64",
23
+ "type": "image/png",
24
+ "purpose": "any"
25
+ },
26
+ {
27
+ "src": "icon-256.png",
28
+ "sizes": "256x256",
29
+ "type": "image/png",
30
+ "purpose": "any"
31
+ },
32
+ {
33
+ "src": "icon-512.png",
34
+ "sizes": "512x512",
35
+ "type": "image/png",
36
+ "purpose": "any"
37
+ }
38
+ ]
39
+ }
Binary file
Binary file
@@ -0,0 +1,5 @@
1
+ import type { Config } from "@react-router/dev/config";
2
+
3
+ export default {
4
+ ssr: true,
5
+ } satisfies Config;
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Scans src/assets/ and generates src/assets/generated-assets.ts with one named
4
+ * export per file. Run via: npm run generate:assets
5
+ *
6
+ * Naming rule: filename → remove extension → uppercase → non-alphanumeric → _
7
+ * icon-256.png → ICON_256
8
+ * logo.svg → LOGO
9
+ * brand-mark.webp → BRAND_MARK
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const ASSETS_DIR = path.join(__dirname, '../src/assets');
16
+ const OUTPUT_FILE = path.join(__dirname, '../src/assets/generated-assets.ts');
17
+
18
+ const MIME_TYPES = {
19
+ '.png': 'image/png',
20
+ '.jpg': 'image/jpeg',
21
+ '.jpeg': 'image/jpeg',
22
+ '.svg': 'image/svg+xml',
23
+ '.gif': 'image/gif',
24
+ '.webp': 'image/webp',
25
+ '.ico': 'image/x-icon',
26
+ };
27
+
28
+ function toConstName(filename) {
29
+ return filename
30
+ .replace(/\.[^.]+$/, '') // strip extension
31
+ .toUpperCase()
32
+ .replace(/[^A-Z0-9]+/g, '_') // non-alphanumeric → underscore
33
+ .replace(/^_+|_+$/g, ''); // trim leading/trailing underscores
34
+ }
35
+
36
+ function toDataUri(filepath) {
37
+ const ext = path.extname(filepath).toLowerCase();
38
+ const mime = MIME_TYPES[ext];
39
+ if (!mime) return null;
40
+
41
+ const data = fs.readFileSync(filepath);
42
+ const b64 = data.toString('base64');
43
+ const chunks = b64.match(/.{1,120}/g) || [];
44
+
45
+ const lines = [];
46
+ lines.push(` "data:${mime};base64," +`);
47
+ chunks.forEach((chunk, i) => {
48
+ lines.push(` "${chunk}"${i === chunks.length - 1 ? ';' : ' +'}`);
49
+ });
50
+ return lines.join('\n');
51
+ }
52
+
53
+ if (!fs.existsSync(ASSETS_DIR)) {
54
+ console.error(`assets directory not found: ${ASSETS_DIR}`);
55
+ process.exit(1);
56
+ }
57
+
58
+ const files = fs
59
+ .readdirSync(ASSETS_DIR)
60
+ .filter((f) => Object.keys(MIME_TYPES).includes(path.extname(f).toLowerCase()))
61
+ .sort();
62
+
63
+ if (files.length === 0) {
64
+ console.warn('No supported asset files found in src/assets/.');
65
+ process.exit(0);
66
+ }
67
+
68
+ const sections = [
69
+ '// Auto-generated by scripts/generate-assets.js',
70
+ '// Do not edit manually — run `npm run generate:assets` to regenerate.',
71
+ '',
72
+ ];
73
+
74
+ const exported = [];
75
+
76
+ for (const file of files) {
77
+ const uri = toDataUri(path.join(ASSETS_DIR, file));
78
+ if (!uri) continue;
79
+
80
+ const constName = toConstName(file);
81
+ exported.push({ file, constName });
82
+
83
+ sections.push(`// Source: src/assets/${file}`);
84
+ sections.push(`export const ${constName} =`);
85
+ sections.push(uri);
86
+ sections.push('');
87
+ }
88
+
89
+ fs.writeFileSync(OUTPUT_FILE, sections.join('\n'));
90
+
91
+ console.log(`Generated ${OUTPUT_FILE}`);
92
+ exported.forEach(({ file, constName }) =>
93
+ console.log(` ${constName.padEnd(30)} ← src/assets/${file}`)
94
+ );
@@ -1,9 +0,0 @@
1
- <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2
- <rect width="64" height="64" fill="url(#pattern0_17_2)"/>
3
- <defs>
4
- <pattern id="pattern0_17_2" patternContentUnits="objectBoundingBox" width="1" height="1">
5
- <use xlink:href="#image0_17_2" transform="scale(0.015625)"/>
6
- </pattern>
7
- <image id="image0_17_2" width="64" height="64" xlink:href="data:image/png;base64,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"/>
8
- </defs>
9
- </svg>