@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.
Files changed (47) hide show
  1. package/.env.example +18 -1
  2. package/app/components/actions/case-manage/operations.ts +2 -1
  3. package/app/routes/striae/utils/case-export.ts +1 -17
  4. package/app/utils/data/operations/case-export-loader.ts +17 -0
  5. package/app/utils/forensics/signature-utils.ts +110 -44
  6. package/functions/[[path]].ts +2 -1
  7. package/load-context.ts +13 -1
  8. package/package.json +23 -21
  9. package/public/_routes.json +1 -0
  10. package/react-router.config.ts +7 -0
  11. package/scripts/deploy-all.sh +28 -14
  12. package/scripts/deploy-config/modules/env-utils.sh +10 -1
  13. package/scripts/deploy-config/modules/keys.sh +33 -4
  14. package/scripts/deploy-config/modules/prompt.sh +2 -0
  15. package/scripts/deploy-config/modules/scaffolding.sh +32 -14
  16. package/scripts/deploy-config/modules/validation.sh +6 -1
  17. package/scripts/deploy-worker-secrets.sh +20 -50
  18. package/scripts/encrypt-registry.mjs +104 -0
  19. package/scripts/upload-registries.sh +146 -0
  20. package/shared/registry/r2-key-registry.ts +161 -0
  21. package/shared/registry/registry-encryption.ts +112 -0
  22. package/vite.config.ts +2 -1
  23. package/workers/audit-worker/package.json +2 -2
  24. package/workers/audit-worker/src/crypto/data-at-rest.ts +9 -72
  25. package/workers/audit-worker/src/types.ts +2 -0
  26. package/workers/audit-worker/wrangler.jsonc.example +5 -1
  27. package/workers/data-worker/package.json +2 -2
  28. package/workers/data-worker/src/handlers/decrypt-export.ts +1 -1
  29. package/workers/data-worker/src/handlers/signing.ts +5 -2
  30. package/workers/data-worker/src/registry/key-registry.ts +43 -101
  31. package/workers/data-worker/src/types.ts +6 -2
  32. package/workers/data-worker/wrangler.jsonc.example +5 -1
  33. package/workers/image-worker/package.json +2 -2
  34. package/workers/image-worker/src/security/key-registry.ts +13 -74
  35. package/workers/image-worker/src/types.ts +2 -0
  36. package/workers/image-worker/wrangler.jsonc.example +5 -1
  37. package/workers/lists-worker/package.json +2 -2
  38. package/workers/lists-worker/wrangler.jsonc.example +1 -1
  39. package/workers/pdf-worker/package.json +2 -2
  40. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  41. package/workers/user-worker/package.json +2 -2
  42. package/workers/user-worker/src/cleanup/account-deletion.ts +9 -74
  43. package/workers/user-worker/src/registry/user-kv.ts +8 -65
  44. package/workers/user-worker/src/storage/user-records.ts +1 -1
  45. package/workers/user-worker/src/types.ts +2 -0
  46. package/workers/user-worker/wrangler.jsonc.example +5 -1
  47. package/wrangler.toml.example +1 -1
@@ -3,7 +3,7 @@
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/image-worker.ts",
5
5
  "workers_dev": false,
6
- "compatibility_date": "2026-05-07",
6
+ "compatibility_date": "2026-06-07",
7
7
  "compatibility_flags": [
8
8
  "nodejs_compat"
9
9
  ],
@@ -16,6 +16,10 @@
16
16
  {
17
17
  "binding": "STRIAE_FILES",
18
18
  "bucket_name": "FILES_BUCKET_NAME"
19
+ },
20
+ {
21
+ "binding": "STRIAE_CONFIG",
22
+ "bucket_name": "CONFIG_BUCKET_NAME"
19
23
  }
20
24
  ],
21
25
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lists-worker",
3
- "version": "7.1.2",
3
+ "version": "8.0.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -8,6 +8,6 @@
8
8
  "start": "wrangler dev"
9
9
  },
10
10
  "devDependencies": {
11
- "wrangler": "^4.89.1"
11
+ "wrangler": "^4.98.0"
12
12
  }
