@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.
- package/app/components/actions/case-export/download-handlers.ts +51 -3
- package/app/components/actions/case-import/confirmation-import.ts +41 -17
- package/app/components/actions/case-import/confirmation-package.ts +86 -0
- package/app/components/actions/case-import/index.ts +1 -0
- package/app/components/actions/case-import/orchestrator.ts +12 -2
- package/app/components/actions/case-import/validation.ts +5 -98
- package/app/components/actions/case-import/zip-processing.ts +44 -2
- package/app/components/actions/confirm-export.ts +44 -13
- package/app/components/form/form-button.tsx +1 -1
- package/app/components/form/form.module.css +9 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
- package/app/components/sidebar/case-export/case-export.tsx +2 -54
- package/app/components/sidebar/case-import/case-import.tsx +18 -6
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +6 -4
- package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
- package/app/components/sidebar/cases/case-sidebar.tsx +101 -46
- package/app/components/sidebar/cases/cases.module.css +101 -18
- package/app/components/sidebar/notes/notes.module.css +33 -13
- package/app/components/user/manage-profile.tsx +1 -1
- package/app/components/user/mfa-phone-update.tsx +15 -12
- package/app/root.tsx +2 -2
- package/app/routes/auth/login.tsx +129 -6
- package/app/utils/SHA256.ts +5 -1
- package/app/utils/confirmation-signature.ts +5 -1
- package/app/utils/export-verification.ts +353 -0
- package/app/utils/signature-utils.ts +74 -4
- package/package.json +7 -4
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +39 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/react-router.config.ts +5 -0
- package/workers/pdf-worker/scripts/generate-assets.js +94 -0
- 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 =
|
|
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.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Striae is a cloud-native
|
|
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
|
|
55
|
-
"workers
|
|
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",
|
package/public/favicon.ico
CHANGED
|
Binary file
|
package/public/icon-256.png
CHANGED
|
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,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
|
+
);
|
package/public/favicon.svg
DELETED
|
@@ -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>
|