@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,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
+ }