@striae-org/striae 5.2.1 → 5.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/.env.example +2 -10
- package/README.md +5 -46
- package/app/components/actions/case-export/core-export.ts +2 -174
- package/app/components/actions/case-export/download-handlers.ts +83 -750
- package/app/components/actions/case-export/index.ts +6 -30
- package/app/components/actions/case-export/metadata-helpers.ts +0 -78
- package/app/components/actions/case-export/types-constants.ts +0 -43
- package/app/components/actions/case-import/confirmation-import.ts +13 -14
- package/app/components/actions/case-import/zip-processing.ts +92 -12
- package/app/components/actions/generate-pdf.ts +3 -2
- package/app/components/audit/user-audit-viewer.tsx +0 -19
- package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
- package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
- package/app/components/navbar/navbar.tsx +1 -1
- package/app/components/sidebar/case-import/case-import.module.css +35 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +59 -3
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +1 -1
- package/app/components/sidebar/notes/class-details-shared.ts +2 -2
- package/app/components/toast/toast.module.css +36 -0
- package/app/components/toast/toast.tsx +6 -2
- package/app/components/user/manage-profile.tsx +4 -3
- package/app/config-example/config.json +1 -2
- package/app/root.tsx +0 -7
- package/app/routes/_index.tsx +1 -1
- package/app/routes/auth/login.example.tsx +22 -103
- package/app/routes/auth/route.ts +1 -1
- package/app/routes/striae/striae.tsx +53 -59
- package/app/services/firebase/index.ts +0 -3
- package/app/types/export.ts +1 -2
- package/app/utils/auth/index.ts +0 -1
- package/app/utils/data/permissions.ts +3 -2
- package/package.json +9 -16
- package/public/_headers +0 -4
- package/public/_routes.json +0 -1
- package/worker-configuration.d.ts +20 -17
- package/workers/audit-worker/src/audit-worker.example.ts +9 -806
- package/workers/audit-worker/src/config.ts +7 -0
- package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
- package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
- package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
- package/workers/audit-worker/src/types.ts +56 -0
- package/workers/audit-worker/worker-configuration.d.ts +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/config.ts +11 -0
- package/workers/data-worker/src/data-worker.example.ts +21 -942
- package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
- package/workers/data-worker/src/handlers/signing.ts +174 -0
- package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
- package/workers/data-worker/src/registry/key-registry.ts +368 -0
- package/workers/data-worker/src/types.ts +46 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/worker-configuration.d.ts +2 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/auth.ts +30 -0
- package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
- package/workers/user-worker/src/config.ts +4 -0
- package/workers/user-worker/src/encryption-utils.ts +25 -0
- package/workers/user-worker/src/firebase/admin.ts +152 -0
- package/workers/user-worker/src/handlers/user-routes.ts +242 -0
- package/workers/user-worker/src/registry/user-kv.ts +172 -0
- package/workers/user-worker/src/storage/user-records.ts +34 -0
- package/workers/user-worker/src/types.ts +106 -0
- package/workers/user-worker/src/user-worker.example.ts +18 -964
- package/workers/user-worker/worker-configuration.d.ts +4 -2
- package/workers/user-worker/wrangler.jsonc.example +12 -1
- package/wrangler.toml.example +1 -1
- package/app/components/actions/case-export/data-processing.ts +0 -223
- package/app/components/sidebar/case-export/case-export.module.css +0 -418
- package/app/components/sidebar/case-export/case-export.tsx +0 -310
- package/app/types/exceljs-bare.d.ts +0 -9
- package/app/utils/auth/auth.ts +0 -11
- package/public/.well-known/security.txt +0 -6
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +0 -39
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/vendor/exceljs.LICENSE +0 -22
- package/public/vendor/exceljs.bare.min.js +0 -45
- package/scripts/deploy-all.sh +0 -166
- package/scripts/deploy-config/modules/env-utils.sh +0 -322
- package/scripts/deploy-config/modules/keys.sh +0 -404
- package/scripts/deploy-config/modules/prompt.sh +0 -372
- package/scripts/deploy-config/modules/scaffolding.sh +0 -344
- package/scripts/deploy-config/modules/validation.sh +0 -365
- package/scripts/deploy-config.sh +0 -236
- package/scripts/deploy-pages-secrets.sh +0 -231
- package/scripts/deploy-pages.sh +0 -34
- package/scripts/deploy-primershear-emails.sh +0 -167
- package/scripts/deploy-worker-secrets.sh +0 -374
- package/scripts/dev.cjs +0 -23
- package/scripts/install-workers.sh +0 -88
- package/scripts/run-eslint.cjs +0 -43
- package/scripts/update-compatibility-dates.cjs +0 -124
- package/scripts/update-markdown-versions.cjs +0 -43
- package/workers/keys-worker/package.json +0 -18
- package/workers/keys-worker/src/keys.example.ts +0 -67
- package/workers/keys-worker/src/keys.ts +0 -67
- package/workers/keys-worker/worker-configuration.d.ts +0 -7447
- package/workers/keys-worker/wrangler.jsonc.example +0 -15
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decryptExportData,
|
|
3
|
+
decryptImageBlob,
|
|
4
|
+
decryptJsonFromStorage,
|
|
5
|
+
type DataAtRestEnvelope
|
|
6
|
+
} from '../encryption-utils';
|
|
7
|
+
import type {
|
|
8
|
+
DecryptionTelemetryOutcome,
|
|
9
|
+
Env,
|
|
10
|
+
ExportDecryptionContext,
|
|
11
|
+
KeyRegistryPayload,
|
|
12
|
+
PrivateKeyRegistry
|
|
13
|
+
} from '../types';
|
|
14
|
+
|
|
15
|
+
function normalizePrivateKeyPem(rawValue: string): string {
|
|
16
|
+
return rawValue.trim().replace(/^['"]|['"]$/g, '').replace(/\\n/g, '\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getNonEmptyString(value: unknown): string | null {
|
|
20
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parsePrivateKeyRegistry(input: {
|
|
24
|
+
registryJson: string | undefined;
|
|
25
|
+
activeKeyId: string | undefined;
|
|
26
|
+
legacyKeyId: string | undefined;
|
|
27
|
+
legacyPrivateKey: string | undefined;
|
|
28
|
+
context: string;
|
|
29
|
+
}): PrivateKeyRegistry {
|
|
30
|
+
const keys: Record<string, string> = {};
|
|
31
|
+
const configuredActiveKeyId = getNonEmptyString(input.activeKeyId);
|
|
32
|
+
const registryJson = getNonEmptyString(input.registryJson);
|
|
33
|
+
|
|
34
|
+
if (registryJson) {
|
|
35
|
+
let parsedRegistry: unknown;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
parsedRegistry = JSON.parse(registryJson) as unknown;
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`${input.context} registry JSON is invalid`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!parsedRegistry || typeof parsedRegistry !== 'object') {
|
|
44
|
+
throw new Error(`${input.context} registry JSON must be an object`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const payload = parsedRegistry as KeyRegistryPayload;
|
|
48
|
+
const payloadActiveKeyId = getNonEmptyString(payload.activeKeyId);
|
|
49
|
+
const rawKeys = payload.keys && typeof payload.keys === 'object'
|
|
50
|
+
? payload.keys as Record<string, unknown>
|
|
51
|
+
: parsedRegistry as Record<string, unknown>;
|
|
52
|
+
|
|
53
|
+
for (const [keyId, pemValue] of Object.entries(rawKeys)) {
|
|
54
|
+
if (keyId === 'activeKeyId' || keyId === 'keys') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const normalizedKeyId = getNonEmptyString(keyId);
|
|
59
|
+
const normalizedPem = getNonEmptyString(pemValue);
|
|
60
|
+
|
|
61
|
+
if (!normalizedKeyId || !normalizedPem) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
keys[normalizedKeyId] = normalizePrivateKeyPem(normalizedPem);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolvedActiveKeyId = configuredActiveKeyId ?? payloadActiveKeyId;
|
|
69
|
+
|
|
70
|
+
if (Object.keys(keys).length === 0) {
|
|
71
|
+
throw new Error(`${input.context} registry does not contain any usable keys`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (resolvedActiveKeyId && !keys[resolvedActiveKeyId]) {
|
|
75
|
+
throw new Error(`${input.context} active key ID is not present in registry`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
activeKeyId: resolvedActiveKeyId ?? null,
|
|
80
|
+
keys
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const legacyKeyId = getNonEmptyString(input.legacyKeyId);
|
|
85
|
+
const legacyPrivateKey = getNonEmptyString(input.legacyPrivateKey);
|
|
86
|
+
|
|
87
|
+
if (!legacyKeyId || !legacyPrivateKey) {
|
|
88
|
+
throw new Error(`${input.context} private key registry is not configured`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
keys[legacyKeyId] = normalizePrivateKeyPem(legacyPrivateKey);
|
|
92
|
+
const resolvedActiveKeyId = configuredActiveKeyId ?? legacyKeyId;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
activeKeyId: resolvedActiveKeyId,
|
|
96
|
+
keys
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildPrivateKeyCandidates(
|
|
101
|
+
recordKeyId: string | null,
|
|
102
|
+
registry: PrivateKeyRegistry
|
|
103
|
+
): Array<{ keyId: string; privateKeyPem: string }> {
|
|
104
|
+
const candidates: Array<{ keyId: string; privateKeyPem: string }> = [];
|
|
105
|
+
const seen = new Set<string>();
|
|
106
|
+
|
|
107
|
+
const appendCandidate = (candidateKeyId: string | null): void => {
|
|
108
|
+
if (!candidateKeyId || seen.has(candidateKeyId)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const privateKeyPem = registry.keys[candidateKeyId];
|
|
113
|
+
if (!privateKeyPem) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
seen.add(candidateKeyId);
|
|
118
|
+
candidates.push({ keyId: candidateKeyId, privateKeyPem });
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
appendCandidate(recordKeyId);
|
|
122
|
+
appendCandidate(registry.activeKeyId);
|
|
123
|
+
|
|
124
|
+
for (const keyId of Object.keys(registry.keys)) {
|
|
125
|
+
appendCandidate(keyId);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return candidates;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function logRegistryDecryptionTelemetry(input: {
|
|
132
|
+
scope: 'data-at-rest' | 'export-data' | 'export-image';
|
|
133
|
+
recordKeyId: string | null;
|
|
134
|
+
selectedKeyId: string | null;
|
|
135
|
+
attemptCount: number;
|
|
136
|
+
outcome: DecryptionTelemetryOutcome;
|
|
137
|
+
reason?: string;
|
|
138
|
+
}): void {
|
|
139
|
+
const details = {
|
|
140
|
+
scope: input.scope,
|
|
141
|
+
recordKeyId: input.recordKeyId,
|
|
142
|
+
selectedKeyId: input.selectedKeyId,
|
|
143
|
+
attemptCount: input.attemptCount,
|
|
144
|
+
fallbackUsed: input.outcome === 'fallback-hit',
|
|
145
|
+
outcome: input.outcome,
|
|
146
|
+
reason: input.reason ?? null
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (input.outcome === 'all-failed') {
|
|
150
|
+
console.warn('Key registry decryption failed', details);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.info('Key registry decryption resolved', details);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getDataAtRestPrivateKeyRegistry(env: Env): PrivateKeyRegistry {
|
|
158
|
+
return parsePrivateKeyRegistry({
|
|
159
|
+
registryJson: env.DATA_AT_REST_ENCRYPTION_KEYS_JSON,
|
|
160
|
+
activeKeyId: env.DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID,
|
|
161
|
+
legacyKeyId: env.DATA_AT_REST_ENCRYPTION_KEY_ID,
|
|
162
|
+
legacyPrivateKey: env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY,
|
|
163
|
+
context: 'Data-at-rest decryption'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getExportPrivateKeyRegistry(env: Env): PrivateKeyRegistry {
|
|
168
|
+
return parsePrivateKeyRegistry({
|
|
169
|
+
registryJson: env.EXPORT_ENCRYPTION_KEYS_JSON,
|
|
170
|
+
activeKeyId: env.EXPORT_ENCRYPTION_ACTIVE_KEY_ID,
|
|
171
|
+
legacyKeyId: env.EXPORT_ENCRYPTION_KEY_ID,
|
|
172
|
+
legacyPrivateKey: env.EXPORT_ENCRYPTION_PRIVATE_KEY,
|
|
173
|
+
context: 'Export decryption'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function buildExportDecryptionContext(keyId: string | null, env: Env): ExportDecryptionContext {
|
|
178
|
+
const keyRegistry = getExportPrivateKeyRegistry(env);
|
|
179
|
+
const candidates = buildPrivateKeyCandidates(keyId, keyRegistry);
|
|
180
|
+
|
|
181
|
+
if (candidates.length === 0) {
|
|
182
|
+
throw new Error('Export decryption key registry does not contain any usable keys');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
recordKeyId: keyId,
|
|
187
|
+
candidates,
|
|
188
|
+
primaryKeyId: candidates[0]?.keyId ?? null
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function decryptJsonFromStorageWithRegistry(
|
|
193
|
+
ciphertext: ArrayBuffer,
|
|
194
|
+
envelope: DataAtRestEnvelope,
|
|
195
|
+
env: Env
|
|
196
|
+
): Promise<string> {
|
|
197
|
+
const keyRegistry = getDataAtRestPrivateKeyRegistry(env);
|
|
198
|
+
const candidates = buildPrivateKeyCandidates(getNonEmptyString(envelope.keyId), keyRegistry);
|
|
199
|
+
const primaryKeyId = candidates[0]?.keyId ?? null;
|
|
200
|
+
let lastError: unknown;
|
|
201
|
+
|
|
202
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
203
|
+
const candidate = candidates[index];
|
|
204
|
+
try {
|
|
205
|
+
const plaintext = await decryptJsonFromStorage(ciphertext, envelope, candidate.privateKeyPem);
|
|
206
|
+
logRegistryDecryptionTelemetry({
|
|
207
|
+
scope: 'data-at-rest',
|
|
208
|
+
recordKeyId: getNonEmptyString(envelope.keyId),
|
|
209
|
+
selectedKeyId: candidate.keyId,
|
|
210
|
+
attemptCount: index + 1,
|
|
211
|
+
outcome: candidate.keyId === primaryKeyId ? 'primary-hit' : 'fallback-hit'
|
|
212
|
+
});
|
|
213
|
+
return plaintext;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
lastError = error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
logRegistryDecryptionTelemetry({
|
|
220
|
+
scope: 'data-at-rest',
|
|
221
|
+
recordKeyId: getNonEmptyString(envelope.keyId),
|
|
222
|
+
selectedKeyId: null,
|
|
223
|
+
attemptCount: candidates.length,
|
|
224
|
+
outcome: 'all-failed',
|
|
225
|
+
reason: lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Failed to decrypt stored data after ${candidates.length} key attempt(s): ${
|
|
230
|
+
lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
231
|
+
}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function decryptExportDataWithRegistry(
|
|
236
|
+
encryptedDataBase64: string,
|
|
237
|
+
wrappedKeyBase64: string,
|
|
238
|
+
ivBase64: string,
|
|
239
|
+
context: ExportDecryptionContext
|
|
240
|
+
): Promise<string> {
|
|
241
|
+
let lastError: unknown;
|
|
242
|
+
|
|
243
|
+
for (let index = 0; index < context.candidates.length; index += 1) {
|
|
244
|
+
const candidate = context.candidates[index];
|
|
245
|
+
try {
|
|
246
|
+
const plaintext = await decryptExportData(
|
|
247
|
+
encryptedDataBase64,
|
|
248
|
+
wrappedKeyBase64,
|
|
249
|
+
ivBase64,
|
|
250
|
+
candidate.privateKeyPem
|
|
251
|
+
);
|
|
252
|
+
logRegistryDecryptionTelemetry({
|
|
253
|
+
scope: 'export-data',
|
|
254
|
+
recordKeyId: context.recordKeyId,
|
|
255
|
+
selectedKeyId: candidate.keyId,
|
|
256
|
+
attemptCount: index + 1,
|
|
257
|
+
outcome: candidate.keyId === context.primaryKeyId ? 'primary-hit' : 'fallback-hit'
|
|
258
|
+
});
|
|
259
|
+
return plaintext;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
lastError = error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
logRegistryDecryptionTelemetry({
|
|
266
|
+
scope: 'export-data',
|
|
267
|
+
recordKeyId: context.recordKeyId,
|
|
268
|
+
selectedKeyId: null,
|
|
269
|
+
attemptCount: context.candidates.length,
|
|
270
|
+
outcome: 'all-failed',
|
|
271
|
+
reason: lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Failed to decrypt export payload after ${context.candidates.length} key attempt(s): ${
|
|
276
|
+
lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
277
|
+
}`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function decryptExportImageWithRegistry(
|
|
282
|
+
encryptedImageBase64: string,
|
|
283
|
+
wrappedKeyBase64: string,
|
|
284
|
+
ivBase64: string,
|
|
285
|
+
context: ExportDecryptionContext
|
|
286
|
+
): Promise<Blob> {
|
|
287
|
+
let lastError: unknown;
|
|
288
|
+
|
|
289
|
+
for (let index = 0; index < context.candidates.length; index += 1) {
|
|
290
|
+
const candidate = context.candidates[index];
|
|
291
|
+
try {
|
|
292
|
+
const imageBlob = await decryptImageBlob(
|
|
293
|
+
encryptedImageBase64,
|
|
294
|
+
wrappedKeyBase64,
|
|
295
|
+
ivBase64,
|
|
296
|
+
candidate.privateKeyPem
|
|
297
|
+
);
|
|
298
|
+
logRegistryDecryptionTelemetry({
|
|
299
|
+
scope: 'export-image',
|
|
300
|
+
recordKeyId: context.recordKeyId,
|
|
301
|
+
selectedKeyId: candidate.keyId,
|
|
302
|
+
attemptCount: index + 1,
|
|
303
|
+
outcome: candidate.keyId === context.primaryKeyId ? 'primary-hit' : 'fallback-hit'
|
|
304
|
+
});
|
|
305
|
+
return imageBlob;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
lastError = error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
logRegistryDecryptionTelemetry({
|
|
312
|
+
scope: 'export-image',
|
|
313
|
+
recordKeyId: context.recordKeyId,
|
|
314
|
+
selectedKeyId: null,
|
|
315
|
+
attemptCount: context.candidates.length,
|
|
316
|
+
outcome: 'all-failed',
|
|
317
|
+
reason: lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Failed to decrypt export image after ${context.candidates.length} key attempt(s): ${
|
|
322
|
+
lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
323
|
+
}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function isDataAtRestEncryptionEnabled(env: Env): boolean {
|
|
328
|
+
const value = env.DATA_AT_REST_ENCRYPTION_ENABLED;
|
|
329
|
+
if (!value) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
334
|
+
return normalizedValue === '1' || normalizedValue === 'true' || normalizedValue === 'yes' || normalizedValue === 'on';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function extractDataAtRestEnvelope(file: R2ObjectBody): DataAtRestEnvelope | null {
|
|
338
|
+
const metadata = file.customMetadata;
|
|
339
|
+
if (!metadata) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const {
|
|
344
|
+
algorithm,
|
|
345
|
+
encryptionVersion,
|
|
346
|
+
keyId,
|
|
347
|
+
dataIv,
|
|
348
|
+
wrappedKey
|
|
349
|
+
} = metadata;
|
|
350
|
+
|
|
351
|
+
if (
|
|
352
|
+
typeof algorithm !== 'string' ||
|
|
353
|
+
typeof encryptionVersion !== 'string' ||
|
|
354
|
+
typeof keyId !== 'string' ||
|
|
355
|
+
typeof dataIv !== 'string' ||
|
|
356
|
+
typeof wrappedKey !== 'string'
|
|
357
|
+
) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
algorithm,
|
|
363
|
+
encryptionVersion,
|
|
364
|
+
keyId,
|
|
365
|
+
dataIv,
|
|
366
|
+
wrappedKey
|
|
367
|
+
};
|
|
368
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface Env {
|
|
2
|
+
R2_KEY_SECRET: string;
|
|
3
|
+
STRIAE_DATA: R2Bucket;
|
|
4
|
+
MANIFEST_SIGNING_PRIVATE_KEY: string;
|
|
5
|
+
MANIFEST_SIGNING_KEY_ID: string;
|
|
6
|
+
EXPORT_ENCRYPTION_PRIVATE_KEY?: string;
|
|
7
|
+
EXPORT_ENCRYPTION_KEY_ID?: string;
|
|
8
|
+
EXPORT_ENCRYPTION_KEYS_JSON?: string;
|
|
9
|
+
EXPORT_ENCRYPTION_ACTIVE_KEY_ID?: string;
|
|
10
|
+
DATA_AT_REST_ENCRYPTION_ENABLED?: string;
|
|
11
|
+
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
|
|
12
|
+
DATA_AT_REST_ENCRYPTION_PUBLIC_KEY?: string;
|
|
13
|
+
DATA_AT_REST_ENCRYPTION_KEY_ID?: string;
|
|
14
|
+
DATA_AT_REST_ENCRYPTION_KEYS_JSON?: string;
|
|
15
|
+
DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface KeyRegistryPayload {
|
|
19
|
+
activeKeyId?: unknown;
|
|
20
|
+
keys?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PrivateKeyRegistry {
|
|
24
|
+
activeKeyId: string | null;
|
|
25
|
+
keys: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ExportDecryptionContext {
|
|
29
|
+
recordKeyId: string | null;
|
|
30
|
+
candidates: Array<{ keyId: string; privateKeyPem: string }>;
|
|
31
|
+
primaryKeyId: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DecryptionTelemetryOutcome = 'primary-hit' | 'fallback-hit' | 'all-failed';
|
|
35
|
+
|
|
36
|
+
export interface SuccessResponse {
|
|
37
|
+
success: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ErrorResponse {
|
|
41
|
+
error: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type APIResponse = SuccessResponse | ErrorResponse | unknown[] | Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
export type CreateResponse = (data: APIResponse, status?: number) => Response;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
// Generated by Wrangler by running `wrangler types` (hash: 4ccb8b314830f4c7bb743cb9b033a6cb)
|
|
3
|
-
// Runtime types generated with workerd@1.20250823.0 2026-03-
|
|
3
|
+
// Runtime types generated with workerd@1.20250823.0 2026-03-26 nodejs_compat
|
|
4
4
|
declare namespace Cloudflare {
|
|
5
5
|
interface Env {
|
|
6
6
|
STRIAE_DATA: R2Bucket;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
// Generated by Wrangler by running `wrangler types` (hash: 031f1b22e4c77b10fe83d4eace0f6b21)
|
|
3
|
-
// Runtime types generated with workerd@1.20250823.0 2026-03-
|
|
3
|
+
// Runtime types generated with workerd@1.20250823.0 2026-03-26 nodejs_compat
|
|
4
4
|
declare namespace Cloudflare {
|
|
5
5
|
interface Env {
|
|
6
6
|
STRIAE_FILES: R2Bucket;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
// Generated by Wrangler by running `wrangler types` (hash:
|
|
3
|
-
// Runtime types generated with workerd@1.20250823.0 2026-03-
|
|
2
|
+
// Generated by Wrangler by running `wrangler types` (hash: 869ac3b4ce0f52ba3b2e0bc70c49089e)
|
|
3
|
+
// Runtime types generated with workerd@1.20250823.0 2026-03-26 nodejs_compat
|
|
4
4
|
declare namespace Cloudflare {
|
|
5
5
|
interface Env {
|
|
6
|
-
BROWSER: Fetcher;
|
|
7
6
|
}
|
|
8
7
|
}
|
|
9
8
|
interface Env extends Cloudflare.Env {}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Env } from './types';
|
|
2
|
+
|
|
3
|
+
export async function authenticate(request: Request, env: Env): Promise<void> {
|
|
4
|
+
const authKey = request.headers.get('X-Custom-Auth-Key');
|
|
5
|
+
if (authKey !== env.USER_DB_AUTH) {
|
|
6
|
+
throw new Error('Unauthorized');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function requireUserKvReadConfig(env: Env): void {
|
|
11
|
+
const hasLegacyPrivateKey = typeof env.USER_KV_ENCRYPTION_PRIVATE_KEY === 'string' && env.USER_KV_ENCRYPTION_PRIVATE_KEY.trim().length > 0;
|
|
12
|
+
const hasRegistryPrivateKeys = typeof env.USER_KV_ENCRYPTION_KEYS_JSON === 'string' && env.USER_KV_ENCRYPTION_KEYS_JSON.trim().length > 0;
|
|
13
|
+
|
|
14
|
+
if (!hasLegacyPrivateKey && !hasRegistryPrivateKeys) {
|
|
15
|
+
throw new Error('User KV encryption is not fully configured');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function requireUserKvWriteConfig(env: Env): void {
|
|
20
|
+
const hasLegacyPrivateKey = typeof env.USER_KV_ENCRYPTION_PRIVATE_KEY === 'string' && env.USER_KV_ENCRYPTION_PRIVATE_KEY.trim().length > 0;
|
|
21
|
+
const hasRegistryPrivateKeys = typeof env.USER_KV_ENCRYPTION_KEYS_JSON === 'string' && env.USER_KV_ENCRYPTION_KEYS_JSON.trim().length > 0;
|
|
22
|
+
|
|
23
|
+
if (
|
|
24
|
+
!env.USER_KV_ENCRYPTION_PUBLIC_KEY ||
|
|
25
|
+
!env.USER_KV_ENCRYPTION_KEY_ID ||
|
|
26
|
+
(!hasLegacyPrivateKey && !hasRegistryPrivateKeys)
|
|
27
|
+
) {
|
|
28
|
+
throw new Error('User KV encryption is not fully configured');
|
|
29
|
+
}
|
|
30
|
+
}
|