@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,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildExportDecryptionContext,
|
|
3
|
+
decryptExportDataWithRegistry,
|
|
4
|
+
decryptExportImageWithRegistry,
|
|
5
|
+
getNonEmptyString
|
|
6
|
+
} from '../registry/key-registry';
|
|
7
|
+
import type { CreateResponse, Env } from '../types';
|
|
8
|
+
|
|
9
|
+
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
10
|
+
const bytes = new Uint8Array(buffer);
|
|
11
|
+
const chunkSize = 8192;
|
|
12
|
+
let binary = '';
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
15
|
+
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
|
|
16
|
+
for (let j = 0; j < chunk.length; j += 1) {
|
|
17
|
+
binary += String.fromCharCode(chunk[j]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return btoa(binary);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function handleDecryptExport(
|
|
25
|
+
request: Request,
|
|
26
|
+
env: Env,
|
|
27
|
+
respond: CreateResponse
|
|
28
|
+
): Promise<Response> {
|
|
29
|
+
try {
|
|
30
|
+
const requestBody = await request.json() as {
|
|
31
|
+
wrappedKey?: string;
|
|
32
|
+
dataIv?: string;
|
|
33
|
+
encryptedData?: string;
|
|
34
|
+
encryptedImages?: Array<{ filename: string; encryptedData: string; iv?: string }>;
|
|
35
|
+
keyId?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const { wrappedKey, dataIv, encryptedData, encryptedImages, keyId } = requestBody;
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
!wrappedKey ||
|
|
42
|
+
typeof wrappedKey !== 'string' ||
|
|
43
|
+
!dataIv ||
|
|
44
|
+
typeof dataIv !== 'string' ||
|
|
45
|
+
!encryptedData ||
|
|
46
|
+
typeof encryptedData !== 'string'
|
|
47
|
+
) {
|
|
48
|
+
return respond(
|
|
49
|
+
{ error: 'Missing or invalid required fields: wrappedKey, dataIv, encryptedData' },
|
|
50
|
+
400
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const recordKeyId = getNonEmptyString(keyId);
|
|
55
|
+
const decryptionContext = buildExportDecryptionContext(recordKeyId, env);
|
|
56
|
+
|
|
57
|
+
let plaintextData: string;
|
|
58
|
+
try {
|
|
59
|
+
plaintextData = await decryptExportDataWithRegistry(
|
|
60
|
+
encryptedData,
|
|
61
|
+
wrappedKey,
|
|
62
|
+
dataIv,
|
|
63
|
+
decryptionContext
|
|
64
|
+
);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Data file decryption failed:', error);
|
|
67
|
+
const errorMessage = error instanceof Error ? error.message : 'Decryption failed';
|
|
68
|
+
return respond(
|
|
69
|
+
{ error: `Failed to decrypt data file: ${errorMessage}` },
|
|
70
|
+
500
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const decryptedImages: Array<{ filename: string; data: string }> = [];
|
|
75
|
+
if (Array.isArray(encryptedImages) && encryptedImages.length > 0) {
|
|
76
|
+
for (const imageEntry of encryptedImages) {
|
|
77
|
+
try {
|
|
78
|
+
if (!imageEntry.iv || typeof imageEntry.iv !== 'string') {
|
|
79
|
+
return respond(
|
|
80
|
+
{ error: `Missing IV for image ${imageEntry.filename}` },
|
|
81
|
+
400
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const imageBlob = await decryptExportImageWithRegistry(
|
|
86
|
+
imageEntry.encryptedData,
|
|
87
|
+
wrappedKey,
|
|
88
|
+
imageEntry.iv,
|
|
89
|
+
decryptionContext
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const base64Data = arrayBufferToBase64(await imageBlob.arrayBuffer());
|
|
93
|
+
decryptedImages.push({
|
|
94
|
+
filename: imageEntry.filename,
|
|
95
|
+
data: base64Data
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`Image decryption failed for ${imageEntry.filename}:`, error);
|
|
99
|
+
const errorMessage = error instanceof Error ? error.message : 'Decryption failed';
|
|
100
|
+
return respond(
|
|
101
|
+
{ error: `Failed to decrypt image ${imageEntry.filename}: ${errorMessage}` },
|
|
102
|
+
500
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return respond({
|
|
109
|
+
success: true,
|
|
110
|
+
plaintext: plaintextData,
|
|
111
|
+
decryptedImages
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Export decryption request failed:', error);
|
|
115
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
116
|
+
return respond({ error: errorMessage }, 500);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { signPayload as signWithWorkerKey } from '../signature-utils';
|
|
2
|
+
import {
|
|
3
|
+
AUDIT_EXPORT_SIGNATURE_VERSION,
|
|
4
|
+
CONFIRMATION_SIGNATURE_VERSION,
|
|
5
|
+
FORENSIC_MANIFEST_SIGNATURE_ALGORITHM,
|
|
6
|
+
FORENSIC_MANIFEST_VERSION,
|
|
7
|
+
type AuditExportSigningPayload,
|
|
8
|
+
type ConfirmationSigningPayload,
|
|
9
|
+
type ForensicManifestPayload,
|
|
10
|
+
createAuditExportSigningPayload,
|
|
11
|
+
createConfirmationSigningPayload,
|
|
12
|
+
createManifestSigningPayload,
|
|
13
|
+
isValidAuditExportPayload,
|
|
14
|
+
isValidConfirmationPayload,
|
|
15
|
+
isValidManifestPayload
|
|
16
|
+
} from '../signing-payload-utils';
|
|
17
|
+
import type { CreateResponse, Env } from '../types';
|
|
18
|
+
|
|
19
|
+
async function signPayloadWithWorkerKey(payload: string, env: Env): Promise<{
|
|
20
|
+
algorithm: string;
|
|
21
|
+
keyId: string;
|
|
22
|
+
signedAt: string;
|
|
23
|
+
value: string;
|
|
24
|
+
}> {
|
|
25
|
+
return signWithWorkerKey(
|
|
26
|
+
payload,
|
|
27
|
+
env.MANIFEST_SIGNING_PRIVATE_KEY,
|
|
28
|
+
env.MANIFEST_SIGNING_KEY_ID,
|
|
29
|
+
FORENSIC_MANIFEST_SIGNATURE_ALGORITHM
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function signManifest(manifest: ForensicManifestPayload, env: Env): Promise<{
|
|
34
|
+
algorithm: string;
|
|
35
|
+
keyId: string;
|
|
36
|
+
signedAt: string;
|
|
37
|
+
value: string;
|
|
38
|
+
}> {
|
|
39
|
+
const payload = createManifestSigningPayload(manifest);
|
|
40
|
+
return signPayloadWithWorkerKey(payload, env);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function signConfirmation(confirmationData: ConfirmationSigningPayload, env: Env): Promise<{
|
|
44
|
+
algorithm: string;
|
|
45
|
+
keyId: string;
|
|
46
|
+
signedAt: string;
|
|
47
|
+
value: string;
|
|
48
|
+
}> {
|
|
49
|
+
const payload = createConfirmationSigningPayload(confirmationData);
|
|
50
|
+
return signPayloadWithWorkerKey(payload, env);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function signAuditExport(auditExportData: AuditExportSigningPayload, env: Env): Promise<{
|
|
54
|
+
algorithm: string;
|
|
55
|
+
keyId: string;
|
|
56
|
+
signedAt: string;
|
|
57
|
+
value: string;
|
|
58
|
+
}> {
|
|
59
|
+
const payload = createAuditExportSigningPayload(auditExportData);
|
|
60
|
+
return signPayloadWithWorkerKey(payload, env);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function handleSignManifest(
|
|
64
|
+
request: Request,
|
|
65
|
+
env: Env,
|
|
66
|
+
respond: CreateResponse
|
|
67
|
+
): Promise<Response> {
|
|
68
|
+
try {
|
|
69
|
+
const requestBody = await request.json() as { manifest?: Partial<ForensicManifestPayload> } & Partial<ForensicManifestPayload>;
|
|
70
|
+
const manifestCandidate: Partial<ForensicManifestPayload> = requestBody.manifest ?? requestBody;
|
|
71
|
+
|
|
72
|
+
if (!manifestCandidate || !isValidManifestPayload(manifestCandidate)) {
|
|
73
|
+
return respond({ error: 'Invalid manifest payload' }, 400);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const signature = await signManifest(manifestCandidate, env);
|
|
77
|
+
|
|
78
|
+
return respond({
|
|
79
|
+
success: true,
|
|
80
|
+
manifestVersion: FORENSIC_MANIFEST_VERSION,
|
|
81
|
+
signature
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Manifest signing failed:', error);
|
|
85
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
86
|
+
return respond({ error: errorMessage }, 500);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function handleSignConfirmation(
|
|
91
|
+
request: Request,
|
|
92
|
+
env: Env,
|
|
93
|
+
respond: CreateResponse
|
|
94
|
+
): Promise<Response> {
|
|
95
|
+
try {
|
|
96
|
+
const requestBody = await request.json() as {
|
|
97
|
+
confirmationData?: Partial<ConfirmationSigningPayload>;
|
|
98
|
+
signatureVersion?: string;
|
|
99
|
+
} & Partial<ConfirmationSigningPayload>;
|
|
100
|
+
|
|
101
|
+
const requestedSignatureVersion =
|
|
102
|
+
typeof requestBody.signatureVersion === 'string' && requestBody.signatureVersion.trim().length > 0
|
|
103
|
+
? requestBody.signatureVersion
|
|
104
|
+
: CONFIRMATION_SIGNATURE_VERSION;
|
|
105
|
+
|
|
106
|
+
if (requestedSignatureVersion !== CONFIRMATION_SIGNATURE_VERSION) {
|
|
107
|
+
return respond(
|
|
108
|
+
{ error: `Unsupported confirmation signature version: ${requestedSignatureVersion}` },
|
|
109
|
+
400
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const confirmationCandidate: Partial<ConfirmationSigningPayload> = requestBody.confirmationData ?? requestBody;
|
|
114
|
+
|
|
115
|
+
if (!confirmationCandidate || !isValidConfirmationPayload(confirmationCandidate)) {
|
|
116
|
+
return respond({ error: 'Invalid confirmation payload' }, 400);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const signature = await signConfirmation(confirmationCandidate, env);
|
|
120
|
+
|
|
121
|
+
return respond({
|
|
122
|
+
success: true,
|
|
123
|
+
signatureVersion: CONFIRMATION_SIGNATURE_VERSION,
|
|
124
|
+
signature
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Confirmation signing failed:', error);
|
|
128
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
129
|
+
return respond({ error: errorMessage }, 500);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function handleSignAuditExport(
|
|
134
|
+
request: Request,
|
|
135
|
+
env: Env,
|
|
136
|
+
respond: CreateResponse
|
|
137
|
+
): Promise<Response> {
|
|
138
|
+
try {
|
|
139
|
+
const requestBody = await request.json() as {
|
|
140
|
+
auditExport?: Partial<AuditExportSigningPayload>;
|
|
141
|
+
signatureVersion?: string;
|
|
142
|
+
} & Partial<AuditExportSigningPayload>;
|
|
143
|
+
|
|
144
|
+
const requestedSignatureVersion =
|
|
145
|
+
typeof requestBody.signatureVersion === 'string' && requestBody.signatureVersion.trim().length > 0
|
|
146
|
+
? requestBody.signatureVersion
|
|
147
|
+
: AUDIT_EXPORT_SIGNATURE_VERSION;
|
|
148
|
+
|
|
149
|
+
if (requestedSignatureVersion !== AUDIT_EXPORT_SIGNATURE_VERSION) {
|
|
150
|
+
return respond(
|
|
151
|
+
{ error: `Unsupported audit export signature version: ${requestedSignatureVersion}` },
|
|
152
|
+
400
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const auditExportCandidate: Partial<AuditExportSigningPayload> = requestBody.auditExport ?? requestBody;
|
|
157
|
+
|
|
158
|
+
if (!auditExportCandidate || !isValidAuditExportPayload(auditExportCandidate)) {
|
|
159
|
+
return respond({ error: 'Invalid audit export payload' }, 400);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const signature = await signAuditExport(auditExportCandidate, env);
|
|
163
|
+
|
|
164
|
+
return respond({
|
|
165
|
+
success: true,
|
|
166
|
+
signatureVersion: AUDIT_EXPORT_SIGNATURE_VERSION,
|
|
167
|
+
signature
|
|
168
|
+
});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Audit export signing failed:', error);
|
|
171
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
172
|
+
return respond({ error: errorMessage }, 500);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DATA_AT_REST_ENCRYPTION_ALGORITHM,
|
|
3
|
+
DATA_AT_REST_ENCRYPTION_VERSION
|
|
4
|
+
} from '../config';
|
|
5
|
+
import { encryptJsonForStorage } from '../encryption-utils';
|
|
6
|
+
import {
|
|
7
|
+
decryptJsonFromStorageWithRegistry,
|
|
8
|
+
extractDataAtRestEnvelope,
|
|
9
|
+
isDataAtRestEncryptionEnabled
|
|
10
|
+
} from '../registry/key-registry';
|
|
11
|
+
import type { CreateResponse, Env } from '../types';
|
|
12
|
+
|
|
13
|
+
export async function handleStorageRequest(
|
|
14
|
+
request: Request,
|
|
15
|
+
env: Env,
|
|
16
|
+
pathname: string,
|
|
17
|
+
respond: CreateResponse
|
|
18
|
+
): Promise<Response> {
|
|
19
|
+
const bucket = env.STRIAE_DATA;
|
|
20
|
+
const filename = pathname.slice(1) || 'data.json';
|
|
21
|
+
|
|
22
|
+
if (!filename.endsWith('.json')) {
|
|
23
|
+
return respond({ error: 'Invalid file type. Only JSON files are allowed.' }, 400);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
switch (request.method) {
|
|
27
|
+
case 'GET': {
|
|
28
|
+
const file = await bucket.get(filename);
|
|
29
|
+
if (!file) {
|
|
30
|
+
return respond([], 200);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const atRestEnvelope = extractDataAtRestEnvelope(file);
|
|
34
|
+
if (atRestEnvelope) {
|
|
35
|
+
if (atRestEnvelope.algorithm !== DATA_AT_REST_ENCRYPTION_ALGORITHM) {
|
|
36
|
+
return respond({ error: 'Unsupported data-at-rest encryption algorithm' }, 500);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (atRestEnvelope.encryptionVersion !== DATA_AT_REST_ENCRYPTION_VERSION) {
|
|
40
|
+
return respond({ error: 'Unsupported data-at-rest encryption version' }, 500);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const encryptedData = await file.arrayBuffer();
|
|
45
|
+
const plaintext = await decryptJsonFromStorageWithRegistry(
|
|
46
|
+
encryptedData,
|
|
47
|
+
atRestEnvelope,
|
|
48
|
+
env
|
|
49
|
+
);
|
|
50
|
+
const decryptedPayload = JSON.parse(plaintext);
|
|
51
|
+
return respond(decryptedPayload);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Data-at-rest decryption failed:', error);
|
|
54
|
+
return respond({ error: 'Failed to decrypt stored data' }, 500);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const fileText = await file.text();
|
|
60
|
+
const data = JSON.parse(fileText);
|
|
61
|
+
return respond(data);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Stored JSON parse failed:', error);
|
|
64
|
+
return respond({ error: 'Stored data is corrupted or not valid JSON' }, 500);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case 'PUT': {
|
|
69
|
+
let newData: unknown;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
newData = await request.json();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Request JSON parse failed:', error);
|
|
75
|
+
return respond({ error: 'Invalid JSON request body' }, 400);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const serializedData = JSON.stringify(newData);
|
|
79
|
+
|
|
80
|
+
if (!isDataAtRestEncryptionEnabled(env)) {
|
|
81
|
+
await bucket.put(filename, serializedData);
|
|
82
|
+
return respond({ success: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY || !env.DATA_AT_REST_ENCRYPTION_KEY_ID) {
|
|
86
|
+
return respond(
|
|
87
|
+
{ error: 'Data-at-rest encryption is enabled but not fully configured' },
|
|
88
|
+
500
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const encryptedPayload = await encryptJsonForStorage(
|
|
94
|
+
serializedData,
|
|
95
|
+
env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY,
|
|
96
|
+
env.DATA_AT_REST_ENCRYPTION_KEY_ID
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await bucket.put(filename, encryptedPayload.ciphertext, {
|
|
100
|
+
customMetadata: {
|
|
101
|
+
algorithm: encryptedPayload.envelope.algorithm,
|
|
102
|
+
encryptionVersion: encryptedPayload.envelope.encryptionVersion,
|
|
103
|
+
keyId: encryptedPayload.envelope.keyId,
|
|
104
|
+
dataIv: encryptedPayload.envelope.dataIv,
|
|
105
|
+
wrappedKey: encryptedPayload.envelope.wrappedKey
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Data-at-rest encryption failed:', error);
|
|
110
|
+
return respond({ error: 'Failed to encrypt data for storage' }, 500);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return respond({ success: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'DELETE': {
|
|
117
|
+
const file = await bucket.get(filename);
|
|
118
|
+
if (!file) {
|
|
119
|
+
return respond({ error: 'File not found' }, 404);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await bucket.delete(filename);
|
|
123
|
+
return respond({ success: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
default:
|
|
127
|
+
return respond({ error: 'Method not allowed' }, 405);
|
|
128
|
+
}
|
|
129
|
+
}
|