@striae-org/striae 7.1.2 → 8.0.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/.env.example +18 -1
- package/app/components/actions/case-manage/operations.ts +2 -1
- package/app/routes/striae/utils/case-export.ts +1 -17
- package/app/utils/data/operations/case-export-loader.ts +17 -0
- package/app/utils/forensics/signature-utils.ts +110 -44
- package/functions/[[path]].ts +2 -1
- package/load-context.ts +13 -1
- package/package.json +23 -21
- package/public/_routes.json +1 -0
- package/react-router.config.ts +7 -0
- package/scripts/deploy-all.sh +28 -14
- package/scripts/deploy-config/modules/env-utils.sh +10 -1
- package/scripts/deploy-config/modules/keys.sh +33 -4
- package/scripts/deploy-config/modules/prompt.sh +2 -0
- package/scripts/deploy-config/modules/scaffolding.sh +32 -14
- package/scripts/deploy-config/modules/validation.sh +6 -1
- package/scripts/deploy-worker-secrets.sh +20 -50
- package/scripts/encrypt-registry.mjs +104 -0
- package/scripts/upload-registries.sh +146 -0
- package/shared/registry/r2-key-registry.ts +161 -0
- package/shared/registry/registry-encryption.ts +112 -0
- package/vite.config.ts +2 -1
- package/workers/audit-worker/package.json +2 -2
- package/workers/audit-worker/src/crypto/data-at-rest.ts +9 -72
- package/workers/audit-worker/src/types.ts +2 -0
- package/workers/audit-worker/wrangler.jsonc.example +5 -1
- package/workers/data-worker/package.json +2 -2
- package/workers/data-worker/src/handlers/decrypt-export.ts +1 -1
- package/workers/data-worker/src/handlers/signing.ts +5 -2
- package/workers/data-worker/src/registry/key-registry.ts +43 -101
- package/workers/data-worker/src/types.ts +6 -2
- package/workers/data-worker/wrangler.jsonc.example +5 -1
- package/workers/image-worker/package.json +2 -2
- package/workers/image-worker/src/security/key-registry.ts +13 -74
- package/workers/image-worker/src/types.ts +2 -0
- package/workers/image-worker/wrangler.jsonc.example +5 -1
- package/workers/lists-worker/package.json +2 -2
- package/workers/lists-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +2 -2
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +2 -2
- package/workers/user-worker/src/cleanup/account-deletion.ts +9 -74
- package/workers/user-worker/src/registry/user-kv.ts +8 -65
- package/workers/user-worker/src/storage/user-records.ts +1 -1
- package/workers/user-worker/src/types.ts +2 -0
- package/workers/user-worker/wrangler.jsonc.example +5 -1
- package/wrangler.toml.example +1 -1
package/.env.example
CHANGED
|
@@ -47,6 +47,10 @@ USER_KV_ENCRYPTION_ACTIVE_KEY_ID=
|
|
|
47
47
|
MANIFEST_SIGNING_PRIVATE_KEY=your_manifest_signing_private_key_here
|
|
48
48
|
MANIFEST_SIGNING_KEY_ID=your_manifest_signing_key_id_here
|
|
49
49
|
MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
|
|
50
|
+
# Optional key registry for rotation-safe manifest signing.
|
|
51
|
+
# JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
|
|
52
|
+
MANIFEST_SIGNING_KEYS_JSON='{}'
|
|
53
|
+
MANIFEST_SIGNING_ACTIVE_KEY_ID=
|
|
50
54
|
|
|
51
55
|
# DATA EXPORT ENCRYPTION CONFIGURATION
|
|
52
56
|
EXPORT_ENCRYPTION_PRIVATE_KEY=your_export_encryption_private_key_here
|
|
@@ -62,11 +66,17 @@ DATA_AT_REST_ENCRYPTION_ENABLED=true
|
|
|
62
66
|
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY=your_data_at_rest_encryption_private_key_here
|
|
63
67
|
DATA_AT_REST_ENCRYPTION_KEY_ID=your_data_at_rest_encryption_key_id_here
|
|
64
68
|
DATA_AT_REST_ENCRYPTION_PUBLIC_KEY=your_data_at_rest_encryption_public_key_here
|
|
65
|
-
#
|
|
69
|
+
# Key registries are now stored in R2 at CONFIG_BUCKET_NAME/{scope}-keys.json.
|
|
70
|
+
# These env vars are only used for initial upload to R2 via scripts/upload-registries.sh.
|
|
66
71
|
# JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
|
|
67
72
|
DATA_AT_REST_ENCRYPTION_KEYS_JSON='{}'
|
|
68
73
|
DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID=
|
|
69
74
|
|
|
75
|
+
# REGISTRY ENCRYPTION KEY
|
|
76
|
+
# AES-256-GCM key (32 bytes, base64url-encoded) used to encrypt key registries at rest in R2.
|
|
77
|
+
# Generated automatically by deploy-config.sh. Shared across all workers that read registries.
|
|
78
|
+
REGISTRY_ENCRYPTION_KEY=your_registry_encryption_key_here
|
|
79
|
+
|
|
70
80
|
# ================================
|
|
71
81
|
# PAGES WORKER ENVIRONMENT VARIABLES
|
|
72
82
|
# ================================
|
|
@@ -79,6 +89,13 @@ PAGES_CUSTOM_DOMAIN=your_custom_domain_here
|
|
|
79
89
|
USER_WORKER_NAME=your_user_worker_name_here
|
|
80
90
|
KV_STORE_ID=your_kv_store_id_here
|
|
81
91
|
|
|
92
|
+
# ================================
|
|
93
|
+
# SHARED CONFIG BUCKET (key registries)
|
|
94
|
+
# ================================
|
|
95
|
+
# All key registries are stored in R2 at this bucket as {scope}-keys.json.
|
|
96
|
+
# Dev: striae-dev-config | Prod: striae-config
|
|
97
|
+
CONFIG_BUCKET_NAME=your_config_bucket_name_here
|
|
98
|
+
|
|
82
99
|
# ================================
|
|
83
100
|
# DATA WORKER ENVIRONMENT VARIABLES
|
|
84
101
|
# ================================
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from '~/utils/data';
|
|
17
17
|
import { type CaseData, type CaseExportData, type ValidationAuditEntry } from '~/types';
|
|
18
18
|
import { auditService } from '~/services/audit';
|
|
19
|
-
import {
|
|
19
|
+
import { loadCaseExportActions } from '~/utils/data/operations/case-export-loader';
|
|
20
20
|
import { buildArchivePackage } from './archive-package-builder';
|
|
21
21
|
import { deleteFileWithoutAudit } from './delete-helpers';
|
|
22
22
|
import { isReadOnlyCaseData, sortCaseNumbers, validateCaseNumber } from './utils';
|
|
@@ -600,6 +600,7 @@ export const archiveCase = async (
|
|
|
600
600
|
isReadOnly: false,
|
|
601
601
|
} as CaseData;
|
|
602
602
|
|
|
603
|
+
const { exportCaseData, formatDateForFilename } = await loadCaseExportActions();
|
|
603
604
|
const exportData = await exportCaseData(user, caseNumber, { includeMetadata: true });
|
|
604
605
|
const archivedExportData: CaseExportData = {
|
|
605
606
|
...exportData,
|
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export type CaseExportActionsModule = typeof CaseExportActions;
|
|
4
|
-
|
|
5
|
-
let caseExportActionsPromise: Promise<CaseExportActionsModule> | null = null;
|
|
6
|
-
|
|
7
|
-
export const loadCaseExportActions = (): Promise<CaseExportActionsModule> => {
|
|
8
|
-
if (!caseExportActionsPromise) {
|
|
9
|
-
caseExportActionsPromise = import('~/components/actions/case-export').catch((error: unknown) => {
|
|
10
|
-
// Clear cached failures so transient chunk/network errors can recover on retry.
|
|
11
|
-
caseExportActionsPromise = null;
|
|
12
|
-
throw error;
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return caseExportActionsPromise;
|
|
17
|
-
};
|
|
1
|
+
export { loadCaseExportActions, type CaseExportActionsModule } from '~/utils/data/operations/case-export-loader';
|
|
18
2
|
|
|
19
3
|
export const getExportProgressLabel = (progress: number): string => {
|
|
20
4
|
if (progress < 30) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type * as CaseExportActions from '~/components/actions/case-export';
|
|
2
|
+
|
|
3
|
+
export type CaseExportActionsModule = typeof CaseExportActions;
|
|
4
|
+
|
|
5
|
+
let caseExportActionsPromise: Promise<CaseExportActionsModule> | null = null;
|
|
6
|
+
|
|
7
|
+
export const loadCaseExportActions = (): Promise<CaseExportActionsModule> => {
|
|
8
|
+
if (!caseExportActionsPromise) {
|
|
9
|
+
caseExportActionsPromise = import('~/components/actions/case-export').catch((error: unknown) => {
|
|
10
|
+
// Clear cached failures so transient chunk/network errors can recover on retry.
|
|
11
|
+
caseExportActionsPromise = null;
|
|
12
|
+
throw error;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return caseExportActionsPromise;
|
|
17
|
+
};
|
|
@@ -29,6 +29,11 @@ export interface PublicSigningKeyDetails {
|
|
|
29
29
|
publicKeyPem: string | null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
interface VerificationKeyCandidate {
|
|
33
|
+
keyId: string;
|
|
34
|
+
publicKeyPem: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
const RSA_PSS_SALT_LENGTH = 32;
|
|
33
38
|
|
|
34
39
|
type ManifestSigningConfig = {
|
|
@@ -155,6 +160,80 @@ export function getVerificationPublicKey(keyId: string): string | null {
|
|
|
155
160
|
return null;
|
|
156
161
|
}
|
|
157
162
|
|
|
163
|
+
function getVerificationPublicKeyCandidates(signatureKeyId: string): VerificationKeyCandidate[] {
|
|
164
|
+
const config = paths as unknown as ManifestSigningConfig;
|
|
165
|
+
const keyMap = config.manifest_signing_public_keys;
|
|
166
|
+
const candidates: VerificationKeyCandidate[] = [];
|
|
167
|
+
const seenKeyIds = new Set<string>();
|
|
168
|
+
|
|
169
|
+
const appendCandidate = (keyId: string | null, publicKeyPem: string | null): void => {
|
|
170
|
+
if (!keyId || !publicKeyPem || seenKeyIds.has(keyId)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
seenKeyIds.add(keyId);
|
|
175
|
+
candidates.push({ keyId, publicKeyPem });
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
appendCandidate(signatureKeyId, getVerificationPublicKey(signatureKeyId));
|
|
179
|
+
|
|
180
|
+
const configuredKeyId =
|
|
181
|
+
typeof config.manifest_signing_key_id === 'string' && config.manifest_signing_key_id.trim().length > 0
|
|
182
|
+
? config.manifest_signing_key_id.trim()
|
|
183
|
+
: null;
|
|
184
|
+
const legacyPublicKeyPem = normalizePemOrNull(config.manifest_signing_public_key);
|
|
185
|
+
|
|
186
|
+
appendCandidate(configuredKeyId, configuredKeyId ? getVerificationPublicKey(configuredKeyId) : legacyPublicKeyPem);
|
|
187
|
+
|
|
188
|
+
if (keyMap && typeof keyMap === 'object') {
|
|
189
|
+
const orderedEntries = Object.entries(keyMap)
|
|
190
|
+
.filter(([, value]) => typeof value === 'string' && value.trim().length > 0)
|
|
191
|
+
.sort(([leftKeyId], [rightKeyId]) => leftKeyId.localeCompare(rightKeyId));
|
|
192
|
+
|
|
193
|
+
for (const [keyId, pemValue] of orderedEntries) {
|
|
194
|
+
appendCandidate(keyId, normalizePemPublicKey(pemValue));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (candidates.length === 0 && legacyPublicKeyPem) {
|
|
199
|
+
appendCandidate(configuredKeyId ?? signatureKeyId, legacyPublicKeyPem);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return candidates;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function verifyWithPublicKey(
|
|
206
|
+
payload: string,
|
|
207
|
+
signatureValue: string,
|
|
208
|
+
publicKeyPem: string,
|
|
209
|
+
invalidPublicKeyError: string
|
|
210
|
+
): Promise<boolean> {
|
|
211
|
+
const key = await crypto.subtle.importKey(
|
|
212
|
+
'spki',
|
|
213
|
+
publicKeyPemToArrayBuffer(publicKeyPem, invalidPublicKeyError),
|
|
214
|
+
{
|
|
215
|
+
name: 'RSA-PSS',
|
|
216
|
+
hash: 'SHA-256'
|
|
217
|
+
},
|
|
218
|
+
false,
|
|
219
|
+
['verify']
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const signatureBytes = base64UrlToUint8Array(signatureValue);
|
|
223
|
+
const signatureBuffer = new Uint8Array(signatureBytes.byteLength);
|
|
224
|
+
signatureBuffer.set(signatureBytes);
|
|
225
|
+
|
|
226
|
+
return crypto.subtle.verify(
|
|
227
|
+
{
|
|
228
|
+
name: 'RSA-PSS',
|
|
229
|
+
saltLength: RSA_PSS_SALT_LENGTH
|
|
230
|
+
},
|
|
231
|
+
key,
|
|
232
|
+
signatureBuffer,
|
|
233
|
+
new TextEncoder().encode(payload)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
158
237
|
export async function verifySignaturePayload(
|
|
159
238
|
payload: string,
|
|
160
239
|
signature: SignatureEnvelope,
|
|
@@ -177,59 +256,46 @@ export async function verifySignaturePayload(
|
|
|
177
256
|
};
|
|
178
257
|
}
|
|
179
258
|
|
|
180
|
-
const publicKeyPem =
|
|
181
|
-
typeof options.verificationPublicKeyPem === 'string' && options.verificationPublicKeyPem.trim().length > 0
|
|
182
|
-
? options.verificationPublicKeyPem
|
|
183
|
-
: getVerificationPublicKey(signature.keyId);
|
|
184
|
-
if (!publicKeyPem) {
|
|
185
|
-
return {
|
|
186
|
-
isValid: false,
|
|
187
|
-
keyId: signature.keyId,
|
|
188
|
-
error: `${messages.noVerificationKeyPrefix || 'No verification key configured for key ID'}: ${signature.keyId}`
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
259
|
const verificationFailedError = messages.verificationFailedError || 'Signature verification failed';
|
|
193
260
|
const invalidPublicKeyError =
|
|
194
261
|
messages.invalidPublicKeyError ||
|
|
195
262
|
`${verificationFailedError}: invalid public key`;
|
|
196
263
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
},
|
|
205
|
-
false,
|
|
206
|
-
['verify']
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
const signatureBytes = base64UrlToUint8Array(signature.value);
|
|
210
|
-
const signatureBuffer = new Uint8Array(signatureBytes.byteLength);
|
|
211
|
-
signatureBuffer.set(signatureBytes);
|
|
212
|
-
|
|
213
|
-
const verified = await crypto.subtle.verify(
|
|
214
|
-
{
|
|
215
|
-
name: 'RSA-PSS',
|
|
216
|
-
saltLength: RSA_PSS_SALT_LENGTH
|
|
217
|
-
},
|
|
218
|
-
key,
|
|
219
|
-
signatureBuffer,
|
|
220
|
-
new TextEncoder().encode(payload)
|
|
221
|
-
);
|
|
264
|
+
const explicitVerificationKey =
|
|
265
|
+
typeof options.verificationPublicKeyPem === 'string' && options.verificationPublicKeyPem.trim().length > 0
|
|
266
|
+
? options.verificationPublicKeyPem
|
|
267
|
+
: null;
|
|
268
|
+
const keyCandidates = explicitVerificationKey
|
|
269
|
+
? [{ keyId: signature.keyId, publicKeyPem: explicitVerificationKey }]
|
|
270
|
+
: getVerificationPublicKeyCandidates(signature.keyId);
|
|
222
271
|
|
|
223
|
-
|
|
224
|
-
isValid: verified,
|
|
225
|
-
keyId: signature.keyId,
|
|
226
|
-
error: verified ? undefined : verificationFailedError
|
|
227
|
-
};
|
|
228
|
-
} catch (error) {
|
|
272
|
+
if (keyCandidates.length === 0) {
|
|
229
273
|
return {
|
|
230
274
|
isValid: false,
|
|
231
275
|
keyId: signature.keyId,
|
|
232
|
-
error:
|
|
276
|
+
error: `${messages.noVerificationKeyPrefix || 'No verification key configured for key ID'}: ${signature.keyId}`
|
|
233
277
|
};
|
|
234
278
|
}
|
|
279
|
+
|
|
280
|
+
let lastError: unknown;
|
|
281
|
+
|
|
282
|
+
for (const candidate of keyCandidates) {
|
|
283
|
+
try {
|
|
284
|
+
const verified = await verifyWithPublicKey(payload, signature.value, candidate.publicKeyPem, invalidPublicKeyError);
|
|
285
|
+
if (verified) {
|
|
286
|
+
return {
|
|
287
|
+
isValid: true,
|
|
288
|
+
keyId: signature.keyId
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
lastError = error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
isValid: false,
|
|
298
|
+
keyId: signature.keyId,
|
|
299
|
+
error: lastError instanceof Error ? lastError.message : verificationFailedError
|
|
300
|
+
};
|
|
235
301
|
}
|
package/functions/[[path]].ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createPagesFunctionHandler } from "@react-router/cloudflare";
|
|
2
|
+
import { getLoadContext } from "../load-context";
|
|
2
3
|
|
|
3
4
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4
5
|
// @ts-ignore - the server build file is generated by `react-router build`
|
|
@@ -6,4 +7,4 @@ import * as build from "../build/server";
|
|
|
6
7
|
|
|
7
8
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
8
9
|
// @ts-ignore - temporary workaround for crossOrigin type issue
|
|
9
|
-
export const onRequest = createPagesFunctionHandler({ build });
|
|
10
|
+
export const onRequest = createPagesFunctionHandler({ build, getLoadContext });
|
package/load-context.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { type PlatformProxy } from "wrangler";
|
|
2
|
+
import { RouterContextProvider } from "react-router";
|
|
2
3
|
|
|
3
4
|
type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
|
|
4
5
|
|
|
5
6
|
declare module "react-router" {
|
|
6
|
-
interface
|
|
7
|
+
interface RouterContextProvider {
|
|
7
8
|
cloudflare: Cloudflare;
|
|
8
9
|
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getLoadContext({
|
|
13
|
+
context,
|
|
14
|
+
}: {
|
|
15
|
+
request: Request;
|
|
16
|
+
context: { cloudflare: Cloudflare };
|
|
17
|
+
}) {
|
|
18
|
+
const provider = new RouterContextProvider();
|
|
19
|
+
Object.assign(provider, context);
|
|
20
|
+
return provider;
|
|
9
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.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",
|
|
@@ -88,6 +88,8 @@
|
|
|
88
88
|
"update-versions": "node ./scripts/update-markdown-versions.cjs",
|
|
89
89
|
"update-compatibility-dates": "node ./scripts/update-compatibility-dates.cjs",
|
|
90
90
|
"deploy-config": "bash ./scripts/deploy-config.sh",
|
|
91
|
+
"deploy-config:rotate-keys": "bash ./scripts/deploy-config.sh --force-rotate-keys",
|
|
92
|
+
"upload-registries": "bash ./scripts/upload-registries.sh",
|
|
91
93
|
"update-env": "bash ./scripts/deploy-config.sh --update-env",
|
|
92
94
|
"install-workers": "bash ./scripts/install-workers.sh",
|
|
93
95
|
"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",
|
|
@@ -102,44 +104,44 @@
|
|
|
102
104
|
"deploy-workers:user": "cd workers/user-worker && npm run deploy"
|
|
103
105
|
},
|
|
104
106
|
"dependencies": {
|
|
105
|
-
"@react-router/cloudflare": "^7.
|
|
106
|
-
"firebase": "^12.
|
|
107
|
-
"isbot": "^5.1.
|
|
107
|
+
"@react-router/cloudflare": "^7.17.0",
|
|
108
|
+
"firebase": "^12.14.0",
|
|
109
|
+
"isbot": "^5.1.41",
|
|
108
110
|
"jszip": "^3.10.1",
|
|
109
111
|
"qrcode": "^1.5.4",
|
|
110
|
-
"react": "^19.2.
|
|
111
|
-
"react-dom": "^19.2.
|
|
112
|
-
"react-router": "^7.
|
|
112
|
+
"react": "^19.2.7",
|
|
113
|
+
"react-dom": "^19.2.7",
|
|
114
|
+
"react-router": "^7.17.0"
|
|
113
115
|
},
|
|
114
116
|
"devDependencies": {
|
|
115
|
-
"@cloudflare/vitest-pool-workers": "^0.
|
|
116
|
-
"@react-router/dev": "^7.
|
|
117
|
-
"@react-router/fs-routes": "^7.
|
|
117
|
+
"@cloudflare/vitest-pool-workers": "^0.16.9",
|
|
118
|
+
"@react-router/dev": "^7.17.0",
|
|
119
|
+
"@react-router/fs-routes": "^7.17.0",
|
|
118
120
|
"@types/qrcode": "^1.5.6",
|
|
119
|
-
"@types/react": "^19.2.
|
|
121
|
+
"@types/react": "^19.2.17",
|
|
120
122
|
"@types/react-dom": "^19.2.3",
|
|
121
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
122
|
-
"@typescript-eslint/parser": "^8.
|
|
123
|
-
"@vitest/coverage-v8": "^4.1.
|
|
123
|
+
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
|
124
|
+
"@typescript-eslint/parser": "^8.60.1",
|
|
125
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
124
126
|
"eslint": "^9.39.4",
|
|
125
|
-
"eslint-import-resolver-typescript": "^4.4.
|
|
127
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
126
128
|
"eslint-plugin-import": "^2.32.0",
|
|
127
129
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
128
130
|
"eslint-plugin-react": "^7.37.5",
|
|
129
131
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
130
|
-
"firebase-admin": "^13.
|
|
132
|
+
"firebase-admin": "^13.10.0",
|
|
131
133
|
"modern-normalize": "^3.0.1",
|
|
132
134
|
"typescript": "^6.0.3",
|
|
133
|
-
"vite": "^8.0.
|
|
134
|
-
"vitest": "^4.1.
|
|
135
|
-
"wrangler": "^4.
|
|
135
|
+
"vite": "^8.0.16",
|
|
136
|
+
"vitest": "^4.1.8",
|
|
137
|
+
"wrangler": "^4.98.0"
|
|
136
138
|
},
|
|
137
139
|
"overrides": {
|
|
138
140
|
"@tootallnate/once": "3.0.1",
|
|
139
141
|
"uuid": "^14.0.0"
|
|
140
142
|
},
|
|
141
143
|
"engines": {
|
|
142
|
-
"node": ">=
|
|
144
|
+
"node": ">=22.0.0"
|
|
143
145
|
},
|
|
144
146
|
"packageManager": "npm@11.13.0"
|
|
145
|
-
}
|
|
147
|
+
}
|
package/public/_routes.json
CHANGED
package/react-router.config.ts
CHANGED
|
@@ -2,4 +2,11 @@ import type { Config } from "@react-router/dev/config";
|
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
ssr: true,
|
|
5
|
+
future: {
|
|
6
|
+
v8_middleware: true,
|
|
7
|
+
v8_splitRouteModules: true,
|
|
8
|
+
v8_viteEnvironmentApi: true,
|
|
9
|
+
v8_passThroughRequests: true,
|
|
10
|
+
v8_trailingSlashAwareDataRequests: true,
|
|
11
|
+
},
|
|
5
12
|
} satisfies Config;
|
package/scripts/deploy-all.sh
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
# This script deploys the entire Striae application:
|
|
7
7
|
# 1. Configuration setup (copy configs, replace placeholders)
|
|
8
8
|
# 2. Worker dependencies installation
|
|
9
|
-
# 3.
|
|
10
|
-
# 4.
|
|
11
|
-
# 5.
|
|
12
|
-
# 6.
|
|
9
|
+
# 3. Wrangler types generation
|
|
10
|
+
# 4. Workers (all 6 workers)
|
|
11
|
+
# 5. Key registries (upload to R2 config bucket)
|
|
12
|
+
# 6. Worker secrets/environment variables
|
|
13
|
+
# 7. Pages secrets/environment variables
|
|
14
|
+
# 8. Pages (frontend)
|
|
13
15
|
|
|
14
16
|
set -e
|
|
15
17
|
set -o pipefail
|
|
@@ -67,6 +69,7 @@ require_command wrangler
|
|
|
67
69
|
assert_file_exists "$SCRIPT_DIR/deploy-config.sh"
|
|
68
70
|
assert_file_exists "$SCRIPT_DIR/install-workers.sh"
|
|
69
71
|
assert_file_exists "$SCRIPT_DIR/deploy-worker-secrets.sh"
|
|
72
|
+
assert_file_exists "$SCRIPT_DIR/upload-registries.sh"
|
|
70
73
|
assert_file_exists "$SCRIPT_DIR/deploy-pages-secrets.sh"
|
|
71
74
|
assert_file_exists "package.json"
|
|
72
75
|
|
|
@@ -79,7 +82,7 @@ echo -e "${GREEN}✅ Preflight checks passed${NC}"
|
|
|
79
82
|
echo ""
|
|
80
83
|
|
|
81
84
|
# Step 1: Configuration Setup
|
|
82
|
-
echo -e "${PURPLE}Step 1/
|
|
85
|
+
echo -e "${PURPLE}Step 1/8: Configuration Setup${NC}"
|
|
83
86
|
echo "------------------------------"
|
|
84
87
|
echo -e "${YELLOW}⚙️ Setting up configuration files and replacing placeholders...${NC}"
|
|
85
88
|
if ! bash "$SCRIPT_DIR/deploy-config.sh"; then
|
|
@@ -92,7 +95,7 @@ run_config_checkpoint
|
|
|
92
95
|
echo ""
|
|
93
96
|
|
|
94
97
|
# Step 2: Install Worker Dependencies
|
|
95
|
-
echo -e "${PURPLE}Step 2/
|
|
98
|
+
echo -e "${PURPLE}Step 2/8: Installing Worker Dependencies${NC}"
|
|
96
99
|
echo "----------------------------------------"
|
|
97
100
|
echo -e "${YELLOW}📦 Installing npm dependencies for all workers...${NC}"
|
|
98
101
|
if ! bash "$SCRIPT_DIR/install-workers.sh"; then
|
|
@@ -103,7 +106,7 @@ echo -e "${GREEN}✅ All worker dependencies installed successfully${NC}"
|
|
|
103
106
|
echo ""
|
|
104
107
|
|
|
105
108
|
# Step 3: Generate Wrangler Types
|
|
106
|
-
echo -e "${PURPLE}Step 3/
|
|
109
|
+
echo -e "${PURPLE}Step 3/8: Generating Wrangler Types${NC}"
|
|
107
110
|
echo "-------------------------------------"
|
|
108
111
|
echo -e "${YELLOW}📝 Running wrangler types in root and all worker directories...${NC}"
|
|
109
112
|
if ! npx wrangler types; then
|
|
@@ -121,7 +124,7 @@ echo -e "${GREEN}✅ Wrangler types generated successfully${NC}"
|
|
|
121
124
|
echo ""
|
|
122
125
|
|
|
123
126
|
# Step 4: Deploy Workers
|
|
124
|
-
echo -e "${PURPLE}Step 4/
|
|
127
|
+
echo -e "${PURPLE}Step 4/8: Deploying Workers${NC}"
|
|
125
128
|
echo "----------------------------"
|
|
126
129
|
echo -e "${YELLOW}🔧 Deploying all 6 Cloudflare Workers...${NC}"
|
|
127
130
|
if ! npm run deploy-workers; then
|
|
@@ -131,8 +134,19 @@ fi
|
|
|
131
134
|
echo -e "${GREEN}✅ All workers deployed successfully${NC}"
|
|
132
135
|
echo ""
|
|
133
136
|
|
|
134
|
-
# Step 5:
|
|
135
|
-
echo -e "${PURPLE}Step 5/
|
|
137
|
+
# Step 5: Upload Key Registries to R2
|
|
138
|
+
echo -e "${PURPLE}Step 5/8: Uploading Key Registries to R2${NC}"
|
|
139
|
+
echo "-----------------------------------------"
|
|
140
|
+
echo -e "${YELLOW}📦 Uploading key registries to config bucket...${NC}"
|
|
141
|
+
if ! bash "$SCRIPT_DIR/upload-registries.sh"; then
|
|
142
|
+
echo -e "${RED}❌ Key registry upload failed!${NC}"
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
echo -e "${GREEN}✅ Key registries uploaded successfully${NC}"
|
|
146
|
+
echo ""
|
|
147
|
+
|
|
148
|
+
# Step 6: Deploy Worker Secrets
|
|
149
|
+
echo -e "${PURPLE}Step 6/8: Deploying Worker Secrets${NC}"
|
|
136
150
|
echo "-----------------------------------"
|
|
137
151
|
echo -e "${YELLOW}🔐 Deploying worker environment variables...${NC}"
|
|
138
152
|
if ! bash "$SCRIPT_DIR/deploy-worker-secrets.sh"; then
|
|
@@ -142,8 +156,8 @@ fi
|
|
|
142
156
|
echo -e "${GREEN}✅ Worker secrets deployed successfully${NC}"
|
|
143
157
|
echo ""
|
|
144
158
|
|
|
145
|
-
# Step
|
|
146
|
-
echo -e "${PURPLE}Step
|
|
159
|
+
# Step 7: Deploy Pages Secrets
|
|
160
|
+
echo -e "${PURPLE}Step 7/8: Deploying Pages Secrets${NC}"
|
|
147
161
|
echo "----------------------------------"
|
|
148
162
|
echo -e "${YELLOW}🔐 Deploying Pages environment variables...${NC}"
|
|
149
163
|
if ! bash "$SCRIPT_DIR/deploy-pages-secrets.sh"; then
|
|
@@ -153,8 +167,8 @@ fi
|
|
|
153
167
|
echo -e "${GREEN}✅ Pages secrets deployed successfully${NC}"
|
|
154
168
|
echo ""
|
|
155
169
|
|
|
156
|
-
# Step
|
|
157
|
-
echo -e "${PURPLE}Step
|
|
170
|
+
# Step 8: Deploy Pages
|
|
171
|
+
echo -e "${PURPLE}Step 8/8: Deploying Pages${NC}"
|
|
158
172
|
echo "--------------------------"
|
|
159
173
|
echo -e "${YELLOW}🌐 Building and deploying Pages...${NC}"
|
|
160
174
|
if ! npm run deploy-pages; then
|
|
@@ -187,7 +187,7 @@ write_env_var() {
|
|
|
187
187
|
var_value=$(strip_carriage_returns "$var_value")
|
|
188
188
|
env_file_value="$var_value"
|
|
189
189
|
|
|
190
|
-
if [ "$var_name" = "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "USER_KV_ENCRYPTION_KEYS_JSON" ]; then
|
|
190
|
+
if [ "$var_name" = "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_KEYS_JSON" ] || [ "$var_name" = "EXPORT_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "USER_KV_ENCRYPTION_KEYS_JSON" ]; then
|
|
191
191
|
# Store as a quoted string so sourced .env preserves escaped newline markers (\n)
|
|
192
192
|
env_file_value=${env_file_value//\"/\\\"}
|
|
193
193
|
env_file_value="\"$env_file_value\""
|
|
@@ -252,3 +252,12 @@ update_private_key_registry() {
|
|
|
252
252
|
escape_for_sed_replacement() {
|
|
253
253
|
printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g'
|
|
254
254
|
}
|
|
255
|
+
|
|
256
|
+
generate_10_char_id() {
|
|
257
|
+
local raw label
|
|
258
|
+
raw=$(openssl rand -base64 32 2>/dev/null | tr -dc 'a-z0-9' || true)
|
|
259
|
+
label="${raw:0:10}"
|
|
260
|
+
if [ -n "$label" ] && [ ${#label} -eq 10 ]; then
|
|
261
|
+
printf '%s' "$label"
|
|
262
|
+
fi
|
|
263
|
+
}
|
|
@@ -108,6 +108,8 @@ configure_manifest_signing_credentials() {
|
|
|
108
108
|
restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_PRIVATE_KEY"
|
|
109
109
|
restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_PUBLIC_KEY"
|
|
110
110
|
restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_KEY_ID"
|
|
111
|
+
restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_KEYS_JSON"
|
|
112
|
+
restore_env_var_from_backup_if_missing "MANIFEST_SIGNING_ACTIVE_KEY_ID"
|
|
111
113
|
|
|
112
114
|
if [ -z "$MANIFEST_SIGNING_PRIVATE_KEY" ] || is_placeholder "$MANIFEST_SIGNING_PRIVATE_KEY" || [ -z "$MANIFEST_SIGNING_PUBLIC_KEY" ] || is_placeholder "$MANIFEST_SIGNING_PUBLIC_KEY"; then
|
|
113
115
|
should_generate="true"
|
|
@@ -131,7 +133,7 @@ configure_manifest_signing_credentials() {
|
|
|
131
133
|
|
|
132
134
|
if [ -z "$MANIFEST_SIGNING_KEY_ID" ] || is_placeholder "$MANIFEST_SIGNING_KEY_ID" || [ "$should_generate" = "true" ]; then
|
|
133
135
|
local generated_key_id
|
|
134
|
-
generated_key_id=$(
|
|
136
|
+
generated_key_id=$(generate_10_char_id)
|
|
135
137
|
if [ -z "$generated_key_id" ] || [ ${#generated_key_id} -ne 10 ]; then
|
|
136
138
|
echo -e "${RED}❌ Error: Failed to generate MANIFEST_SIGNING_KEY_ID${NC}"
|
|
137
139
|
exit 1
|
|
@@ -144,6 +146,8 @@ configure_manifest_signing_credentials() {
|
|
|
144
146
|
echo -e "${GREEN}✅ MANIFEST_SIGNING_KEY_ID: $MANIFEST_SIGNING_KEY_ID${NC}"
|
|
145
147
|
fi
|
|
146
148
|
|
|
149
|
+
update_private_key_registry "MANIFEST_SIGNING_KEYS_JSON" "MANIFEST_SIGNING_ACTIVE_KEY_ID" "$MANIFEST_SIGNING_KEY_ID" "$MANIFEST_SIGNING_PRIVATE_KEY" "manifest signing"
|
|
150
|
+
|
|
147
151
|
echo ""
|
|
148
152
|
}
|
|
149
153
|
|
|
@@ -212,7 +216,7 @@ configure_export_encryption_credentials() {
|
|
|
212
216
|
|
|
213
217
|
if [ -z "$EXPORT_ENCRYPTION_KEY_ID" ] || is_placeholder "$EXPORT_ENCRYPTION_KEY_ID" || [ "$should_generate" = "true" ]; then
|
|
214
218
|
local generated_key_id
|
|
215
|
-
generated_key_id=$(
|
|
219
|
+
generated_key_id=$(generate_10_char_id)
|
|
216
220
|
if [ -z "$generated_key_id" ] || [ ${#generated_key_id} -ne 10 ]; then
|
|
217
221
|
echo -e "${RED}❌ Error: Failed to generate EXPORT_ENCRYPTION_KEY_ID${NC}"
|
|
218
222
|
exit 1
|
|
@@ -327,7 +331,7 @@ configure_user_kv_encryption_credentials() {
|
|
|
327
331
|
|
|
328
332
|
if [ -z "$USER_KV_ENCRYPTION_KEY_ID" ] || is_placeholder "$USER_KV_ENCRYPTION_KEY_ID" || [ "$should_generate" = "true" ]; then
|
|
329
333
|
local generated_key_id
|
|
330
|
-
generated_key_id=$(
|
|
334
|
+
generated_key_id=$(generate_10_char_id)
|
|
331
335
|
if [ -z "$generated_key_id" ] || [ ${#generated_key_id} -ne 10 ]; then
|
|
332
336
|
echo -e "${RED}❌ Error: Failed to generate USER_KV_ENCRYPTION_KEY_ID${NC}"
|
|
333
337
|
exit 1
|
|
@@ -385,7 +389,7 @@ configure_data_at_rest_encryption_credentials() {
|
|
|
385
389
|
|
|
386
390
|
if [ -z "$DATA_AT_REST_ENCRYPTION_KEY_ID" ] || is_placeholder "$DATA_AT_REST_ENCRYPTION_KEY_ID" || [ "$should_generate" = "true" ]; then
|
|
387
391
|
local generated_key_id
|
|
388
|
-
generated_key_id=$(
|
|
392
|
+
generated_key_id=$(generate_10_char_id)
|
|
389
393
|
if [ -z "$generated_key_id" ] || [ ${#generated_key_id} -ne 10 ]; then
|
|
390
394
|
echo -e "${RED}❌ Error: Failed to generate DATA_AT_REST_ENCRYPTION_KEY_ID${NC}"
|
|
391
395
|
exit 1
|
|
@@ -402,3 +406,28 @@ configure_data_at_rest_encryption_credentials() {
|
|
|
402
406
|
|
|
403
407
|
echo ""
|
|
404
408
|
}
|
|
409
|
+
|
|
410
|
+
configure_registry_encryption_key() {
|
|
411
|
+
echo -e "${BLUE}🔒 REGISTRY ENCRYPTION KEY CONFIGURATION${NC}"
|
|
412
|
+
echo "========================================="
|
|
413
|
+
|
|
414
|
+
restore_env_var_from_backup_if_missing "REGISTRY_ENCRYPTION_KEY"
|
|
415
|
+
|
|
416
|
+
if [ -n "$REGISTRY_ENCRYPTION_KEY" ] && ! is_placeholder "$REGISTRY_ENCRYPTION_KEY"; then
|
|
417
|
+
echo -e "${GREEN}✅ REGISTRY_ENCRYPTION_KEY already configured${NC}"
|
|
418
|
+
else
|
|
419
|
+
echo -e "${YELLOW}Generating REGISTRY_ENCRYPTION_KEY (32 random bytes, base64url)...${NC}"
|
|
420
|
+
local key_value
|
|
421
|
+
key_value=$(node -e "const { randomBytes } = require('crypto'); const buf = randomBytes(32); process.stdout.write(buf.toString('base64url'));")
|
|
422
|
+
if [ -z "$key_value" ] || [ ${#key_value} -lt 20 ]; then
|
|
423
|
+
echo -e "${RED}❌ Error: Failed to generate REGISTRY_ENCRYPTION_KEY${NC}"
|
|
424
|
+
exit 1
|
|
425
|
+
fi
|
|
426
|
+
REGISTRY_ENCRYPTION_KEY="$key_value"
|
|
427
|
+
export REGISTRY_ENCRYPTION_KEY
|
|
428
|
+
write_env_var "REGISTRY_ENCRYPTION_KEY" "$REGISTRY_ENCRYPTION_KEY"
|
|
429
|
+
echo -e "${GREEN}✅ REGISTRY_ENCRYPTION_KEY generated${NC}"
|
|
430
|
+
fi
|
|
431
|
+
|
|
432
|
+
echo ""
|
|
433
|
+
}
|
|
@@ -271,6 +271,7 @@ prompt_for_secrets() {
|
|
|
271
271
|
prompt_for_var "DATA_BUCKET_NAME" "Your R2 bucket name for case data storage"
|
|
272
272
|
prompt_for_var "AUDIT_BUCKET_NAME" "Your R2 bucket name for audit logs (separate from data bucket)"
|
|
273
273
|
prompt_for_var "FILES_BUCKET_NAME" "Your R2 bucket name for encrypted files storage"
|
|
274
|
+
prompt_for_var "CONFIG_BUCKET_NAME" "Your R2 bucket name for config/key registries (shared across workers)"
|
|
274
275
|
prompt_for_var "KV_STORE_ID" "Your KV namespace ID (UUID format)"
|
|
275
276
|
prompt_for_var "STRIAE_LISTS_KV_ID" "KV namespace ID for the lists-worker (UUID format; backs registration and primershear allowlists)"
|
|
276
277
|
|
|
@@ -296,6 +297,7 @@ prompt_for_secrets() {
|
|
|
296
297
|
configure_export_encryption_credentials
|
|
297
298
|
configure_user_kv_encryption_credentials
|
|
298
299
|
configure_data_at_rest_encryption_credentials
|
|
300
|
+
configure_registry_encryption_key
|
|
299
301
|
|
|
300
302
|
# Reload the updated .env file
|
|
301
303
|
source .env
|