@striae-org/striae 5.2.1 → 5.3.1

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 (117) hide show
  1. package/.env.example +2 -10
  2. package/README.md +5 -46
  3. package/app/components/actions/case-export/core-export.ts +5 -174
  4. package/app/components/actions/case-export/download-handlers.ts +84 -751
  5. package/app/components/actions/case-export/index.ts +6 -30
  6. package/app/components/actions/case-export/metadata-helpers.ts +0 -78
  7. package/app/components/actions/case-export/types-constants.ts +0 -43
  8. package/app/components/actions/case-import/confirmation-import.ts +75 -36
  9. package/app/components/actions/case-import/confirmation-package.ts +68 -1
  10. package/app/components/actions/case-import/index.ts +1 -1
  11. package/app/components/actions/case-import/orchestrator.ts +78 -53
  12. package/app/components/actions/case-import/zip-processing.ts +160 -330
  13. package/app/components/actions/generate-pdf.ts +3 -2
  14. package/app/components/audit/user-audit-viewer.tsx +0 -19
  15. package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
  16. package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
  17. package/app/components/navbar/case-modals/export-case-modal.module.css +27 -0
  18. package/app/components/navbar/case-modals/export-case-modal.tsx +132 -0
  19. package/app/components/navbar/case-modals/export-confirmations-modal.module.css +24 -0
  20. package/app/components/navbar/case-modals/export-confirmations-modal.tsx +108 -0
  21. package/app/components/navbar/navbar.tsx +1 -1
  22. package/app/components/sidebar/case-import/case-import.module.css +35 -0
  23. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +51 -3
  24. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
  25. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +36 -5
  26. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +5 -9
  27. package/app/components/sidebar/case-import/index.ts +1 -4
  28. package/app/components/sidebar/notes/class-details-shared.ts +2 -2
  29. package/app/components/toast/toast.module.css +36 -0
  30. package/app/components/toast/toast.tsx +6 -2
  31. package/app/components/user/manage-profile.tsx +4 -3
  32. package/app/config-example/config.json +1 -2
  33. package/app/root.tsx +0 -7
  34. package/app/routes/_index.tsx +1 -1
  35. package/app/routes/auth/login.example.tsx +22 -103
  36. package/app/routes/auth/login.tsx +22 -103
  37. package/app/routes/auth/route.ts +1 -1
  38. package/app/routes/striae/striae.tsx +117 -59
  39. package/app/services/firebase/index.ts +0 -3
  40. package/app/types/case.ts +1 -0
  41. package/app/types/export.ts +2 -2
  42. package/app/types/import.ts +10 -0
  43. package/app/utils/auth/index.ts +0 -1
  44. package/app/utils/data/permissions.ts +3 -2
  45. package/package.json +9 -16
  46. package/public/_headers +0 -4
  47. package/public/_routes.json +0 -1
  48. package/worker-configuration.d.ts +20 -17
  49. package/workers/audit-worker/src/audit-worker.example.ts +9 -806
  50. package/workers/audit-worker/src/config.ts +7 -0
  51. package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
  52. package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
  53. package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
  54. package/workers/audit-worker/src/types.ts +56 -0
  55. package/workers/audit-worker/worker-configuration.d.ts +1 -1
  56. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  57. package/workers/data-worker/src/config.ts +11 -0
  58. package/workers/data-worker/src/data-worker.example.ts +21 -942
  59. package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
  60. package/workers/data-worker/src/handlers/signing.ts +174 -0
  61. package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
  62. package/workers/data-worker/src/registry/key-registry.ts +368 -0
  63. package/workers/data-worker/src/types.ts +46 -0
  64. package/workers/data-worker/worker-configuration.d.ts +1 -1
  65. package/workers/data-worker/wrangler.jsonc.example +1 -1
  66. package/workers/image-worker/worker-configuration.d.ts +1 -1
  67. package/workers/image-worker/wrangler.jsonc.example +1 -1
  68. package/workers/pdf-worker/worker-configuration.d.ts +2 -3
  69. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  70. package/workers/user-worker/src/auth.ts +30 -0
  71. package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
  72. package/workers/user-worker/src/config.ts +4 -0
  73. package/workers/user-worker/src/encryption-utils.ts +25 -0
  74. package/workers/user-worker/src/firebase/admin.ts +152 -0
  75. package/workers/user-worker/src/handlers/user-routes.ts +242 -0
  76. package/workers/user-worker/src/registry/user-kv.ts +172 -0
  77. package/workers/user-worker/src/storage/user-records.ts +34 -0
  78. package/workers/user-worker/src/types.ts +106 -0
  79. package/workers/user-worker/src/user-worker.example.ts +18 -964
  80. package/workers/user-worker/worker-configuration.d.ts +4 -2
  81. package/workers/user-worker/wrangler.jsonc.example +12 -1
  82. package/wrangler.toml.example +1 -1
  83. package/app/components/actions/case-export/data-processing.ts +0 -223
  84. package/app/components/sidebar/case-export/case-export.module.css +0 -418
  85. package/app/components/sidebar/case-export/case-export.tsx +0 -310
  86. package/app/types/exceljs-bare.d.ts +0 -9
  87. package/app/utils/auth/auth.ts +0 -11
  88. package/public/.well-known/security.txt +0 -6
  89. package/public/favicon.ico +0 -0
  90. package/public/icon-256.png +0 -0
  91. package/public/icon-512.png +0 -0
  92. package/public/manifest.json +0 -39
  93. package/public/shortcut.png +0 -0
  94. package/public/social-image.png +0 -0
  95. package/public/vendor/exceljs.LICENSE +0 -22
  96. package/public/vendor/exceljs.bare.min.js +0 -45
  97. package/scripts/deploy-all.sh +0 -166
  98. package/scripts/deploy-config/modules/env-utils.sh +0 -322
  99. package/scripts/deploy-config/modules/keys.sh +0 -404
  100. package/scripts/deploy-config/modules/prompt.sh +0 -372
  101. package/scripts/deploy-config/modules/scaffolding.sh +0 -344
  102. package/scripts/deploy-config/modules/validation.sh +0 -365
  103. package/scripts/deploy-config.sh +0 -236
  104. package/scripts/deploy-pages-secrets.sh +0 -231
  105. package/scripts/deploy-pages.sh +0 -34
  106. package/scripts/deploy-primershear-emails.sh +0 -167
  107. package/scripts/deploy-worker-secrets.sh +0 -374
  108. package/scripts/dev.cjs +0 -23
  109. package/scripts/install-workers.sh +0 -88
  110. package/scripts/run-eslint.cjs +0 -43
  111. package/scripts/update-compatibility-dates.cjs +0 -124
  112. package/scripts/update-markdown-versions.cjs +0 -43
  113. package/workers/keys-worker/package.json +0 -18
  114. package/workers/keys-worker/src/keys.example.ts +0 -67
  115. package/workers/keys-worker/src/keys.ts +0 -67
  116. package/workers/keys-worker/worker-configuration.d.ts +0 -7447
  117. 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-23 nodejs_compat
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;
@@ -5,7 +5,7 @@
5
5
  "name": "DATA_WORKER_NAME",
6
6
  "account_id": "ACCOUNT_ID",
7
7
  "main": "src/data-worker.ts",
8
- "compatibility_date": "2026-03-25",
8
+ "compatibility_date": "2026-03-29",
9
9
  "compatibility_flags": [
10
10
  "nodejs_compat"
11
11
  ],
@@ -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-24 nodejs_compat
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;
@@ -2,7 +2,7 @@
2
2
  "name": "IMAGES_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/image-worker.ts",
5
- "compatibility_date": "2026-03-25",
5
+ "compatibility_date": "2026-03-29",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,9 +1,8 @@
1
1
  /* eslint-disable */
2
- // Generated by Wrangler by running `wrangler types` (hash: 2dee743b5210da80c0776559961add52)
3
- // Runtime types generated with workerd@1.20250823.0 2026-03-20 nodejs_compat
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 {}
@@ -2,7 +2,7 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-03-25",
5
+ "compatibility_date": "2026-03-29",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -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
+ }