@striae-org/striae 5.2.0 → 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.
Files changed (105) hide show
  1. package/.env.example +36 -33
  2. package/README.md +5 -46
  3. package/app/components/actions/case-export/core-export.ts +2 -174
  4. package/app/components/actions/case-export/download-handlers.ts +83 -750
  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 +13 -14
  9. package/app/components/actions/case-import/zip-processing.ts +92 -12
  10. package/app/components/actions/generate-pdf.ts +3 -2
  11. package/app/components/audit/user-audit-viewer.tsx +0 -19
  12. package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
  13. package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
  14. package/app/components/navbar/navbar.tsx +1 -1
  15. package/app/components/sidebar/case-import/case-import.module.css +35 -0
  16. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +59 -3
  17. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
  18. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +1 -1
  19. package/app/components/sidebar/notes/class-details-shared.ts +2 -2
  20. package/app/components/toast/toast.module.css +36 -0
  21. package/app/components/toast/toast.tsx +6 -2
  22. package/app/components/user/manage-profile.tsx +4 -3
  23. package/app/config-example/config.json +1 -2
  24. package/app/root.tsx +0 -7
  25. package/app/routes/_index.tsx +1 -1
  26. package/app/routes/auth/login.example.tsx +22 -103
  27. package/app/routes/auth/route.ts +1 -1
  28. package/app/routes/striae/striae.tsx +53 -59
  29. package/app/services/firebase/index.ts +0 -3
  30. package/app/types/export.ts +1 -2
  31. package/app/utils/auth/index.ts +0 -1
  32. package/app/utils/data/permissions.ts +3 -2
  33. package/package.json +10 -17
  34. package/public/_headers +0 -4
  35. package/public/_routes.json +0 -1
  36. package/worker-configuration.d.ts +20 -17
  37. package/workers/audit-worker/src/audit-worker.example.ts +9 -806
  38. package/workers/audit-worker/src/config.ts +7 -0
  39. package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
  40. package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
  41. package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
  42. package/workers/audit-worker/src/types.ts +56 -0
  43. package/workers/audit-worker/worker-configuration.d.ts +1 -1
  44. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  45. package/workers/data-worker/src/config.ts +11 -0
  46. package/workers/data-worker/src/data-worker.example.ts +21 -942
  47. package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
  48. package/workers/data-worker/src/handlers/signing.ts +174 -0
  49. package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
  50. package/workers/data-worker/src/registry/key-registry.ts +368 -0
  51. package/workers/data-worker/src/types.ts +46 -0
  52. package/workers/data-worker/worker-configuration.d.ts +1 -1
  53. package/workers/data-worker/wrangler.jsonc.example +1 -1
  54. package/workers/image-worker/worker-configuration.d.ts +1 -1
  55. package/workers/image-worker/wrangler.jsonc.example +1 -1
  56. package/workers/pdf-worker/worker-configuration.d.ts +2 -3
  57. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  58. package/workers/user-worker/src/auth.ts +30 -0
  59. package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
  60. package/workers/user-worker/src/config.ts +4 -0
  61. package/workers/user-worker/src/encryption-utils.ts +25 -0
  62. package/workers/user-worker/src/firebase/admin.ts +152 -0
  63. package/workers/user-worker/src/handlers/user-routes.ts +242 -0
  64. package/workers/user-worker/src/registry/user-kv.ts +172 -0
  65. package/workers/user-worker/src/storage/user-records.ts +34 -0
  66. package/workers/user-worker/src/types.ts +106 -0
  67. package/workers/user-worker/src/user-worker.example.ts +18 -964
  68. package/workers/user-worker/worker-configuration.d.ts +4 -2
  69. package/workers/user-worker/wrangler.jsonc.example +12 -1
  70. package/wrangler.toml.example +1 -1
  71. package/app/components/actions/case-export/data-processing.ts +0 -223
  72. package/app/components/sidebar/case-export/case-export.module.css +0 -418
  73. package/app/components/sidebar/case-export/case-export.tsx +0 -310
  74. package/app/types/exceljs-bare.d.ts +0 -9
  75. package/app/utils/auth/auth.ts +0 -11
  76. package/public/.well-known/security.txt +0 -6
  77. package/public/favicon.ico +0 -0
  78. package/public/icon-256.png +0 -0
  79. package/public/icon-512.png +0 -0
  80. package/public/manifest.json +0 -39
  81. package/public/shortcut.png +0 -0
  82. package/public/social-image.png +0 -0
  83. package/public/vendor/exceljs.LICENSE +0 -22
  84. package/public/vendor/exceljs.bare.min.js +0 -45
  85. package/scripts/deploy-all.sh +0 -166
  86. package/scripts/deploy-config/modules/env-utils.sh +0 -322
  87. package/scripts/deploy-config/modules/keys.sh +0 -404
  88. package/scripts/deploy-config/modules/prompt.sh +0 -372
  89. package/scripts/deploy-config/modules/scaffolding.sh +0 -336
  90. package/scripts/deploy-config/modules/validation.sh +0 -365
  91. package/scripts/deploy-config.sh +0 -236
  92. package/scripts/deploy-pages-secrets.sh +0 -231
  93. package/scripts/deploy-pages.sh +0 -34
  94. package/scripts/deploy-primershear-emails.sh +0 -167
  95. package/scripts/deploy-worker-secrets.sh +0 -374
  96. package/scripts/dev.cjs +0 -23
  97. package/scripts/install-workers.sh +0 -88
  98. package/scripts/run-eslint.cjs +0 -43
  99. package/scripts/update-compatibility-dates.cjs +0 -124
  100. package/scripts/update-markdown-versions.cjs +0 -43
  101. package/workers/keys-worker/package.json +0 -18
  102. package/workers/keys-worker/src/keys.example.ts +0 -67
  103. package/workers/keys-worker/src/keys.ts +0 -67
  104. package/workers/keys-worker/worker-configuration.d.ts +0 -7447
  105. package/workers/keys-worker/wrangler.jsonc.example +0 -15