13
13
  }
@@ -3,7 +3,7 @@
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/lists-worker.ts",
5
5
  "workers_dev": false,
6
- "compatibility_date": "2026-05-07",
6
+ "compatibility_date": "2026-06-07",
7
7
  "compatibility_flags": [
8
8
  "nodejs_compat"
9
9
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-worker",
3
- "version": "7.1.2",
3
+ "version": "8.0.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "generate:assets": "node scripts/generate-assets.js",
@@ -9,6 +9,6 @@
9
9
  "start": "wrangler dev"
10
10
  },
11
11
  "devDependencies": {
12
- "wrangler": "^4.89.1"
12
+ "wrangler": "^4.98.0"
13
13
  }
14
14
  }
@@ -3,7 +3,7 @@
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
5
  "workers_dev": false,
6
- "compatibility_date": "2026-05-07",
6
+ "compatibility_date": "2026-06-07",
7
7
  "compatibility_flags": [
8
8
  "nodejs_compat"
9
9
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "user-worker",
3
- "version": "7.1.2",
3
+ "version": "8.0.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -8,6 +8,6 @@
8
8
  "start": "wrangler dev"
9
9
  },
10
10
  "devDependencies": {
11
- "wrangler": "^4.89.1"
11
+ "wrangler": "^4.98.0"
12
12
  }
13
13
  }
@@ -4,87 +4,22 @@ import { readUserRecord } from '../storage/user-records';
4
4
  import type {
5
5
  AccountDeletionProgressEvent,
6
6
  Env,
7
- KeyRegistryPayload,
8
7
  PrivateKeyRegistry,
9
8
  StoredCaseData
10
9
  } from '../types';
10
+ import { fetchKeyRegistryFromR2 } from '../../../../shared/registry/r2-key-registry';
11
11
 
12
12
  function getNonEmptyString(value: unknown): string | null {
13
13
  return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
14
14
  }
15
15
 
16
- function normalizePrivateKeyPem(rawValue: string): string {
17
- return rawValue.trim().replace(/^['"]|['"]$/g, '').replace(/\\n/g, '\n');
18
- }
19
-
20
- function parseDataAtRestPrivateKeyRegistry(env: Env): PrivateKeyRegistry {
21
- const keys: Record<string, string> = {};
22
- const configuredActiveKeyId = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID);
23
- const registryJson = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_KEYS_JSON);
24
-
25
- if (registryJson) {
26
- let parsedRegistry: unknown;
27
-
28
- try {
29
- parsedRegistry = JSON.parse(registryJson) as unknown;
30
- } catch {
31
- throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON is not valid JSON');
32
- }
33
-
34
- if (!parsedRegistry || typeof parsedRegistry !== 'object') {
35
- throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON must be an object');
36
- }
37
-
38
- const payload = parsedRegistry as KeyRegistryPayload;
39
- const payloadActiveKeyId = getNonEmptyString(payload.activeKeyId);
40
- const rawKeys = payload.keys && typeof payload.keys === 'object'
41
- ? payload.keys as Record<string, unknown>
42
- : parsedRegistry as Record<string, unknown>;
43
-
44
- for (const [keyId, pemValue] of Object.entries(rawKeys)) {
45
- if (keyId === 'activeKeyId' || keyId === 'keys') {
46
- continue;
47
- }
48
-
49
- const normalizedKeyId = getNonEmptyString(keyId);
50
- const normalizedPem = getNonEmptyString(pemValue);
51
-
52
- if (!normalizedKeyId || !normalizedPem) {
53
- continue;
54
- }
55
-
56
- keys[normalizedKeyId] = normalizePrivateKeyPem(normalizedPem);
57
- }
58
-
59
- const resolvedActiveKeyId = configuredActiveKeyId ?? payloadActiveKeyId;
60
-
61
- if (Object.keys(keys).length === 0) {
62
- throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON does not contain any usable keys');
63
- }
64
-
65
- if (resolvedActiveKeyId && !keys[resolvedActiveKeyId]) {
66
- throw new Error('DATA_AT_REST_ENCRYPTION active key ID is not present in registry');
67
- }
68
-
69
- return {
70
- activeKeyId: resolvedActiveKeyId ?? null,
71
- keys
72
- };
73
- }
74
-
75
- const legacyKeyId = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_KEY_ID);
76
- const legacyPrivateKey = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY);
77
-
78
- if (!legacyKeyId || !legacyPrivateKey) {
79
- throw new Error('Data-at-rest decryption key registry is not configured');
80
- }
81
-
82
- keys[legacyKeyId] = normalizePrivateKeyPem(legacyPrivateKey);
83
-
84
- return {
85
- activeKeyId: configuredActiveKeyId ?? legacyKeyId,
86
- keys
87
- };
16
+ async function getDataAtRestPrivateKeyRegistry(env: Env): Promise<PrivateKeyRegistry> {
17
+ return fetchKeyRegistryFromR2(
18
+ env.STRIAE_CONFIG,
19
+ 'data-at-rest',
20
+ env.DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID,
21
+ env.REGISTRY_ENCRYPTION_KEY
22
+ );
88
23
  }
