@striae-org/striae 3.0.4
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 +100 -0
- package/LICENSE +190 -0
- package/NOTICE +18 -0
- package/README.md +133 -0
- package/app/components/actions/case-export/core-export.ts +328 -0
- package/app/components/actions/case-export/data-processing.ts +167 -0
- package/app/components/actions/case-export/download-handlers.ts +900 -0
- package/app/components/actions/case-export/index.ts +41 -0
- package/app/components/actions/case-export/metadata-helpers.ts +107 -0
- package/app/components/actions/case-export/types-constants.ts +56 -0
- package/app/components/actions/case-export/validation-utils.ts +25 -0
- package/app/components/actions/case-export.ts +4 -0
- package/app/components/actions/case-import/annotation-import.ts +35 -0
- package/app/components/actions/case-import/confirmation-import.ts +363 -0
- package/app/components/actions/case-import/image-operations.ts +61 -0
- package/app/components/actions/case-import/index.ts +39 -0
- package/app/components/actions/case-import/orchestrator.ts +420 -0
- package/app/components/actions/case-import/storage-operations.ts +270 -0
- package/app/components/actions/case-import/validation.ts +189 -0
- package/app/components/actions/case-import/zip-processing.ts +413 -0
- package/app/components/actions/case-manage.ts +524 -0
- package/app/components/actions/case-review.ts +4 -0
- package/app/components/actions/confirm-export.ts +351 -0
- package/app/components/actions/generate-pdf.ts +210 -0
- package/app/components/actions/image-manage.ts +385 -0
- package/app/components/actions/notes-manage.ts +33 -0
- package/app/components/actions/signout.module.css +15 -0
- package/app/components/actions/signout.tsx +50 -0
- package/app/components/audit/user-audit-viewer.tsx +975 -0
- package/app/components/audit/user-audit.module.css +568 -0
- package/app/components/auth/auth-provider.tsx +78 -0
- package/app/components/auth/mfa-enrollment.module.css +268 -0
- package/app/components/auth/mfa-enrollment.tsx +398 -0
- package/app/components/auth/mfa-verification.module.css +251 -0
- package/app/components/auth/mfa-verification.tsx +295 -0
- package/app/components/button/button.module.css +63 -0
- package/app/components/button/button.tsx +46 -0
- package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
- package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
- package/app/components/canvas/canvas.module.css +314 -0
- package/app/components/canvas/canvas.tsx +449 -0
- package/app/components/canvas/confirmation/confirmation.module.css +187 -0
- package/app/components/canvas/confirmation/confirmation.tsx +214 -0
- package/app/components/colors/colors.module.css +59 -0
- package/app/components/colors/colors.tsx +68 -0
- package/app/components/form/base-form.tsx +21 -0
- package/app/components/form/form-button.tsx +28 -0
- package/app/components/form/form-field.tsx +53 -0
- package/app/components/form/form-message.tsx +17 -0
- package/app/components/form/form-toggle.tsx +23 -0
- package/app/components/form/form.module.css +427 -0
- package/app/components/form/index.ts +6 -0
- package/app/components/icon/icon.module.css +3 -0
- package/app/components/icon/icon.tsx +27 -0
- package/app/components/icon/icons.svg +102 -0
- package/app/components/icon/manifest.json +110 -0
- package/app/components/sidebar/case-export/case-export.module.css +386 -0
- package/app/components/sidebar/case-export/case-export.tsx +317 -0
- package/app/components/sidebar/case-import/case-import.module.css +626 -0
- package/app/components/sidebar/case-import/case-import.tsx +404 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
- package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
- package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
- package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
- package/app/components/sidebar/case-import/index.ts +18 -0
- package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
- package/app/components/sidebar/cases/cases-modal.module.css +166 -0
- package/app/components/sidebar/cases/cases-modal.tsx +201 -0
- package/app/components/sidebar/cases/cases.module.css +713 -0
- package/app/components/sidebar/files/files-modal.module.css +209 -0
- package/app/components/sidebar/files/files-modal.tsx +239 -0
- package/app/components/sidebar/hash/hash-utility.module.css +366 -0
- package/app/components/sidebar/hash/hash-utility.tsx +982 -0
- package/app/components/sidebar/notes/notes-modal.tsx +51 -0
- package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
- package/app/components/sidebar/notes/notes.module.css +360 -0
- package/app/components/sidebar/sidebar-container.tsx +149 -0
- package/app/components/sidebar/sidebar.module.css +321 -0
- package/app/components/sidebar/sidebar.tsx +215 -0
- package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
- package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
- package/app/components/theme-provider/theme-provider.tsx +131 -0
- package/app/components/theme-provider/theme.ts +155 -0
- package/app/components/toast/toast.module.css +137 -0
- package/app/components/toast/toast.tsx +56 -0
- package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
- package/app/components/toolbar/toolbar.module.css +42 -0
- package/app/components/toolbar/toolbar.tsx +167 -0
- package/app/components/user/delete-account.module.css +274 -0
- package/app/components/user/delete-account.tsx +471 -0
- package/app/components/user/inactivity-warning.module.css +145 -0
- package/app/components/user/inactivity-warning.tsx +84 -0
- package/app/components/user/manage-profile.module.css +190 -0
- package/app/components/user/manage-profile.tsx +253 -0
- package/app/components/user/mfa-phone-update.tsx +739 -0
- package/app/config-example/admin-service.json +13 -0
- package/app/config-example/config.json +17 -0
- package/app/config-example/firebase.ts +21 -0
- package/app/config-example/inactivity.ts +13 -0
- package/app/config-example/meta-config.json +6 -0
- package/app/contexts/auth.context.ts +12 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +44 -0
- package/app/hooks/useInactivityTimeout.ts +110 -0
- package/app/root.tsx +170 -0
- package/app/routes/_index.tsx +16 -0
- package/app/routes/auth/emailActionHandler.module.css +232 -0
- package/app/routes/auth/emailActionHandler.tsx +405 -0
- package/app/routes/auth/emailVerification.tsx +120 -0
- package/app/routes/auth/login.module.css +523 -0
- package/app/routes/auth/login.tsx +654 -0
- package/app/routes/auth/passwordReset.module.css +274 -0
- package/app/routes/auth/passwordReset.tsx +154 -0
- package/app/routes/auth/route.ts +16 -0
- package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
- package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
- package/app/routes/mobile-prevented/route.ts +14 -0
- package/app/routes/striae/striae.module.css +30 -0
- package/app/routes/striae/striae.tsx +417 -0
- package/app/services/audit-export.service.ts +755 -0
- package/app/services/audit.service.ts +1454 -0
- package/app/services/firebase-errors.ts +106 -0
- package/app/services/firebase.ts +15 -0
- package/app/styles/legal-pages.module.css +113 -0
- package/app/styles/root.module.css +146 -0
- package/app/tailwind.css +225 -0
- package/app/types/annotations.ts +45 -0
- package/app/types/audit.ts +301 -0
- package/app/types/case.ts +90 -0
- package/app/types/export.ts +8 -0
- package/app/types/file.ts +30 -0
- package/app/types/import.ts +107 -0
- package/app/types/index.ts +24 -0
- package/app/types/user.ts +38 -0
- package/app/utils/SHA256.ts +461 -0
- package/app/utils/annotation-timestamp.ts +25 -0
- package/app/utils/audit-export-signature.ts +117 -0
- package/app/utils/auth-action-settings.ts +48 -0
- package/app/utils/auth.ts +34 -0
- package/app/utils/batch-operations.ts +135 -0
- package/app/utils/confirmation-signature.ts +193 -0
- package/app/utils/data-operations.ts +871 -0
- package/app/utils/device-detection.ts +5 -0
- package/app/utils/html-sanitizer.ts +80 -0
- package/app/utils/id-generator.ts +36 -0
- package/app/utils/meta.ts +48 -0
- package/app/utils/mfa-phone.ts +97 -0
- package/app/utils/mfa.ts +79 -0
- package/app/utils/password-policy.ts +28 -0
- package/app/utils/permissions.ts +562 -0
- package/app/utils/signature-utils.ts +160 -0
- package/app/utils/style.ts +83 -0
- package/app/utils/version.ts +5 -0
- package/firebase.json +11 -0
- package/functions/[[path]].ts +10 -0
- package/package.json +138 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/publickey.info@striae.org.asc +17 -0
- package/public/.well-known/security.txt +7 -0
- package/public/_headers +28 -0
- package/public/_routes.json +13 -0
- package/public/assets/striae.jpg +0 -0
- package/public/clear.jpg +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-dark.png +0 -0
- package/public/manifest.json +25 -0
- package/public/oin-badge.png +0 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/striae-ascii.txt +10 -0
- package/scripts/deploy-all.sh +100 -0
- package/scripts/deploy-config.sh +940 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-worker-secrets.sh +215 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/install-workers.sh +88 -0
- package/scripts/run-eslint.cjs +35 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/tailwind.config.ts +22 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +35 -0
- package/worker-configuration.d.ts +7490 -0
- package/workers/audit-worker/package.json +17 -0
- package/workers/audit-worker/src/audit-worker.example.ts +195 -0
- package/workers/audit-worker/worker-configuration.d.ts +7448 -0
- package/workers/audit-worker/wrangler.jsonc.example +29 -0
- package/workers/data-worker/package.json +17 -0
- package/workers/data-worker/src/data-worker.example.ts +267 -0
- package/workers/data-worker/src/signature-utils.ts +79 -0
- package/workers/data-worker/src/signing-payload-utils.ts +290 -0
- package/workers/data-worker/worker-configuration.d.ts +7448 -0
- package/workers/data-worker/wrangler.jsonc.example +30 -0
- package/workers/image-worker/package.json +17 -0
- package/workers/image-worker/src/image-worker.example.ts +180 -0
- package/workers/image-worker/worker-configuration.d.ts +7447 -0
- package/workers/image-worker/wrangler.jsonc.example +22 -0
- package/workers/keys-worker/package.json +17 -0
- package/workers/keys-worker/src/keys.example.ts +66 -0
- package/workers/keys-worker/src/keys.ts +66 -0
- package/workers/keys-worker/worker-configuration.d.ts +7447 -0
- package/workers/keys-worker/wrangler.jsonc.example +22 -0
- package/workers/pdf-worker/package.json +17 -0
- package/workers/pdf-worker/src/format-striae.ts +534 -0
- package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
- package/workers/pdf-worker/src/report-types.ts +69 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
- package/workers/pdf-worker/wrangler.jsonc.example +26 -0
- package/workers/user-worker/package.json +17 -0
- package/workers/user-worker/src/user-worker.example.ts +636 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -0
- package/workers/user-worker/wrangler.jsonc.example +29 -0
- package/wrangler.toml.example +8 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 utility functions for data integrity validation
|
|
3
|
+
* Uses cryptographically secure SHA-256 algorithm for forensic applications
|
|
4
|
+
* Provides enhanced security compared to CRC32 for tamper detection
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { verifySignaturePayload } from './signature-utils';
|
|
8
|
+
|
|
9
|
+
export const FORENSIC_MANIFEST_VERSION = '2.0';
|
|
10
|
+
export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-PKCS1-v1_5-SHA-256';
|
|
11
|
+
|
|
12
|
+
export interface ForensicManifestData {
|
|
13
|
+
dataHash: string;
|
|
14
|
+
imageHashes: { [filename: string]: string };
|
|
15
|
+
manifestHash: string;
|
|
16
|
+
totalFiles: number;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ForensicManifestSignature {
|
|
21
|
+
algorithm: string;
|
|
22
|
+
keyId: string;
|
|
23
|
+
signedAt: string;
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SignedForensicManifest extends ForensicManifestData {
|
|
28
|
+
manifestVersion?: string;
|
|
29
|
+
signature?: ForensicManifestSignature;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ManifestSignatureVerificationResult {
|
|
33
|
+
isValid: boolean;
|
|
34
|
+
keyId?: string;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
|
|
39
|
+
|
|
40
|
+
function normalizeImageHashes(imageHashes: { [filename: string]: string }): { [filename: string]: string } {
|
|
41
|
+
const normalized: { [filename: string]: string } = {};
|
|
42
|
+
const sortedFilenames = Object.keys(imageHashes).sort();
|
|
43
|
+
|
|
44
|
+
for (const filename of sortedFilenames) {
|
|
45
|
+
normalized[filename] = imageHashes[filename].toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isValidManifestData(candidate: Partial<ForensicManifestData>): candidate is ForensicManifestData {
|
|
52
|
+
if (!candidate) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof candidate.dataHash !== 'string' || !SHA256_HEX_REGEX.test(candidate.dataHash)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!candidate.imageHashes || typeof candidate.imageHashes !== 'object') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const hash of Object.values(candidate.imageHashes)) {
|
|
65
|
+
if (typeof hash !== 'string' || !SHA256_HEX_REGEX.test(hash)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof candidate.manifestHash !== 'string' || !SHA256_HEX_REGEX.test(candidate.manifestHash)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof candidate.totalFiles !== 'number' || candidate.totalFiles <= 0) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof candidate.createdAt !== 'string' || Number.isNaN(Date.parse(candidate.createdAt))) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function extractForensicManifestData(candidate: Partial<SignedForensicManifest>): ForensicManifestData | null {
|
|
86
|
+
if (!isValidManifestData(candidate)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
dataHash: candidate.dataHash.toLowerCase(),
|
|
92
|
+
imageHashes: normalizeImageHashes(candidate.imageHashes),
|
|
93
|
+
manifestHash: candidate.manifestHash.toLowerCase(),
|
|
94
|
+
totalFiles: candidate.totalFiles,
|
|
95
|
+
createdAt: candidate.createdAt
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build canonical payload for manifest signatures.
|
|
101
|
+
* Every signer/verifier must use this exact ordering.
|
|
102
|
+
*/
|
|
103
|
+
export function createManifestSigningPayload(
|
|
104
|
+
manifest: ForensicManifestData,
|
|
105
|
+
manifestVersion: string = FORENSIC_MANIFEST_VERSION
|
|
106
|
+
): string {
|
|
107
|
+
const canonicalPayload = {
|
|
108
|
+
manifestVersion,
|
|
109
|
+
dataHash: manifest.dataHash.toLowerCase(),
|
|
110
|
+
imageHashes: normalizeImageHashes(manifest.imageHashes),
|
|
111
|
+
manifestHash: manifest.manifestHash.toLowerCase(),
|
|
112
|
+
totalFiles: manifest.totalFiles,
|
|
113
|
+
createdAt: manifest.createdAt
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return JSON.stringify(canonicalPayload);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Verify manifest signature using configured public key(s).
|
|
121
|
+
*/
|
|
122
|
+
export async function verifyForensicManifestSignature(
|
|
123
|
+
manifest: Partial<SignedForensicManifest>
|
|
124
|
+
): Promise<ManifestSignatureVerificationResult> {
|
|
125
|
+
if (!manifest.signature) {
|
|
126
|
+
return {
|
|
127
|
+
isValid: false,
|
|
128
|
+
error: 'Missing forensic manifest signature'
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (manifest.manifestVersion !== FORENSIC_MANIFEST_VERSION) {
|
|
133
|
+
return {
|
|
134
|
+
isValid: false,
|
|
135
|
+
keyId: manifest.signature.keyId,
|
|
136
|
+
error: `Unsupported manifest version: ${manifest.manifestVersion || 'unknown'}`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const manifestData = extractForensicManifestData(manifest);
|
|
141
|
+
if (!manifestData) {
|
|
142
|
+
return {
|
|
143
|
+
isValid: false,
|
|
144
|
+
keyId: manifest.signature.keyId,
|
|
145
|
+
error: 'Manifest content is malformed'
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const payload = createManifestSigningPayload(manifestData, manifest.manifestVersion);
|
|
150
|
+
|
|
151
|
+
return verifySignaturePayload(
|
|
152
|
+
payload,
|
|
153
|
+
manifest.signature,
|
|
154
|
+
FORENSIC_MANIFEST_SIGNATURE_ALGORITHM,
|
|
155
|
+
{
|
|
156
|
+
unsupportedAlgorithmPrefix: 'Unsupported signature algorithm',
|
|
157
|
+
missingKeyOrValueError: 'Missing signature key ID or value',
|
|
158
|
+
noVerificationKeyPrefix: 'No verification key configured for key ID',
|
|
159
|
+
invalidPublicKeyError: 'Manifest signature verification failed: invalid public key',
|
|
160
|
+
verificationFailedError: 'Manifest signature verification failed'
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Calculate SHA-256 hash for content integrity validation
|
|
167
|
+
* This implementation uses the Web Crypto API's SHA-256 for cryptographically
|
|
168
|
+
* secure hash generation used throughout the Striae application for forensic data validation.
|
|
169
|
+
*
|
|
170
|
+
* @param content - The string content to calculate hash for
|
|
171
|
+
* @returns SHA-256 hash as lowercase hexadecimal string (64 characters)
|
|
172
|
+
* @throws Error if content is null, undefined, or not a string
|
|
173
|
+
*/
|
|
174
|
+
export async function calculateSHA256(content: string): Promise<string> {
|
|
175
|
+
// Input validation for forensic integrity
|
|
176
|
+
if (content === null) {
|
|
177
|
+
throw new Error('SHA-256 calculation failed: Content cannot be null');
|
|
178
|
+
}
|
|
179
|
+
if (content === undefined) {
|
|
180
|
+
throw new Error('SHA-256 calculation failed: Content cannot be undefined');
|
|
181
|
+
}
|
|
182
|
+
if (typeof content !== 'string') {
|
|
183
|
+
throw new Error(`SHA-256 calculation failed: Content must be a string, received ${typeof content}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const encoder = new TextEncoder();
|
|
187
|
+
const data = encoder.encode(content);
|
|
188
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
189
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
190
|
+
|
|
191
|
+
return Array.from(hashArray)
|
|
192
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
193
|
+
.join('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Calculate SHA-256 hash with timing attack mitigation
|
|
198
|
+
* This version uses constant-time processing to prevent timing-based attacks
|
|
199
|
+
* on forensically sensitive content. Use this for high-security environments.
|
|
200
|
+
*
|
|
201
|
+
* @param content - The string content to calculate hash for
|
|
202
|
+
* @returns SHA-256 hash as lowercase hexadecimal string (64 characters)
|
|
203
|
+
* @throws Error if content is null, undefined, or not a string
|
|
204
|
+
*/
|
|
205
|
+
export async function calculateSHA256Secure(content: string): Promise<string> {
|
|
206
|
+
// Input validation for forensic integrity
|
|
207
|
+
if (content === null) {
|
|
208
|
+
throw new Error('SHA-256 secure calculation failed: Content cannot be null');
|
|
209
|
+
}
|
|
210
|
+
if (content === undefined) {
|
|
211
|
+
throw new Error('SHA-256 secure calculation failed: Content cannot be undefined');
|
|
212
|
+
}
|
|
213
|
+
if (typeof content !== 'string') {
|
|
214
|
+
throw new Error(`SHA-256 secure calculation failed: Content must be a string, received ${typeof content}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const encoder = new TextEncoder();
|
|
218
|
+
const originalData = encoder.encode(content);
|
|
219
|
+
|
|
220
|
+
// Timing attack mitigation: pad to next 64-byte boundary
|
|
221
|
+
// This reduces timing variance while maintaining algorithm correctness
|
|
222
|
+
const BLOCK_SIZE = 64;
|
|
223
|
+
const paddedLength = Math.ceil(originalData.length / BLOCK_SIZE) * BLOCK_SIZE;
|
|
224
|
+
const paddedData = new Uint8Array(paddedLength);
|
|
225
|
+
|
|
226
|
+
// Copy original data and pad with zeros
|
|
227
|
+
paddedData.set(originalData);
|
|
228
|
+
|
|
229
|
+
// For SHA-256 we hash original content, then add bounded extra work.
|
|
230
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', originalData);
|
|
231
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
232
|
+
|
|
233
|
+
const paddingBytes = paddedLength - originalData.length;
|
|
234
|
+
if (paddingBytes > 0) {
|
|
235
|
+
// Compute digest over padded data to reduce timing variance.
|
|
236
|
+
const paddingDigestBuffer = await crypto.subtle.digest('SHA-256', paddedData);
|
|
237
|
+
const paddingDigestArray = new Uint8Array(paddingDigestBuffer);
|
|
238
|
+
let volatile = 0;
|
|
239
|
+
for (let i = 0; i < paddingDigestArray.length; i += 1) {
|
|
240
|
+
volatile = (volatile * 31) ^ paddingDigestArray[i];
|
|
241
|
+
}
|
|
242
|
+
if (volatile === 0xdeadbeef) {
|
|
243
|
+
console.debug('');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return Array.from(hashArray)
|
|
248
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
249
|
+
.join('');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Calculate SHA-256 hash for binary data (images, files)
|
|
254
|
+
*
|
|
255
|
+
* @param data - Binary data as Uint8Array, ArrayBuffer, or Blob
|
|
256
|
+
* @returns SHA-256 hash as lowercase hexadecimal string (64 characters)
|
|
257
|
+
* @throws Error if data is null, undefined, or unsupported type
|
|
258
|
+
*/
|
|
259
|
+
export async function calculateSHA256Binary(data: Uint8Array | ArrayBuffer | Blob): Promise<string> {
|
|
260
|
+
// Input validation for forensic integrity
|
|
261
|
+
if (data === null) {
|
|
262
|
+
throw new Error('SHA-256 binary calculation failed: Data cannot be null');
|
|
263
|
+
}
|
|
264
|
+
if (data === undefined) {
|
|
265
|
+
throw new Error('SHA-256 binary calculation failed: Data cannot be undefined');
|
|
266
|
+
}
|
|
267
|
+
if (!(data instanceof Uint8Array || data instanceof ArrayBuffer || data instanceof Blob)) {
|
|
268
|
+
throw new Error('SHA-256 binary calculation failed: Data must be Uint8Array, ArrayBuffer, or Blob');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let buffer: ArrayBuffer;
|
|
272
|
+
|
|
273
|
+
if (data instanceof Blob) {
|
|
274
|
+
buffer = await data.arrayBuffer();
|
|
275
|
+
} else if (data instanceof ArrayBuffer) {
|
|
276
|
+
buffer = data;
|
|
277
|
+
} else {
|
|
278
|
+
buffer = data.buffer instanceof ArrayBuffer
|
|
279
|
+
? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
|
|
280
|
+
: new ArrayBuffer(data.length);
|
|
281
|
+
if (!(data.buffer instanceof ArrayBuffer)) {
|
|
282
|
+
new Uint8Array(buffer).set(data);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
|
287
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
288
|
+
|
|
289
|
+
return Array.from(hashArray)
|
|
290
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
291
|
+
.join('');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Generate comprehensive file manifest with secure hashes for forensic applications.
|
|
296
|
+
*/
|
|
297
|
+
export async function generateForensicManifestSecure(
|
|
298
|
+
dataContent: string,
|
|
299
|
+
imageFiles: { [filename: string]: Blob }
|
|
300
|
+
): Promise<ForensicManifestData> {
|
|
301
|
+
const dataHash = await calculateSHA256Secure(dataContent);
|
|
302
|
+
|
|
303
|
+
const imageHashes: { [filename: string]: string } = {};
|
|
304
|
+
const sortedFilenames = Object.keys(imageFiles).sort();
|
|
305
|
+
for (const filename of sortedFilenames) {
|
|
306
|
+
imageHashes[filename] = await calculateSHA256Binary(imageFiles[filename]);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const manifestForHash = {
|
|
310
|
+
dataHash,
|
|
311
|
+
imageHashes,
|
|
312
|
+
totalFiles: Object.keys(imageFiles).length + 1,
|
|
313
|
+
createdAt: new Date().toISOString()
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const manifestContent = JSON.stringify(manifestForHash);
|
|
317
|
+
const manifestHash = await calculateSHA256Secure(manifestContent);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
dataHash,
|
|
321
|
+
imageHashes,
|
|
322
|
+
manifestHash,
|
|
323
|
+
totalFiles: manifestForHash.totalFiles,
|
|
324
|
+
createdAt: manifestForHash.createdAt
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Generate secure forensic manifest with specific timestamp (for validation purposes).
|
|
330
|
+
*/
|
|
331
|
+
export async function generateForensicManifestWithTimestampSecure(
|
|
332
|
+
dataContent: string,
|
|
333
|
+
imageFiles: { [filename: string]: Blob },
|
|
334
|
+
createdAt: string
|
|
335
|
+
): Promise<ForensicManifestData> {
|
|
336
|
+
const dataHash = await calculateSHA256Secure(dataContent);
|
|
337
|
+
|
|
338
|
+
const imageHashes: { [filename: string]: string } = {};
|
|
339
|
+
const sortedFilenames = Object.keys(imageFiles).sort();
|
|
340
|
+
for (const filename of sortedFilenames) {
|
|
341
|
+
imageHashes[filename] = await calculateSHA256Binary(imageFiles[filename]);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const manifestForHash = {
|
|
345
|
+
dataHash,
|
|
346
|
+
imageHashes,
|
|
347
|
+
totalFiles: Object.keys(imageFiles).length + 1,
|
|
348
|
+
createdAt
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const manifestContent = JSON.stringify(manifestForHash);
|
|
352
|
+
const manifestHash = await calculateSHA256Secure(manifestContent);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
dataHash,
|
|
356
|
+
imageHashes,
|
|
357
|
+
manifestHash,
|
|
358
|
+
totalFiles: manifestForHash.totalFiles,
|
|
359
|
+
createdAt: manifestForHash.createdAt
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Validate complete case integrity including data and images using secure SHA-256.
|
|
365
|
+
*/
|
|
366
|
+
export async function validateCaseIntegritySecure(
|
|
367
|
+
dataContent: string,
|
|
368
|
+
imageFiles: { [filename: string]: Blob },
|
|
369
|
+
expectedManifest: ForensicManifestData
|
|
370
|
+
): Promise<{
|
|
371
|
+
isValid: boolean;
|
|
372
|
+
dataValid: boolean;
|
|
373
|
+
imageValidation: { [filename: string]: boolean };
|
|
374
|
+
manifestValid: boolean;
|
|
375
|
+
errors: string[];
|
|
376
|
+
summary: string;
|
|
377
|
+
}> {
|
|
378
|
+
const errors: string[] = [];
|
|
379
|
+
const imageValidation: { [filename: string]: boolean } = {};
|
|
380
|
+
|
|
381
|
+
const actualDataHash = await calculateSHA256Secure(dataContent);
|
|
382
|
+
const dataValid = actualDataHash === expectedManifest.dataHash.toLowerCase();
|
|
383
|
+
if (!dataValid) {
|
|
384
|
+
errors.push(`Data hash mismatch: expected ${expectedManifest.dataHash}, got ${actualDataHash}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const actualImageFiles = Object.keys(imageFiles).sort();
|
|
388
|
+
const expectedImageFiles = Object.keys(expectedManifest.imageHashes).sort();
|
|
389
|
+
|
|
390
|
+
const missingFiles = expectedImageFiles.filter((f) => !actualImageFiles.includes(f));
|
|
391
|
+
const extraFiles = actualImageFiles.filter((f) => !expectedImageFiles.includes(f));
|
|
392
|
+
|
|
393
|
+
if (missingFiles.length > 0) {
|
|
394
|
+
errors.push(`Missing image files: ${missingFiles.join(', ')}`);
|
|
395
|
+
}
|
|
396
|
+
if (extraFiles.length > 0) {
|
|
397
|
+
errors.push(`Extra image files not in manifest: ${extraFiles.join(', ')}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const filename of actualImageFiles) {
|
|
401
|
+
if (expectedManifest.imageHashes[filename]) {
|
|
402
|
+
const actualHash = await calculateSHA256Binary(imageFiles[filename]);
|
|
403
|
+
const isValid = actualHash === expectedManifest.imageHashes[filename].toLowerCase();
|
|
404
|
+
imageValidation[filename] = isValid;
|
|
405
|
+
|
|
406
|
+
if (!isValid) {
|
|
407
|
+
errors.push(`Image hash mismatch for ${filename}: expected ${expectedManifest.imageHashes[filename]}, got ${actualHash}`);
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
imageValidation[filename] = false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const recreatedManifest = await generateForensicManifestWithTimestampSecure(
|
|
415
|
+
dataContent,
|
|
416
|
+
imageFiles,
|
|
417
|
+
expectedManifest.createdAt
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
const manifestValid = recreatedManifest.manifestHash === expectedManifest.manifestHash.toLowerCase();
|
|
421
|
+
if (!manifestValid) {
|
|
422
|
+
errors.push(`Manifest hash mismatch: expected ${expectedManifest.manifestHash}, got ${recreatedManifest.manifestHash}`);
|
|
423
|
+
|
|
424
|
+
if (recreatedManifest.dataHash !== expectedManifest.dataHash.toLowerCase()) {
|
|
425
|
+
errors.push('Manifest data hash field differs from actual data');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const filename of Object.keys(imageFiles).sort()) {
|
|
429
|
+
if (
|
|
430
|
+
recreatedManifest.imageHashes[filename] &&
|
|
431
|
+
recreatedManifest.imageHashes[filename] !== expectedManifest.imageHashes[filename]?.toLowerCase()
|
|
432
|
+
) {
|
|
433
|
+
errors.push(`Manifest image hash entry for ${filename} differs from actual file`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const allImageFilesValid = Object.values(imageValidation).every((valid) => valid);
|
|
439
|
+
const isValid = dataValid && allImageFilesValid && manifestValid && errors.length === 0;
|
|
440
|
+
|
|
441
|
+
const totalFiles = Object.keys(imageFiles).length;
|
|
442
|
+
const validFiles = Object.values(imageValidation).filter((valid) => valid).length;
|
|
443
|
+
|
|
444
|
+
let summary = `Validation ${isValid ? 'PASSED' : 'FAILED'}: `;
|
|
445
|
+
summary += `Data ${dataValid ? 'valid' : 'invalid'}, `;
|
|
446
|
+
summary += `${validFiles}/${totalFiles} images valid, `;
|
|
447
|
+
summary += `manifest ${manifestValid ? 'valid' : 'invalid'}`;
|
|
448
|
+
|
|
449
|
+
if (errors.length > 0) {
|
|
450
|
+
summary += `. ${errors.length} error(s) detected`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
isValid,
|
|
455
|
+
dataValid,
|
|
456
|
+
imageValidation,
|
|
457
|
+
manifestValid,
|
|
458
|
+
errors,
|
|
459
|
+
summary
|
|
460
|
+
};
|
|
461
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const resolveEarliestAnnotationTimestamp = (
|
|
2
|
+
incomingTimestamp?: string,
|
|
3
|
+
existingTimestamp?: string,
|
|
4
|
+
fallbackTimestamp: string = new Date().toISOString()
|
|
5
|
+
): string => {
|
|
6
|
+
const candidates = [incomingTimestamp, existingTimestamp, fallbackTimestamp].filter(
|
|
7
|
+
(timestamp): timestamp is string => !!timestamp
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const oldestValid = candidates.reduce<{ value: string; time: number } | null>((oldest, timestamp) => {
|
|
11
|
+
const parsedTime = Date.parse(timestamp);
|
|
12
|
+
|
|
13
|
+
if (Number.isNaN(parsedTime)) {
|
|
14
|
+
return oldest;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!oldest || parsedTime < oldest.time) {
|
|
18
|
+
return { value: timestamp, time: parsedTime };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return oldest;
|
|
22
|
+
}, null);
|
|
23
|
+
|
|
24
|
+
return oldestValid?.value || fallbackTimestamp;
|
|
25
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ForensicManifestSignature,
|
|
3
|
+
FORENSIC_MANIFEST_SIGNATURE_ALGORITHM,
|
|
4
|
+
ManifestSignatureVerificationResult
|
|
5
|
+
} from './SHA256';
|
|
6
|
+
import { verifySignaturePayload } from './signature-utils';
|
|
7
|
+
|
|
8
|
+
export const AUDIT_EXPORT_SIGNATURE_VERSION = '1.0';
|
|
9
|
+
|
|
10
|
+
const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
|
|
11
|
+
|
|
12
|
+
export type AuditExportFormat = 'csv' | 'json' | 'txt';
|
|
13
|
+
export type AuditExportType = 'entries' | 'trail' | 'report';
|
|
14
|
+
export type AuditExportScopeType = 'case' | 'user';
|
|
15
|
+
|
|
16
|
+
export interface AuditExportSigningPayload {
|
|
17
|
+
signatureVersion: string;
|
|
18
|
+
exportFormat: AuditExportFormat;
|
|
19
|
+
exportType: AuditExportType;
|
|
20
|
+
scopeType: AuditExportScopeType;
|
|
21
|
+
scopeIdentifier: string;
|
|
22
|
+
generatedAt: string;
|
|
23
|
+
totalEntries: number;
|
|
24
|
+
hash: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isValidAuditExportSigningPayload(
|
|
28
|
+
payload: Partial<AuditExportSigningPayload>
|
|
29
|
+
): payload is AuditExportSigningPayload {
|
|
30
|
+
if (!payload) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (payload.signatureVersion !== AUDIT_EXPORT_SIGNATURE_VERSION) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (payload.exportFormat !== 'csv' && payload.exportFormat !== 'json' && payload.exportFormat !== 'txt') {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (payload.exportType !== 'entries' && payload.exportType !== 'trail' && payload.exportType !== 'report') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (payload.scopeType !== 'case' && payload.scopeType !== 'user') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof payload.scopeIdentifier !== 'string' || payload.scopeIdentifier.trim().length === 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof payload.generatedAt !== 'string' || Number.isNaN(Date.parse(payload.generatedAt))) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof payload.totalEntries !== 'number' || payload.totalEntries < 0) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof payload.hash !== 'string' || !SHA256_HEX_REGEX.test(payload.hash)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createAuditExportSigningPayload(payload: AuditExportSigningPayload): string {
|
|
70
|
+
const canonicalPayload = {
|
|
71
|
+
signatureVersion: payload.signatureVersion,
|
|
72
|
+
exportFormat: payload.exportFormat,
|
|
73
|
+
exportType: payload.exportType,
|
|
74
|
+
scopeType: payload.scopeType,
|
|
75
|
+
scopeIdentifier: payload.scopeIdentifier,
|
|
76
|
+
generatedAt: payload.generatedAt,
|
|
77
|
+
totalEntries: payload.totalEntries,
|
|
78
|
+
hash: payload.hash.toUpperCase()
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return JSON.stringify(canonicalPayload);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function verifyAuditExportSignature(
|
|
85
|
+
payload: Partial<AuditExportSigningPayload>,
|
|
86
|
+
signature?: ForensicManifestSignature
|
|
87
|
+
): Promise<ManifestSignatureVerificationResult> {
|
|
88
|
+
if (!signature) {
|
|
89
|
+
return {
|
|
90
|
+
isValid: false,
|
|
91
|
+
error: 'Missing audit export signature'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isValidAuditExportSigningPayload(payload)) {
|
|
96
|
+
return {
|
|
97
|
+
isValid: false,
|
|
98
|
+
keyId: signature.keyId,
|
|
99
|
+
error: 'Audit export signature metadata is malformed'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const signingPayload = createAuditExportSigningPayload(payload);
|
|
104
|
+
|
|
105
|
+
return verifySignaturePayload(
|
|
106
|
+
signingPayload,
|
|
107
|
+
signature,
|
|
108
|
+
FORENSIC_MANIFEST_SIGNATURE_ALGORITHM,
|
|
109
|
+
{
|
|
110
|
+
unsupportedAlgorithmPrefix: 'Unsupported audit export signature algorithm',
|
|
111
|
+
missingKeyOrValueError: 'Missing audit export signature key ID or value',
|
|
112
|
+
noVerificationKeyPrefix: 'No verification key configured for key ID',
|
|
113
|
+
invalidPublicKeyError: 'Audit export signature verification failed: invalid public key',
|
|
114
|
+
verificationFailedError: 'Audit export signature verification failed'
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ActionCodeSettings } from 'firebase/auth';
|
|
2
|
+
import paths from '~/config/config.json';
|
|
3
|
+
|
|
4
|
+
const AUTH_ROUTE_PATH = '/';
|
|
5
|
+
const DEFAULT_CONTINUE_PATH = '/';
|
|
6
|
+
|
|
7
|
+
const normalizedBaseUrl = paths.url.replace(/\/$/, '');
|
|
8
|
+
const appOrigin = new URL(normalizedBaseUrl).origin;
|
|
9
|
+
|
|
10
|
+
const normalizeContinuePath = (continuePath?: string): string => {
|
|
11
|
+
if (!continuePath || continuePath.trim().length === 0) {
|
|
12
|
+
return DEFAULT_CONTINUE_PATH;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!continuePath.startsWith('/') || continuePath.startsWith('//')) {
|
|
16
|
+
return DEFAULT_CONTINUE_PATH;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return continuePath;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const buildActionCodeSettings = (continuePath?: string): ActionCodeSettings => {
|
|
23
|
+
const safeContinuePath = normalizeContinuePath(continuePath);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
url: `${normalizedBaseUrl}${safeContinuePath}`,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getSafeContinuePath = (continueUrl: string | null | undefined): string => {
|
|
31
|
+
if (!continueUrl || continueUrl.trim().length === 0) {
|
|
32
|
+
return DEFAULT_CONTINUE_PATH;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const parsedUrl = new URL(continueUrl, appOrigin);
|
|
37
|
+
if (parsedUrl.origin !== appOrigin) {
|
|
38
|
+
return DEFAULT_CONTINUE_PATH;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const safePath = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
|
|
42
|
+
return safePath.startsWith('/') ? safePath : DEFAULT_CONTINUE_PATH;
|
|
43
|
+
} catch {
|
|
44
|
+
return DEFAULT_CONTINUE_PATH;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getAuthActionRoutePath = (): string => AUTH_ROUTE_PATH;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import paths from '~/config/config.json';
|
|
2
|
+
|
|
3
|
+
const KEYS_URL = paths.keys_url;
|
|
4
|
+
const KEYS_AUTH = paths.keys_auth;
|
|
5
|
+
|
|
6
|
+
type KeyType = 'USER_DB_AUTH' | 'R2_KEY_SECRET' | 'IMAGES_API_TOKEN' | 'ACCOUNT_HASH';
|
|
7
|
+
|
|
8
|
+
async function getApiKey(keyType: KeyType): Promise<string> {
|
|
9
|
+
const keyResponse = await fetch(`${KEYS_URL}/${keyType}`, {
|
|
10
|
+
headers: {
|
|
11
|
+
'X-Custom-Auth-Key': KEYS_AUTH
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
if (!keyResponse.ok) {
|
|
15
|
+
throw new Error(`Failed to retrieve ${keyType}`);
|
|
16
|
+
}
|
|
17
|
+
return keyResponse.text();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function getUserApiKey(): Promise<string> {
|
|
21
|
+
return getApiKey('USER_DB_AUTH');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getDataApiKey(): Promise<string> {
|
|
25
|
+
return getApiKey('R2_KEY_SECRET');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getImageApiKey(): Promise<string> {
|
|
29
|
+
return getApiKey('IMAGES_API_TOKEN');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function getAccountHash(): Promise<string> {
|
|
33
|
+
return getApiKey('ACCOUNT_HASH');
|
|
34
|
+
}
|