@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.
- package/.env.example +36 -33
- 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 +10 -17
- 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 -336
- 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
|
@@ -1,735 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
DATA_AT_REST_ENCRYPTION_ENABLED?: string;
|
|
5
|
-
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
|
|
6
|
-
DATA_AT_REST_ENCRYPTION_PUBLIC_KEY?: string;
|
|
7
|
-
DATA_AT_REST_ENCRYPTION_KEY_ID?: string;
|
|
8
|
-
DATA_AT_REST_ENCRYPTION_KEYS_JSON?: string;
|
|
9
|
-
DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface KeyRegistryPayload {
|
|
13
|
-
activeKeyId?: unknown;
|
|
14
|
-
keys?: unknown;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface PrivateKeyRegistry {
|
|
18
|
-
activeKeyId: string | null;
|
|
19
|
-
keys: Record<string, string>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type DecryptionTelemetryOutcome = 'primary-hit' | 'fallback-hit' | 'all-failed';
|
|
23
|
-
|
|
24
|
-
interface AuditEntry {
|
|
25
|
-
timestamp: string;
|
|
26
|
-
userId: string;
|
|
27
|
-
action: string;
|
|
28
|
-
[key: string]: unknown;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface SuccessResponse {
|
|
32
|
-
success: boolean;
|
|
33
|
-
entryCount?: number;
|
|
34
|
-
filename?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ErrorResponse {
|
|
38
|
-
error: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface AuditRetrievalResponse {
|
|
42
|
-
entries: AuditEntry[];
|
|
43
|
-
total: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type APIResponse = SuccessResponse | ErrorResponse | AuditRetrievalResponse | Record<string, unknown>;
|
|
47
|
-
|
|
48
|
-
interface DataAtRestEnvelope {
|
|
49
|
-
algorithm: string;
|
|
50
|
-
encryptionVersion: string;
|
|
51
|
-
keyId: string;
|
|
52
|
-
dataIv: string;
|
|
53
|
-
wrappedKey: string;
|
|
54
|
-
}
|
|
1
|
+
import { hasValidHeader } from './config';
|
|
2
|
+
import { handleAuditRequest } from './handlers/audit-routes';
|
|
3
|
+
import type { CreateResponse, Env } from './types';
|
|
55
4
|
|
|
56
5
|
const corsHeaders: Record<string, string> = {
|
|
57
6
|
'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
|
|
58
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
7
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
59
8
|
'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key',
|
|
60
9
|
'Content-Type': 'application/json'
|
|
61
10
|
};
|
|
62
11
|
|
|
63
|
-
const
|
|
12
|
+
const createWorkerResponse: CreateResponse = (data, status: number = 200): Response => new Response(
|
|
64
13
|
JSON.stringify(data),
|
|
65
14
|
{ status, headers: corsHeaders }
|
|
66
15
|
);
|
|
67
16
|
|
|
68
|
-
const hasValidHeader = (request: Request, env: Env): boolean =>
|
|
69
|
-
request.headers.get('X-Custom-Auth-Key') === env.R2_KEY_SECRET;
|
|
70
|
-
|
|
71
|
-
const DATA_AT_REST_BACKFILL_PATH = '/api/admin/data-at-rest-backfill';
|
|
72
|
-
const DATA_AT_REST_ENCRYPTION_ALGORITHM = 'RSA-OAEP-AES-256-GCM';
|
|
73
|
-
const DATA_AT_REST_ENCRYPTION_VERSION = '1.0';
|
|
74
|
-
|
|
75
|
-
function normalizePrivateKeyPem(rawValue: string): string {
|
|
76
|
-
return rawValue.trim().replace(/^['"]|['"]$/g, '').replace(/\\n/g, '\n');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function getNonEmptyString(value: unknown): string | null {
|
|
80
|
-
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function parseDataAtRestPrivateKeyRegistry(env: Env): PrivateKeyRegistry {
|
|
84
|
-
const keys: Record<string, string> = {};
|
|
85
|
-
const configuredActiveKeyId = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID);
|
|
86
|
-
const registryJson = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_KEYS_JSON);
|
|
87
|
-
|
|
88
|
-
if (registryJson) {
|
|
89
|
-
let parsedRegistry: unknown;
|
|
90
|
-
try {
|
|
91
|
-
parsedRegistry = JSON.parse(registryJson) as unknown;
|
|
92
|
-
} catch {
|
|
93
|
-
throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON is not valid JSON');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!parsedRegistry || typeof parsedRegistry !== 'object') {
|
|
97
|
-
throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON must be an object');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const payload = parsedRegistry as KeyRegistryPayload;
|
|
101
|
-
const payloadActiveKeyId = getNonEmptyString(payload.activeKeyId);
|
|
102
|
-
const rawKeys = payload.keys && typeof payload.keys === 'object'
|
|
103
|
-
? payload.keys as Record<string, unknown>
|
|
104
|
-
: parsedRegistry as Record<string, unknown>;
|
|
105
|
-
|
|
106
|
-
for (const [keyId, pemValue] of Object.entries(rawKeys)) {
|
|
107
|
-
if (keyId === 'activeKeyId' || keyId === 'keys') {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const normalizedKeyId = getNonEmptyString(keyId);
|
|
112
|
-
const normalizedPem = getNonEmptyString(pemValue);
|
|
113
|
-
if (!normalizedKeyId || !normalizedPem) {
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
keys[normalizedKeyId] = normalizePrivateKeyPem(normalizedPem);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const resolvedActiveKeyId = configuredActiveKeyId ?? payloadActiveKeyId;
|
|
121
|
-
|
|
122
|
-
if (Object.keys(keys).length === 0) {
|
|
123
|
-
throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON does not contain any usable keys');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (resolvedActiveKeyId && !keys[resolvedActiveKeyId]) {
|
|
127
|
-
throw new Error('DATA_AT_REST active key ID is not present in DATA_AT_REST_ENCRYPTION_KEYS_JSON');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
activeKeyId: resolvedActiveKeyId ?? null,
|
|
132
|
-
keys
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const legacyKeyId = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_KEY_ID);
|
|
137
|
-
const legacyPrivateKey = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY);
|
|
138
|
-
if (!legacyKeyId || !legacyPrivateKey) {
|
|
139
|
-
throw new Error('Data-at-rest decryption key registry is not configured');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
keys[legacyKeyId] = normalizePrivateKeyPem(legacyPrivateKey);
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
activeKeyId: configuredActiveKeyId ?? legacyKeyId,
|
|
146
|
-
keys
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function buildPrivateKeyCandidates(
|
|
151
|
-
recordKeyId: string,
|
|
152
|
-
registry: PrivateKeyRegistry
|
|
153
|
-
): Array<{ keyId: string; privateKeyPem: string }> {
|
|
154
|
-
const candidates: Array<{ keyId: string; privateKeyPem: string }> = [];
|
|
155
|
-
const seen = new Set<string>();
|
|
156
|
-
|
|
157
|
-
const appendCandidate = (candidateKeyId: string | null): void => {
|
|
158
|
-
if (!candidateKeyId || seen.has(candidateKeyId)) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const privateKeyPem = registry.keys[candidateKeyId];
|
|
163
|
-
if (!privateKeyPem) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
seen.add(candidateKeyId);
|
|
168
|
-
candidates.push({ keyId: candidateKeyId, privateKeyPem });
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
appendCandidate(getNonEmptyString(recordKeyId));
|
|
172
|
-
appendCandidate(registry.activeKeyId);
|
|
173
|
-
|
|
174
|
-
for (const keyId of Object.keys(registry.keys)) {
|
|
175
|
-
appendCandidate(keyId);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return candidates;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function logAuditDecryptionTelemetry(input: {
|
|
182
|
-
recordKeyId: string;
|
|
183
|
-
selectedKeyId: string | null;
|
|
184
|
-
attemptCount: number;
|
|
185
|
-
outcome: DecryptionTelemetryOutcome;
|
|
186
|
-
reason?: string;
|
|
187
|
-
}): void {
|
|
188
|
-
const details = {
|
|
189
|
-
scope: 'audit-at-rest',
|
|
190
|
-
recordKeyId: input.recordKeyId,
|
|
191
|
-
selectedKeyId: input.selectedKeyId,
|
|
192
|
-
attemptCount: input.attemptCount,
|
|
193
|
-
fallbackUsed: input.outcome === 'fallback-hit',
|
|
194
|
-
outcome: input.outcome,
|
|
195
|
-
reason: input.reason ?? null
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
if (input.outcome === 'all-failed') {
|
|
199
|
-
console.warn('Key registry decryption failed', details);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
console.info('Key registry decryption resolved', details);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function decryptAuditJsonWithRegistry(
|
|
207
|
-
ciphertext: ArrayBuffer,
|
|
208
|
-
envelope: DataAtRestEnvelope,
|
|
209
|
-
env: Env
|
|
210
|
-
): Promise<string> {
|
|
211
|
-
const keyRegistry = parseDataAtRestPrivateKeyRegistry(env);
|
|
212
|
-
const candidates = buildPrivateKeyCandidates(envelope.keyId, keyRegistry);
|
|
213
|
-
const primaryKeyId = candidates[0]?.keyId ?? null;
|
|
214
|
-
let lastError: unknown;
|
|
215
|
-
|
|
216
|
-
for (let index = 0; index < candidates.length; index += 1) {
|
|
217
|
-
const candidate = candidates[index];
|
|
218
|
-
try {
|
|
219
|
-
const plaintext = await decryptJsonFromStorage(ciphertext, envelope, candidate.privateKeyPem);
|
|
220
|
-
logAuditDecryptionTelemetry({
|
|
221
|
-
recordKeyId: envelope.keyId,
|
|
222
|
-
selectedKeyId: candidate.keyId,
|
|
223
|
-
attemptCount: index + 1,
|
|
224
|
-
outcome: candidate.keyId === primaryKeyId ? 'primary-hit' : 'fallback-hit'
|
|
225
|
-
});
|
|
226
|
-
return plaintext;
|
|
227
|
-
} catch (error) {
|
|
228
|
-
lastError = error;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
logAuditDecryptionTelemetry({
|
|
233
|
-
recordKeyId: envelope.keyId,
|
|
234
|
-
selectedKeyId: null,
|
|
235
|
-
attemptCount: candidates.length,
|
|
236
|
-
outcome: 'all-failed',
|
|
237
|
-
reason: lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
throw new Error(
|
|
241
|
-
`Failed to decrypt audit record after ${candidates.length} key attempt(s): ${
|
|
242
|
-
lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
243
|
-
}`
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function isDataAtRestEncryptionEnabled(env: Env): boolean {
|
|
248
|
-
const value = env.DATA_AT_REST_ENCRYPTION_ENABLED;
|
|
249
|
-
if (!value) {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const normalizedValue = value.trim().toLowerCase();
|
|
254
|
-
return normalizedValue === '1' || normalizedValue === 'true' || normalizedValue === 'yes' || normalizedValue === 'on';
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function generateAuditFileName(userId: string): string {
|
|
258
|
-
const date = new Date().toISOString().split('T')[0];
|
|
259
|
-
return `audit-trails/${userId}/${date}.json`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function isValidAuditEntry(entry: unknown): entry is AuditEntry {
|
|
263
|
-
const candidate = entry as Partial<AuditEntry> | null;
|
|
264
|
-
|
|
265
|
-
return (
|
|
266
|
-
typeof candidate === 'object' &&
|
|
267
|
-
candidate !== null &&
|
|
268
|
-
typeof candidate.timestamp === 'string' &&
|
|
269
|
-
typeof candidate.userId === 'string' &&
|
|
270
|
-
typeof candidate.action === 'string'
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function base64UrlDecode(value: string): Uint8Array {
|
|
275
|
-
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
276
|
-
const padding = '='.repeat((4 - (normalized.length % 4)) % 4);
|
|
277
|
-
const decoded = atob(normalized + padding);
|
|
278
|
-
const bytes = new Uint8Array(decoded.length);
|
|
279
|
-
|
|
280
|
-
for (let i = 0; i < decoded.length; i += 1) {
|
|
281
|
-
bytes[i] = decoded.charCodeAt(i);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return bytes;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function base64UrlEncode(value: Uint8Array): string {
|
|
288
|
-
let binary = '';
|
|
289
|
-
const chunkSize = 8192;
|
|
290
|
-
|
|
291
|
-
for (let i = 0; i < value.length; i += chunkSize) {
|
|
292
|
-
const chunk = value.subarray(i, Math.min(i + chunkSize, value.length));
|
|
293
|
-
for (let j = 0; j < chunk.length; j += 1) {
|
|
294
|
-
binary += String.fromCharCode(chunk[j]);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return btoa(binary)
|
|
299
|
-
.replace(/\+/g, '-')
|
|
300
|
-
.replace(/\//g, '_')
|
|
301
|
-
.replace(/=+$/g, '');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function parsePkcs8PrivateKey(privateKey: string): ArrayBuffer {
|
|
305
|
-
const normalizedKey = privateKey
|
|
306
|
-
.trim()
|
|
307
|
-
.replace(/^['"]|['"]$/g, '')
|
|
308
|
-
.replace(/\\n/g, '\n');
|
|
309
|
-
|
|
310
|
-
const pemBody = normalizedKey
|
|
311
|
-
.replace('-----BEGIN PRIVATE KEY-----', '')
|
|
312
|
-
.replace('-----END PRIVATE KEY-----', '')
|
|
313
|
-
.replace(/\s+/g, '');
|
|
314
|
-
|
|
315
|
-
if (!pemBody) {
|
|
316
|
-
throw new Error('Encryption private key is invalid');
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const binary = atob(pemBody);
|
|
320
|
-
const bytes = new Uint8Array(binary.length);
|
|
321
|
-
|
|
322
|
-
for (let index = 0; index < binary.length; index += 1) {
|
|
323
|
-
bytes[index] = binary.charCodeAt(index);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return bytes.buffer;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function parseSpkiPublicKey(publicKey: string): ArrayBuffer {
|
|
330
|
-
const normalizedKey = publicKey
|
|
331
|
-
.trim()
|
|
332
|
-
.replace(/^['"]|['"]$/g, '')
|
|
333
|
-
.replace(/\\n/g, '\n');
|
|
334
|
-
|
|
335
|
-
const pemBody = normalizedKey
|
|
336
|
-
.replace('-----BEGIN PUBLIC KEY-----', '')
|
|
337
|
-
.replace('-----END PUBLIC KEY-----', '')
|
|
338
|
-
.replace(/\s+/g, '');
|
|
339
|
-
|
|
340
|
-
if (!pemBody) {
|
|
341
|
-
throw new Error('Encryption public key is invalid');
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const binary = atob(pemBody);
|
|
345
|
-
const bytes = new Uint8Array(binary.length);
|
|
346
|
-
|
|
347
|
-
for (let index = 0; index < binary.length; index += 1) {
|
|
348
|
-
bytes[index] = binary.charCodeAt(index);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return bytes.buffer;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
async function importRsaOaepPrivateKey(privateKeyPem: string): Promise<CryptoKey> {
|
|
355
|
-
return crypto.subtle.importKey(
|
|
356
|
-
'pkcs8',
|
|
357
|
-
parsePkcs8PrivateKey(privateKeyPem),
|
|
358
|
-
{
|
|
359
|
-
name: 'RSA-OAEP',
|
|
360
|
-
hash: 'SHA-256'
|
|
361
|
-
},
|
|
362
|
-
false,
|
|
363
|
-
['decrypt']
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async function importRsaOaepPublicKey(publicKeyPem: string): Promise<CryptoKey> {
|
|
368
|
-
return crypto.subtle.importKey(
|
|
369
|
-
'spki',
|
|
370
|
-
parseSpkiPublicKey(publicKeyPem),
|
|
371
|
-
{
|
|
372
|
-
name: 'RSA-OAEP',
|
|
373
|
-
hash: 'SHA-256'
|
|
374
|
-
},
|
|
375
|
-
false,
|
|
376
|
-
['encrypt']
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async function createAesGcmKey(usages: KeyUsage[]): Promise<CryptoKey> {
|
|
381
|
-
return crypto.subtle.generateKey(
|
|
382
|
-
{
|
|
383
|
-
name: 'AES-GCM',
|
|
384
|
-
length: 256
|
|
385
|
-
},
|
|
386
|
-
true,
|
|
387
|
-
usages
|
|
388
|
-
) as Promise<CryptoKey>;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
async function wrapAesKey(aesKey: CryptoKey, publicKeyPem: string): Promise<string> {
|
|
392
|
-
const rsaPublicKey = await importRsaOaepPublicKey(publicKeyPem);
|
|
393
|
-
const rawAesKey = await crypto.subtle.exportKey('raw', aesKey);
|
|
394
|
-
const wrappedKey = await crypto.subtle.encrypt(
|
|
395
|
-
{ name: 'RSA-OAEP' },
|
|
396
|
-
rsaPublicKey,
|
|
397
|
-
rawAesKey as BufferSource
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
return base64UrlEncode(new Uint8Array(wrappedKey));
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async function unwrapAesKey(wrappedKeyBase64: string, privateKeyPem: string): Promise<CryptoKey> {
|
|
404
|
-
const rsaPrivateKey = await importRsaOaepPrivateKey(privateKeyPem);
|
|
405
|
-
const wrappedKeyBytes = base64UrlDecode(wrappedKeyBase64);
|
|
406
|
-
|
|
407
|
-
const rawAesKey = await crypto.subtle.decrypt(
|
|
408
|
-
{ name: 'RSA-OAEP' },
|
|
409
|
-
rsaPrivateKey,
|
|
410
|
-
wrappedKeyBytes as BufferSource
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
return crypto.subtle.importKey(
|
|
414
|
-
'raw',
|
|
415
|
-
rawAesKey,
|
|
416
|
-
{ name: 'AES-GCM' },
|
|
417
|
-
false,
|
|
418
|
-
['decrypt']
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async function decryptJsonFromStorage(
|
|
423
|
-
ciphertext: ArrayBuffer,
|
|
424
|
-
envelope: DataAtRestEnvelope,
|
|
425
|
-
privateKeyPem: string
|
|
426
|
-
): Promise<string> {
|
|
427
|
-
const aesKey = await unwrapAesKey(envelope.wrappedKey, privateKeyPem);
|
|
428
|
-
const iv = base64UrlDecode(envelope.dataIv);
|
|
429
|
-
|
|
430
|
-
const plaintext = await crypto.subtle.decrypt(
|
|
431
|
-
{ name: 'AES-GCM', iv: iv as BufferSource },
|
|
432
|
-
aesKey,
|
|
433
|
-
ciphertext as BufferSource
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
return new TextDecoder().decode(plaintext);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async function encryptJsonForStorage(
|
|
440
|
-
plaintextJson: string,
|
|
441
|
-
publicKeyPem: string,
|
|
442
|
-
keyId: string
|
|
443
|
-
): Promise<{ ciphertext: Uint8Array; envelope: DataAtRestEnvelope }> {
|
|
444
|
-
const aesKey = await createAesGcmKey(['encrypt', 'decrypt']);
|
|
445
|
-
const wrappedKey = await wrapAesKey(aesKey, publicKeyPem);
|
|
446
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
447
|
-
|
|
448
|
-
const plaintextBytes = new TextEncoder().encode(plaintextJson);
|
|
449
|
-
const encryptedBuffer = await crypto.subtle.encrypt(
|
|
450
|
-
{ name: 'AES-GCM', iv: iv as BufferSource },
|
|
451
|
-
aesKey,
|
|
452
|
-
plaintextBytes as BufferSource
|
|
453
|
-
);
|
|
454
|
-
|
|
455
|
-
return {
|
|
456
|
-
ciphertext: new Uint8Array(encryptedBuffer),
|
|
457
|
-
envelope: {
|
|
458
|
-
algorithm: DATA_AT_REST_ENCRYPTION_ALGORITHM,
|
|
459
|
-
encryptionVersion: DATA_AT_REST_ENCRYPTION_VERSION,
|
|
460
|
-
keyId,
|
|
461
|
-
dataIv: base64UrlEncode(iv),
|
|
462
|
-
wrappedKey
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function extractDataAtRestEnvelope(file: R2ObjectBody): DataAtRestEnvelope | null {
|
|
468
|
-
const metadata = file.customMetadata;
|
|
469
|
-
if (!metadata) {
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const {
|
|
474
|
-
algorithm,
|
|
475
|
-
encryptionVersion,
|
|
476
|
-
keyId,
|
|
477
|
-
dataIv,
|
|
478
|
-
wrappedKey
|
|
479
|
-
} = metadata;
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
typeof algorithm !== 'string' ||
|
|
483
|
-
typeof encryptionVersion !== 'string' ||
|
|
484
|
-
typeof keyId !== 'string' ||
|
|
485
|
-
typeof dataIv !== 'string' ||
|
|
486
|
-
typeof wrappedKey !== 'string'
|
|
487
|
-
) {
|
|
488
|
-
return null;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return {
|
|
492
|
-
algorithm,
|
|
493
|
-
encryptionVersion,
|
|
494
|
-
keyId,
|
|
495
|
-
dataIv,
|
|
496
|
-
wrappedKey
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function hasDataAtRestMetadata(metadata: Record<string, string> | undefined): boolean {
|
|
501
|
-
if (!metadata) {
|
|
502
|
-
return false;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return (
|
|
506
|
-
typeof metadata.algorithm === 'string' &&
|
|
507
|
-
typeof metadata.encryptionVersion === 'string' &&
|
|
508
|
-
typeof metadata.keyId === 'string' &&
|
|
509
|
-
typeof metadata.dataIv === 'string' &&
|
|
510
|
-
typeof metadata.wrappedKey === 'string'
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function clampBackfillBatchSize(size: number | undefined): number {
|
|
515
|
-
if (typeof size !== 'number' || !Number.isFinite(size)) {
|
|
516
|
-
return 100;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const normalized = Math.floor(size);
|
|
520
|
-
if (normalized < 1) {
|
|
521
|
-
return 1;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (normalized > 1000) {
|
|
525
|
-
return 1000;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return normalized;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
async function readAuditEntriesFromObject(file: R2ObjectBody, env: Env): Promise<AuditEntry[]> {
|
|
532
|
-
const atRestEnvelope = extractDataAtRestEnvelope(file);
|
|
533
|
-
if (!atRestEnvelope) {
|
|
534
|
-
const fileText = await file.text();
|
|
535
|
-
return JSON.parse(fileText) as AuditEntry[];
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (atRestEnvelope.algorithm !== DATA_AT_REST_ENCRYPTION_ALGORITHM) {
|
|
539
|
-
throw new Error('Unsupported data-at-rest encryption algorithm');
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (atRestEnvelope.encryptionVersion !== DATA_AT_REST_ENCRYPTION_VERSION) {
|
|
543
|
-
throw new Error('Unsupported data-at-rest encryption version');
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const encryptedData = await file.arrayBuffer();
|
|
547
|
-
const plaintext = await decryptAuditJsonWithRegistry(
|
|
548
|
-
encryptedData,
|
|
549
|
-
atRestEnvelope,
|
|
550
|
-
env
|
|
551
|
-
);
|
|
552
|
-
|
|
553
|
-
return JSON.parse(plaintext) as AuditEntry[];
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
async function writeAuditEntriesToObject(
|
|
557
|
-
bucket: R2Bucket,
|
|
558
|
-
filename: string,
|
|
559
|
-
entries: AuditEntry[],
|
|
560
|
-
env: Env
|
|
561
|
-
): Promise<void> {
|
|
562
|
-
const serializedData = JSON.stringify(entries);
|
|
563
|
-
|
|
564
|
-
if (!isDataAtRestEncryptionEnabled(env)) {
|
|
565
|
-
await bucket.put(filename, serializedData);
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (!env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY || !env.DATA_AT_REST_ENCRYPTION_KEY_ID) {
|
|
570
|
-
throw new Error('Data-at-rest encryption is enabled but not fully configured');
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const encryptedPayload = await encryptJsonForStorage(
|
|
574
|
-
serializedData,
|
|
575
|
-
env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY,
|
|
576
|
-
env.DATA_AT_REST_ENCRYPTION_KEY_ID
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
await bucket.put(filename, encryptedPayload.ciphertext, {
|
|
580
|
-
customMetadata: {
|
|
581
|
-
algorithm: encryptedPayload.envelope.algorithm,
|
|
582
|
-
encryptionVersion: encryptedPayload.envelope.encryptionVersion,
|
|
583
|
-
keyId: encryptedPayload.envelope.keyId,
|
|
584
|
-
dataIv: encryptedPayload.envelope.dataIv,
|
|
585
|
-
wrappedKey: encryptedPayload.envelope.wrappedKey
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async function appendAuditEntry(
|
|
591
|
-
bucket: R2Bucket,
|
|
592
|
-
filename: string,
|
|
593
|
-
newEntry: AuditEntry,
|
|
594
|
-
env: Env
|
|
595
|
-
): Promise<number> {
|
|
596
|
-
try {
|
|
597
|
-
const existingFile = await bucket.get(filename);
|
|
598
|
-
let entries: AuditEntry[] = [];
|
|
599
|
-
|
|
600
|
-
if (existingFile) {
|
|
601
|
-
entries = await readAuditEntriesFromObject(existingFile, env);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
entries.push(newEntry);
|
|
605
|
-
await writeAuditEntriesToObject(bucket, filename, entries, env);
|
|
606
|
-
return entries.length;
|
|
607
|
-
} catch (error) {
|
|
608
|
-
console.error('Error appending audit entry:', error);
|
|
609
|
-
throw error;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
async function handleDataAtRestBackfill(request: Request, env: Env): Promise<Response> {
|
|
614
|
-
if (!env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY || !env.DATA_AT_REST_ENCRYPTION_KEY_ID) {
|
|
615
|
-
return createResponse(
|
|
616
|
-
{ error: 'Data-at-rest encryption is not configured for backfill writes' },
|
|
617
|
-
400
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const requestBody = await request.json().catch(() => ({})) as {
|
|
622
|
-
dryRun?: boolean;
|
|
623
|
-
prefix?: string;
|
|
624
|
-
cursor?: string;
|
|
625
|
-
batchSize?: number;
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
const dryRun = requestBody.dryRun === true;
|
|
629
|
-
const prefix = typeof requestBody.prefix === 'string' ? requestBody.prefix : '';
|
|
630
|
-
const cursor = typeof requestBody.cursor === 'string' && requestBody.cursor.length > 0
|
|
631
|
-
? requestBody.cursor
|
|
632
|
-
: undefined;
|
|
633
|
-
const batchSize = clampBackfillBatchSize(requestBody.batchSize);
|
|
634
|
-
|
|
635
|
-
const bucket = env.STRIAE_AUDIT;
|
|
636
|
-
const listed = await bucket.list({
|
|
637
|
-
prefix: prefix.length > 0 ? prefix : undefined,
|
|
638
|
-
cursor,
|
|
639
|
-
limit: batchSize
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
let scanned = 0;
|
|
643
|
-
let eligible = 0;
|
|
644
|
-
let encrypted = 0;
|
|
645
|
-
let skippedEncrypted = 0;
|
|
646
|
-
let skippedNonJson = 0;
|
|
647
|
-
let failed = 0;
|
|
648
|
-
const failures: Array<{ key: string; error: string }> = [];
|
|
649
|
-
|
|
650
|
-
for (const object of listed.objects) {
|
|
651
|
-
scanned += 1;
|
|
652
|
-
const key = object.key;
|
|
653
|
-
|
|
654
|
-
if (!key.endsWith('.json')) {
|
|
655
|
-
skippedNonJson += 1;
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const objectHead = await bucket.head(key);
|
|
660
|
-
if (!objectHead) {
|
|
661
|
-
failed += 1;
|
|
662
|
-
if (failures.length < 20) {
|
|
663
|
-
failures.push({ key, error: 'Object not found during metadata check' });
|
|
664
|
-
}
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
if (hasDataAtRestMetadata(objectHead.customMetadata)) {
|
|
669
|
-
skippedEncrypted += 1;
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
eligible += 1;
|
|
674
|
-
|
|
675
|
-
if (dryRun) {
|
|
676
|
-
continue;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
try {
|
|
680
|
-
const existingObject = await bucket.get(key);
|
|
681
|
-
if (!existingObject) {
|
|
682
|
-
failed += 1;
|
|
683
|
-
if (failures.length < 20) {
|
|
684
|
-
failures.push({ key, error: 'Object disappeared before processing' });
|
|
685
|
-
}
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const plaintext = await existingObject.text();
|
|
690
|
-
const encryptedPayload = await encryptJsonForStorage(
|
|
691
|
-
plaintext,
|
|
692
|
-
env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY,
|
|
693
|
-
env.DATA_AT_REST_ENCRYPTION_KEY_ID
|
|
694
|
-
);
|
|
695
|
-
|
|
696
|
-
await bucket.put(key, encryptedPayload.ciphertext, {
|
|
697
|
-
customMetadata: {
|
|
698
|
-
algorithm: encryptedPayload.envelope.algorithm,
|
|
699
|
-
encryptionVersion: encryptedPayload.envelope.encryptionVersion,
|
|
700
|
-
keyId: encryptedPayload.envelope.keyId,
|
|
701
|
-
dataIv: encryptedPayload.envelope.dataIv,
|
|
702
|
-
wrappedKey: encryptedPayload.envelope.wrappedKey
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
encrypted += 1;
|
|
707
|
-
} catch (error) {
|
|
708
|
-
failed += 1;
|
|
709
|
-
if (failures.length < 20) {
|
|
710
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown backfill failure';
|
|
711
|
-
failures.push({ key, error: errorMessage });
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
return createResponse({
|
|
717
|
-
success: failed === 0,
|
|
718
|
-
dryRun,
|
|
719
|
-
prefix: prefix.length > 0 ? prefix : null,
|
|
720
|
-
batchSize,
|
|
721
|
-
scanned,
|
|
722
|
-
eligible,
|
|
723
|
-
encrypted,
|
|
724
|
-
skippedEncrypted,
|
|
725
|
-
skippedNonJson,
|
|
726
|
-
failed,
|
|
727
|
-
failures,
|
|
728
|
-
hasMore: listed.truncated,
|
|
729
|
-
nextCursor: listed.truncated ? listed.cursor : null
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
|
|
733
17
|
export default {
|
|
734
18
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
735
19
|
if (request.method === 'OPTIONS') {
|
|
@@ -737,104 +21,23 @@ export default {
|
|
|
737
21
|
}
|
|
738
22
|
|
|
739
23
|
if (!hasValidHeader(request, env)) {
|
|
740
|
-
return
|
|
24
|
+
return createWorkerResponse({ error: 'Forbidden' }, 403);
|
|
741
25
|
}
|
|
742
26
|
|
|
743
27
|
try {
|
|
744
28
|
const url = new URL(request.url);
|
|
745
29
|
const pathname = url.pathname;
|
|
746
|
-
const bucket = env.STRIAE_AUDIT;
|
|
747
|
-
|
|
748
|
-
if (request.method === 'POST' && pathname === DATA_AT_REST_BACKFILL_PATH) {
|
|
749
|
-
return await handleDataAtRestBackfill(request, env);
|
|
750
|
-
}
|
|
751
30
|
|
|
752
31
|
if (!pathname.startsWith('/audit/')) {
|
|
753
|
-
return
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const userId = url.searchParams.get('userId');
|
|
757
|
-
const startDate = url.searchParams.get('startDate');
|
|
758
|
-
const endDate = url.searchParams.get('endDate');
|
|
759
|
-
|
|
760
|
-
if (request.method === 'POST') {
|
|
761
|
-
if (!userId) {
|
|
762
|
-
return createResponse({ error: 'userId parameter is required' }, 400);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const auditEntry: unknown = await request.json();
|
|
766
|
-
|
|
767
|
-
if (!isValidAuditEntry(auditEntry)) {
|
|
768
|
-
return createResponse({ error: 'Invalid audit entry structure. Required fields: timestamp, userId, action' }, 400);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const filename = generateAuditFileName(userId);
|
|
772
|
-
|
|
773
|
-
try {
|
|
774
|
-
const entryCount = await appendAuditEntry(bucket, filename, auditEntry, env);
|
|
775
|
-
return createResponse({
|
|
776
|
-
success: true,
|
|
777
|
-
entryCount,
|
|
778
|
-
filename
|
|
779
|
-
});
|
|
780
|
-
} catch (error) {
|
|
781
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
782
|
-
return createResponse({ error: `Failed to store audit entry: ${errorMessage}` }, 500);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (request.method === 'GET') {
|
|
787
|
-
if (!userId) {
|
|
788
|
-
return createResponse({ error: 'userId parameter is required' }, 400);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
try {
|
|
792
|
-
let allEntries: AuditEntry[] = [];
|
|
793
|
-
|
|
794
|
-
if (startDate && endDate) {
|
|
795
|
-
const start = new Date(startDate);
|
|
796
|
-
const end = new Date(endDate);
|
|
797
|
-
const currentDate = new Date(start);
|
|
798
|
-
|
|
799
|
-
while (currentDate <= end) {
|
|
800
|
-
const dateStr = currentDate.toISOString().split('T')[0];
|
|
801
|
-
const filename = `audit-trails/${userId}/${dateStr}.json`;
|
|
802
|
-
const file = await bucket.get(filename);
|
|
803
|
-
|
|
804
|
-
if (file) {
|
|
805
|
-
const entries = await readAuditEntriesFromObject(file, env);
|
|
806
|
-
allEntries.push(...entries);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
currentDate.setDate(currentDate.getDate() + 1);
|
|
810
|
-
}
|
|
811
|
-
} else {
|
|
812
|
-
const filename = generateAuditFileName(userId);
|
|
813
|
-
const file = await bucket.get(filename);
|
|
814
|
-
|
|
815
|
-
if (file) {
|
|
816
|
-
allEntries = await readAuditEntriesFromObject(file, env);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
allEntries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
821
|
-
|
|
822
|
-
return createResponse({
|
|
823
|
-
entries: allEntries,
|
|
824
|
-
total: allEntries.length
|
|
825
|
-
});
|
|
826
|
-
} catch (error) {
|
|
827
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
828
|
-
return createResponse({ error: `Failed to retrieve audit entries: ${errorMessage}` }, 500);
|
|
829
|
-
}
|
|
32
|
+
return createWorkerResponse({ error: 'This worker only handles audit endpoints. Use /audit/ path.' }, 404);
|
|
830
33
|
}
|
|
831
34
|
|
|
832
|
-
return
|
|
35
|
+
return await handleAuditRequest(request, env, url, createWorkerResponse);
|
|
833
36
|
|
|
834
37
|
} catch (error) {
|
|
835
38
|
console.error('Audit Worker error:', error);
|
|
836
39
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
837
|
-
return
|
|
40
|
+
return createWorkerResponse({ error: errorMessage }, 500);
|
|
838
41
|
}
|
|
839
42
|
}
|
|
840
43
|
};
|