89
24
 
90
25
  function buildPrivateKeyCandidates(
@@ -149,7 +84,7 @@ async function decryptCaseDataWithRegistry(
149
84
  envelope: DataAtRestEnvelope,
150
85
  env: Env
151
86
  ): Promise<string> {
152
- const keyRegistry = parseDataAtRestPrivateKeyRegistry(env);
87
+ const keyRegistry = await getDataAtRestPrivateKeyRegistry(env);
153
88
  const candidates = buildPrivateKeyCandidates(getNonEmptyString(envelope.keyId), keyRegistry);
154
89
  let lastError: unknown;
155
90
 
@@ -2,78 +2,21 @@ import { decryptJsonFromUserKv, type UserKvEncryptedRecord } from '../encryption
2
2
  import type {
3
3
  DecryptionTelemetryOutcome,
4
4
  Env,
5
- KeyRegistryPayload,
6
5
  PrivateKeyRegistry
7
6
  } from '../types';
8
-
9
- function normalizePrivateKeyPem(rawValue: string): string {
10
- return rawValue.trim().replace(/^['"]|['"]$/g, '').replace(/\\n/g, '\n');
11
- }
7
+ import { fetchKeyRegistryFromR2 } from '../../../../shared/registry/r2-key-registry';
12
8
 
13
9
  function getNonEmptyString(value: unknown): string | null {
14
10
  return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
15
11
  }
16
12
 
17
- export function parseUserKvPrivateKeyRegistry(env: Env): PrivateKeyRegistry {
18
- const keys: Record<string, string> = {};
19
- const configuredActiveKeyId = getNonEmptyString(env.USER_KV_ENCRYPTION_ACTIVE_KEY_ID);
20
-
21
- if (getNonEmptyString(env.USER_KV_ENCRYPTION_KEYS_JSON)) {
22
- let parsedRegistry: unknown;
23
- try {
24
- parsedRegistry = JSON.parse(env.USER_KV_ENCRYPTION_KEYS_JSON as string) as unknown;
25
- } catch {
26
- throw new Error('USER_KV_ENCRYPTION_KEYS_JSON is not valid JSON');
27
- }
28
-
29
- if (!parsedRegistry || typeof parsedRegistry !== 'object') {
30
- throw new Error('USER_KV_ENCRYPTION_KEYS_JSON must be an object');
31
- }
32
-
33
- const payload = parsedRegistry as KeyRegistryPayload;
34
- if (!payload.keys || typeof payload.keys !== 'object') {
35
- throw new Error('USER_KV_ENCRYPTION_KEYS_JSON must include a keys object');
36
- }
37
-
38
- for (const [keyId, pemValue] of Object.entries(payload.keys as Record<string, unknown>)) {
39
- const normalizedKeyId = getNonEmptyString(keyId);
40
- const normalizedPem = getNonEmptyString(pemValue);
41
- if (!normalizedKeyId || !normalizedPem) {
42
- continue;
43
- }
44
-
45
- keys[normalizedKeyId] = normalizePrivateKeyPem(normalizedPem);
46
- }
47
-
48
- const payloadActiveKeyId = getNonEmptyString(payload.activeKeyId);
49
- const activeKeyId = configuredActiveKeyId ?? payloadActiveKeyId;
50
-
51
- if (Object.keys(keys).length === 0) {
52
- throw new Error('USER_KV_ENCRYPTION_KEYS_JSON does not contain any usable keys');
53
- }
54
-
55
- if (activeKeyId && !keys[activeKeyId]) {
56
- throw new Error('USER_KV active key ID is not present in USER_KV_ENCRYPTION_KEYS_JSON');
57
- }
58
-
59
- return {
60
- activeKeyId: activeKeyId ?? null,
61
- keys
62
- };
63
- }
64
-
65
- const legacyKeyId = getNonEmptyString(env.USER_KV_ENCRYPTION_KEY_ID);
66
- const legacyPrivateKey = getNonEmptyString(env.USER_KV_ENCRYPTION_PRIVATE_KEY);
67
- if (!legacyKeyId || !legacyPrivateKey) {
68
- throw new Error('User KV encryption private key registry is not configured');
69
- }
70
-
71
- keys[legacyKeyId] = normalizePrivateKeyPem(legacyPrivateKey);
72
-
73
- return {
74
- activeKeyId: configuredActiveKeyId ?? legacyKeyId,
75
- keys
76
- };
13
+ export async function parseUserKvPrivateKeyRegistry(env: Env): Promise<PrivateKeyRegistry> {
14
+ return fetchKeyRegistryFromR2(
15
+ env.STRIAE_CONFIG,
16
+ 'user-kv',
17
+ env.USER_KV_ENCRYPTION_ACTIVE_KEY_ID,
18
+ env.REGISTRY_ENCRYPTION_KEY
19
+ );
77
20
  }
78
21
 
79
22
  function buildPrivateKeyCandidates(
@@ -18,7 +18,7 @@ export async function readUserRecord(env: Env, userUid: string): Promise<UserDat
18
18
  }
19
19
 
20
20
  validateEncryptedRecord(encryptedRecord);
21
- const keyRegistry = parseUserKvPrivateKeyRegistry(env);
21
+ const keyRegistry = await parseUserKvPrivateKeyRegistry(env);
22
22
  const decryptedJson = await decryptUserKvRecord(encryptedRecord, keyRegistry);
23
23
  return JSON.parse(decryptedJson) as UserData;
24
24
  }
@@ -2,6 +2,8 @@ export interface Env {
2
2
  USER_DB: KVNamespace;
3
3
  STRIAE_DATA: R2Bucket;
4
4
  STRIAE_FILES: R2Bucket;
5
+ STRIAE_CONFIG: R2Bucket;
6
+ REGISTRY_ENCRYPTION_KEY: string;
5
7
  DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
6
8
  DATA_AT_REST_ENCRYPTION_KEY_ID?: string;
7
9
  DATA_AT_REST_ENCRYPTION_KEYS_JSON?: string;
@@ -3,7 +3,7 @@
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/user-worker.ts",
5
5
  "workers_dev": false,
6
- "compatibility_date": "2026-05-07",
6
+ "compatibility_date": "2026-06-07",
7
7
  "compatibility_flags": [
8
8
  "nodejs_compat"
9
9
  ],
@@ -27,6 +27,10 @@
27
27
  {
28
28
  "binding": "STRIAE_FILES",
29
29
  "bucket_name": "FILES_BUCKET_NAME"
30
+ },
31
+ {
32
+ "binding": "STRIAE_CONFIG",
33
+ "bucket_name": "CONFIG_BUCKET_NAME"
30
34
  }
31
35
  ],
32
36
 
@@ -1,6 +1,6 @@
1
1
  #:schema node_modules/wrangler/config-schema.json
2
2
  name = "PAGES_PROJECT_NAME"
3
- compatibility_date = "2026-05-07"
3
+ compatibility_date = "2026-06-07"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6