@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
|
@@ -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-
|
|
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": "
|
|
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.
|
|
11
|
+
"wrangler": "^4.98.0"
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pdf-worker",
|
|
3
|
-
"version": "
|
|
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.
|
|
12
|
+
"wrangler": "^4.98.0"
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "user-worker",
|
|
3
|
-
"version": "
|
|
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.
|
|
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
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 =
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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-
|
|
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
|
|
package/wrangler.toml.example
CHANGED