@@ -0,0 +1,242 @@
1
+ import { executeUserDeletion } from '../cleanup/account-deletion';
2
+ import { readUserRecord, writeUserRecord } from '../storage/user-records';
3
+ import type {
4
+ AddCasesRequest,
5
+ AccountDeletionProgressEvent,
6
+ DeleteCasesRequest,
7
+ Env,
8
+ ResponseHeaders,
9
+ UserData,
10
+ UserRequestData
11
+ } from '../types';
12
+
13
+ function createJsonResponse(data: unknown, headers: ResponseHeaders, status: number = 200): Response {
14
+ return new Response(JSON.stringify(data), {
15
+ status,
16
+ headers
17
+ });
18
+ }
19
+
20
+ export async function handleGetUser(
21
+ env: Env,
22
+ userUid: string,
23
+ corsHeaders: ResponseHeaders
24
+ ): Promise<Response> {
25
+ try {
26
+ const userData = await readUserRecord(env, userUid);
27
+ if (userData === null) {
28
+ return new Response('User not found', {
29
+ status: 404,
30
+ headers: corsHeaders
31
+ });
32
+ }
33
+
34
+ return createJsonResponse(userData, corsHeaders);
35
+ } catch (error) {
36
+ const errorMessage = error instanceof Error ? error.message : 'Unknown user data read error';
37
+ console.error('Failed to get user data:', { uid: userUid, reason: errorMessage });
38
+
39
+ return new Response('Failed to get user data', {
40
+ status: 500,
41
+ headers: corsHeaders
42
+ });
43
+ }
44
+ }
45
+
46
+ export async function handleAddUser(
47
+ request: Request,
48
+ env: Env,
49
+ userUid: string,
50
+ corsHeaders: ResponseHeaders
51
+ ): Promise<Response> {
52
+ try {
53
+ const requestData: UserRequestData = await request.json();
54
+ const { email, firstName, lastName, company, badgeId, permitted } = requestData;
55
+ const normalizedBadgeId = typeof badgeId === 'string' ? badgeId.trim() : undefined;
56
+ const existingUser = await readUserRecord(env, userUid);
57
+
58
+ let userData: UserData;
59
+ if (existingUser !== null) {
60
+ userData = {
61
+ ...existingUser,
62
+ email: email || existingUser.email,
63
+ firstName: firstName || existingUser.firstName,
64
+ lastName: lastName || existingUser.lastName,
65
+ company: company || existingUser.company,
66
+ badgeId: normalizedBadgeId !== undefined ? normalizedBadgeId : (existingUser.badgeId ?? ''),
67
+ permitted: permitted !== undefined ? permitted : existingUser.permitted,
68
+ updatedAt: new Date().toISOString()
69
+ };
70
+ if (requestData.readOnlyCases !== undefined) {
71
+ userData.readOnlyCases = requestData.readOnlyCases;
72
+ }
73
+ } else {
74
+ userData = {
75
+ uid: userUid,
76
+ email: email || '',
77
+ firstName: firstName || '',
78
+ lastName: lastName || '',
79
+ company: company || '',
80
+ badgeId: normalizedBadgeId ?? '',
81
+ permitted: permitted !== undefined ? permitted : true,
82
+ cases: [],
83
+ createdAt: new Date().toISOString()
84
+ };
85
+ if (requestData.readOnlyCases !== undefined) {
86
+ userData.readOnlyCases = requestData.readOnlyCases;
87
+ }
88
+ }
89
+
90
+ await writeUserRecord(env, userUid, userData);
91
+
92
+ return createJsonResponse(userData, corsHeaders, existingUser !== null ? 200 : 201);
93
+ } catch {
94
+ return new Response('Failed to save user data', {
95
+ status: 500,
96
+ headers: corsHeaders
97
+ });
98
+ }
99
+ }
100
+
101
+ export async function handleDeleteUser(
102
+ env: Env,
103
+ userUid: string,
104
+ corsHeaders: ResponseHeaders
105
+ ): Promise<Response> {
106
+ try {
107
+ const result = await executeUserDeletion(env, userUid);
108
+
109
+ return createJsonResponse({
110
+ success: result.success,
111
+ message: result.message
112
+ }, corsHeaders);
113
+ } catch (error) {
114
+ console.error('Delete user error:', error);
115
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
116
+
117
+ if (errorMessage === 'User not found') {
118
+ return new Response('User not found', {
119
+ status: 404,
120
+ headers: corsHeaders
121
+ });
122
+ }
123
+
124
+ return createJsonResponse({
125
+ success: false,
126
+ message: 'Failed to delete user account'
127
+ }, corsHeaders, 500);
128
+ }
129
+ }
130
+
131
+ export function handleDeleteUserWithProgress(
132
+ env: Env,
133
+ userUid: string,
134
+ corsHeaders: ResponseHeaders
135
+ ): Response {
136
+ const sseHeaders: ResponseHeaders = {
137
+ ...corsHeaders,
138
+ 'Content-Type': 'text/event-stream',
139
+ 'Cache-Control': 'no-cache, no-transform',
140
+ Connection: 'keep-alive'
141
+ };
142
+
143
+ const encoder = new TextEncoder();
144
+ const stream = new ReadableStream<Uint8Array>({
145
+ async start(controller) {
146
+ const sendEvent = (payload: AccountDeletionProgressEvent): void => {
147
+ controller.enqueue(encoder.encode(`event: ${payload.event}\ndata: ${JSON.stringify(payload)}\n\n`));
148
+ };
149
+
150
+ try {
151
+ const result = await executeUserDeletion(env, userUid, sendEvent);
152
+ sendEvent({
153
+ event: 'complete',
154
+ totalCases: result.totalCases,
155
+ completedCases: result.completedCases,
156
+ success: result.success,
157
+ message: result.message
158
+ });
159
+ } catch (error) {
160
+ const errorMessage = error instanceof Error ? error.message : 'Failed to delete user account';
161
+
162
+ sendEvent({
163
+ event: 'error',
164
+ totalCases: 0,
165
+ completedCases: 0,
166
+ success: false,
167
+ message: errorMessage
168
+ });
169
+ } finally {
170
+ controller.close();
171
+ }
172
+ }
173
+ });
174
+
175
+ return new Response(stream, {
176
+ status: 200,
177
+ headers: sseHeaders
178
+ });
179
+ }
180
+
181
+ export async function handleAddCases(
182
+ request: Request,
183
+ env: Env,
184
+ userUid: string,
185
+ corsHeaders: ResponseHeaders
186
+ ): Promise<Response> {
187
+ try {
188
+ const { cases = [] }: AddCasesRequest = await request.json();
189
+ const userData = await readUserRecord(env, userUid);
190
+ if (!userData) {
191
+ return new Response('User not found', {
192
+ status: 404,
193
+ headers: corsHeaders
194
+ });
195
+ }
196
+
197
+ const existingCases = userData.cases || [];
198
+ const newCases = cases.filter((newCase) =>
199
+ !existingCases.some((existingCase) => existingCase.caseNumber === newCase.caseNumber)
200
+ );
201
+
202
+ userData.cases = [...existingCases, ...newCases];
203
+ userData.updatedAt = new Date().toISOString();
204
+ await writeUserRecord(env, userUid, userData);
205
+
206
+ return createJsonResponse(userData, corsHeaders);
207
+ } catch {
208
+ return new Response('Failed to add cases', {
209
+ status: 500,
210
+ headers: corsHeaders
211
+ });
212
+ }
213
+ }
214
+
215
+ export async function handleDeleteCases(
216
+ request: Request,
217
+ env: Env,
218
+ userUid: string,
219
+ corsHeaders: ResponseHeaders
220
+ ): Promise<Response> {
221
+ try {
222
+ const { casesToDelete }: DeleteCasesRequest = await request.json();
223
+ const userData = await readUserRecord(env, userUid);
224
+ if (!userData) {
225
+ return new Response('User not found', {
226
+ status: 404,
227
+ headers: corsHeaders
228
+ });
229
+ }
230
+
231
+ userData.cases = userData.cases.filter((caseItem) => !casesToDelete.includes(caseItem.caseNumber));
232
+ userData.updatedAt = new Date().toISOString();
233
+ await writeUserRecord(env, userUid, userData);
234
+
235
+ return createJsonResponse(userData, corsHeaders);
236
+ } catch {
237
+ return new Response('Failed to delete cases', {
238
+ status: 500,
239
+ headers: corsHeaders
240
+ });
241
+ }
242
+ }
@@ -0,0 +1,172 @@
1
+ import { decryptJsonFromUserKv, type UserKvEncryptedRecord } from '../encryption-utils';
2
+ import type {
3
+ DecryptionTelemetryOutcome,
4
+ Env,
5
+ KeyRegistryPayload,
6
+ PrivateKeyRegistry
7
+ } from '../types';
8
+
9
+ function normalizePrivateKeyPem(rawValue: string): string {
10
+ return rawValue.trim().replace(/^['"]|['"]$/g, '').replace(/\\n/g, '\n');
11
+ }
12
+
13
+ function getNonEmptyString(value: unknown): string | null {
14
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
15
+ }
16
+
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
+ };
77
+ }
78
+
79
+ function buildPrivateKeyCandidates(
80
+ recordKeyId: string,
81
+ registry: PrivateKeyRegistry
82
+ ): Array<{ keyId: string; privateKeyPem: string }> {
83
+ const candidates: Array<{ keyId: string; privateKeyPem: string }> = [];
84
+ const seen = new Set<string>();
85
+
86
+ const appendCandidate = (candidateKeyId: string | null): void => {
87
+ if (!candidateKeyId || seen.has(candidateKeyId)) {
88
+ return;
89
+ }
90
+
91
+ const privateKeyPem = registry.keys[candidateKeyId];
92
+ if (!privateKeyPem) {
93
+ return;
94
+ }
95
+
96
+ seen.add(candidateKeyId);
97
+ candidates.push({ keyId: candidateKeyId, privateKeyPem });
98
+ };
99
+
100
+ appendCandidate(getNonEmptyString(recordKeyId));
101
+ appendCandidate(registry.activeKeyId);
102
+
103
+ for (const keyId of Object.keys(registry.keys)) {
104
+ appendCandidate(keyId);
105
+ }
106
+
107
+ return candidates;
108
+ }
109
+
110
+ function logUserKvDecryptionTelemetry(input: {
111
+ recordKeyId: string;
112
+ selectedKeyId: string | null;
113
+ attemptCount: number;
114
+ outcome: DecryptionTelemetryOutcome;
115
+ reason?: string;
116
+ }): void {
117
+ const details = {
118
+ scope: 'user-kv',
119
+ recordKeyId: input.recordKeyId,
120
+ selectedKeyId: input.selectedKeyId,
121
+ attemptCount: input.attemptCount,
122
+ fallbackUsed: input.outcome === 'fallback-hit',
123
+ outcome: input.outcome,
124
+ reason: input.reason ?? null
125
+ };
126
+
127
+ if (input.outcome === 'all-failed') {
128
+ console.warn('Key registry decryption failed', details);
129
+ return;
130
+ }
131
+
132
+ console.info('Key registry decryption resolved', details);
133
+ }
134
+
135
+ export async function decryptUserKvRecord(
136
+ encryptedRecord: UserKvEncryptedRecord,
137
+ registry: PrivateKeyRegistry
138
+ ): Promise<string> {
139
+ const candidates = buildPrivateKeyCandidates(encryptedRecord.keyId, registry);
140
+ const primaryKeyId = candidates[0]?.keyId ?? null;
141
+ let lastError: unknown;
142
+
143
+ for (let index = 0; index < candidates.length; index += 1) {
144
+ const candidate = candidates[index];
145
+ try {
146
+ const decryptedJson = await decryptJsonFromUserKv(encryptedRecord, candidate.privateKeyPem);
147
+ logUserKvDecryptionTelemetry({
148
+ recordKeyId: encryptedRecord.keyId,
149
+ selectedKeyId: candidate.keyId,
150
+ attemptCount: index + 1,
151
+ outcome: candidate.keyId === primaryKeyId ? 'primary-hit' : 'fallback-hit'
152
+ });
153
+ return decryptedJson;
154
+ } catch (error) {
155
+ lastError = error;
156
+ }
157
+ }
158
+
159
+ logUserKvDecryptionTelemetry({
160
+ recordKeyId: encryptedRecord.keyId,
161
+ selectedKeyId: null,
162
+ attemptCount: candidates.length,
163
+ outcome: 'all-failed',
164
+ reason: lastError instanceof Error ? lastError.message : 'unknown decryption error'
165
+ });
166
+
167
+ throw new Error(
168
+ `Failed to decrypt user KV record after ${candidates.length} key attempt(s): ${
169
+ lastError instanceof Error ? lastError.message : 'unknown decryption error'
170
+ }`
171
+ );
172
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ encryptJsonForUserKv,
3
+ tryParseEncryptedRecord,
4
+ validateEncryptedRecord
5
+ } from '../encryption-utils';
6
+ import { decryptUserKvRecord, parseUserKvPrivateKeyRegistry } from '../registry/user-kv';
7
+ import type { Env, UserData } from '../types';
8
+
9
+ export async function readUserRecord(env: Env, userUid: string): Promise<UserData | null> {
10
+ const storedValue = await env.USER_DB.get(userUid);
11
+ if (storedValue === null) {
12
+ return null;
13
+ }
14
+
15
+ const encryptedRecord = tryParseEncryptedRecord(storedValue);
16
+ if (!encryptedRecord) {
17
+ throw new Error('User KV record is not encrypted');
18
+ }
19
+
20
+ validateEncryptedRecord(encryptedRecord);
21
+ const keyRegistry = parseUserKvPrivateKeyRegistry(env);
22
+ const decryptedJson = await decryptUserKvRecord(encryptedRecord, keyRegistry);
23
+ return JSON.parse(decryptedJson) as UserData;
24
+ }
25
+
26
+ export async function writeUserRecord(env: Env, userUid: string, userData: UserData): Promise<void> {
27
+ const encryptedPayload = await encryptJsonForUserKv(
28
+ JSON.stringify(userData),
29
+ env.USER_KV_ENCRYPTION_PUBLIC_KEY,
30
+ env.USER_KV_ENCRYPTION_KEY_ID
31
+ );
32
+
33
+ await env.USER_DB.put(userUid, encryptedPayload);
34
+ }
@@ -0,0 +1,106 @@
1
+ export interface Env {
2
+ USER_DB_AUTH: string;
3
+ USER_DB: KVNamespace;
4
+ STRIAE_DATA: R2Bucket;
5
+ STRIAE_FILES: R2Bucket;
6
+ R2_KEY_SECRET: string;
7
+ DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
8
+ DATA_AT_REST_ENCRYPTION_KEY_ID?: string;
9
+ DATA_AT_REST_ENCRYPTION_KEYS_JSON?: string;
10
+ DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID?: string;
11
+ PROJECT_ID: string;
12
+ FIREBASE_SERVICE_ACCOUNT_EMAIL: string;
13
+ FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY: string;
14
+ USER_KV_ENCRYPTION_PRIVATE_KEY: string;
15
+ USER_KV_ENCRYPTION_PUBLIC_KEY: string;
16
+ USER_KV_ENCRYPTION_KEY_ID: string;
17
+ USER_KV_ENCRYPTION_KEYS_JSON?: string;
18
+ USER_KV_ENCRYPTION_ACTIVE_KEY_ID?: string;
19
+ }
20
+
21
+ export interface KeyRegistryPayload {
22
+ activeKeyId?: unknown;
23
+ keys?: unknown;
24
+ }
25
+
26
+ export interface PrivateKeyRegistry {
27
+ activeKeyId: string | null;
28
+ keys: Record<string, string>;
29
+ }
30
+
31
+ export type DecryptionTelemetryOutcome = 'primary-hit' | 'fallback-hit' | 'all-failed';
32
+
33
+ export interface CaseItem {
34
+ caseNumber: string;
35
+ caseName?: string;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export interface ReadOnlyCaseItem {
40
+ caseNumber: string;
41
+ caseName?: string;
42
+ [key: string]: unknown;
43
+ }
44
+
45
+ export interface UserData {
46
+ uid: string;
47
+ email: string;
48
+ firstName: string;
49
+ lastName: string;
50
+ company: string;
51
+ badgeId?: string;
52
+ permitted: boolean;
53
+ cases: CaseItem[];
54
+ readOnlyCases?: ReadOnlyCaseItem[];
55
+ createdAt?: string;
56
+ updatedAt?: string;
57
+ }
58
+
59
+ export interface StoredCaseFileData {
60
+ id: string;
61
+ }
62
+
63
+ export interface StoredCaseData {
64
+ files?: StoredCaseFileData[];
65
+ }
66
+
67
+ export interface UserRequestData {
68
+ email?: string;
69
+ firstName?: string;
70
+ lastName?: string;
71
+ company?: string;
72
+ badgeId?: string;
73
+ permitted?: boolean;
74
+ readOnlyCases?: ReadOnlyCaseItem[];
75
+ }
76
+
77
+ export interface AddCasesRequest {
78
+ cases: CaseItem[];
79
+ }
80
+
81
+ export interface DeleteCasesRequest {
82
+ casesToDelete: string[];
83
+ }
84
+
85
+ export interface AccountDeletionProgressEvent {
86
+ event: 'start' | 'case-start' | 'case-complete' | 'complete' | 'error';
87
+ totalCases: number;
88
+ completedCases: number;
89
+ currentCaseNumber?: string;
90
+ success?: boolean;
91
+ message?: string;
92
+ }
93
+
94
+ export interface GoogleOAuthTokenResponse {
95
+ access_token?: string;
96
+ error?: string;
97
+ error_description?: string;
98
+ }
99
+
100
+ export interface FirebaseDeleteAccountErrorResponse {
101
+ error?: {
102
+ message?: string;
103
+ };
104
+ }
105
+
106
+ export type ResponseHeaders = Record<string, string>;