@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,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "AUDIT_WORKER_NAME",
|
|
3
|
+
"account_id": "ACCOUNT_ID",
|
|
4
|
+
"main": "src/audit-worker.ts",
|
|
5
|
+
"compatibility_date": "2026-03-09",
|
|
6
|
+
"compatibility_flags": [
|
|
7
|
+
"nodejs_compat"
|
|
8
|
+
],
|
|
9
|
+
|
|
10
|
+
"observability": {
|
|
11
|
+
"enabled": true
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
"r2_buckets": [
|
|
15
|
+
{
|
|
16
|
+
"binding": "STRIAE_AUDIT",
|
|
17
|
+
"bucket_name": "AUDIT_BUCKET_NAME"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
|
|
21
|
+
"routes": [
|
|
22
|
+
{
|
|
23
|
+
"pattern": "AUDIT_WORKER_DOMAIN",
|
|
24
|
+
"custom_domain": true
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
"placement": { "mode": "smart" }
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "data-worker",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"deploy": "wrangler deploy",
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"start": "wrangler dev",
|
|
9
|
+
"test": "vitest"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@cloudflare/puppeteer": "^1.0.4",
|
|
13
|
+
"@cloudflare/vitest-pool-workers": "^0.12.9",
|
|
14
|
+
"vitest": "~3.2.0",
|
|
15
|
+
"wrangler": "^4.69.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
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
|
+
AuditExportSigningPayload,
|
|
8
|
+
ConfirmationSigningPayload,
|
|
9
|
+
ForensicManifestPayload,
|
|
10
|
+
createAuditExportSigningPayload,
|
|
11
|
+
createConfirmationSigningPayload,
|
|
12
|
+
createManifestSigningPayload,
|
|
13
|
+
isValidAuditExportPayload,
|
|
14
|
+
isValidConfirmationPayload,
|
|
15
|
+
isValidManifestPayload
|
|
16
|
+
} from './signing-payload-utils';
|
|
17
|
+
|
|
18
|
+
interface Env {
|
|
19
|
+
R2_KEY_SECRET: string;
|
|
20
|
+
STRIAE_DATA: R2Bucket;
|
|
21
|
+
MANIFEST_SIGNING_PRIVATE_KEY: string;
|
|
22
|
+
MANIFEST_SIGNING_KEY_ID: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SuccessResponse {
|
|
26
|
+
success: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ErrorResponse {
|
|
30
|
+
error: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type APIResponse = SuccessResponse | ErrorResponse | any[] | Record<string, any>;
|
|
34
|
+
|
|
35
|
+
const corsHeaders: Record<string, string> = {
|
|
36
|
+
'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
|
|
37
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
38
|
+
'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key',
|
|
39
|
+
'Content-Type': 'application/json'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const createResponse = (data: APIResponse, status: number = 200): Response => new Response(
|
|
43
|
+
JSON.stringify(data),
|
|
44
|
+
{ status, headers: corsHeaders }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const hasValidHeader = (request: Request, env: Env): boolean =>
|
|
48
|
+
request.headers.get('X-Custom-Auth-Key') === env.R2_KEY_SECRET;
|
|
49
|
+
|
|
50
|
+
const SIGN_MANIFEST_PATH = '/api/forensic/sign-manifest';
|
|
51
|
+
const SIGN_CONFIRMATION_PATH = '/api/forensic/sign-confirmation';
|
|
52
|
+
const SIGN_AUDIT_EXPORT_PATH = '/api/forensic/sign-audit-export';
|
|
53
|
+
|
|
54
|
+
async function signPayloadWithWorkerKey(payload: string, env: Env): Promise<{
|
|
55
|
+
algorithm: string;
|
|
56
|
+
keyId: string;
|
|
57
|
+
signedAt: string;
|
|
58
|
+
value: string;
|
|
59
|
+
}> {
|
|
60
|
+
return signWithWorkerKey(
|
|
61
|
+
payload,
|
|
62
|
+
env.MANIFEST_SIGNING_PRIVATE_KEY,
|
|
63
|
+
env.MANIFEST_SIGNING_KEY_ID,
|
|
64
|
+
FORENSIC_MANIFEST_SIGNATURE_ALGORITHM
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function signManifest(manifest: ForensicManifestPayload, env: Env): Promise<{
|
|
69
|
+
algorithm: string;
|
|
70
|
+
keyId: string;
|
|
71
|
+
signedAt: string;
|
|
72
|
+
value: string;
|
|
73
|
+
}> {
|
|
74
|
+
const payload = createManifestSigningPayload(manifest);
|
|
75
|
+
return signPayloadWithWorkerKey(payload, env);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function signConfirmation(confirmationData: ConfirmationSigningPayload, env: Env): Promise<{
|
|
79
|
+
algorithm: string;
|
|
80
|
+
keyId: string;
|
|
81
|
+
signedAt: string;
|
|
82
|
+
value: string;
|
|
83
|
+
}> {
|
|
84
|
+
const payload = createConfirmationSigningPayload(confirmationData);
|
|
85
|
+
return signPayloadWithWorkerKey(payload, env);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function signAuditExport(auditExportData: AuditExportSigningPayload, env: Env): Promise<{
|
|
89
|
+
algorithm: string;
|
|
90
|
+
keyId: string;
|
|
91
|
+
signedAt: string;
|
|
92
|
+
value: string;
|
|
93
|
+
}> {
|
|
94
|
+
const payload = createAuditExportSigningPayload(auditExportData);
|
|
95
|
+
return signPayloadWithWorkerKey(payload, env);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleSignManifest(request: Request, env: Env): Promise<Response> {
|
|
99
|
+
try {
|
|
100
|
+
const requestBody = await request.json() as { manifest?: Partial<ForensicManifestPayload> } & Partial<ForensicManifestPayload>;
|
|
101
|
+
const manifestCandidate: Partial<ForensicManifestPayload> = requestBody.manifest ?? requestBody;
|
|
102
|
+
|
|
103
|
+
if (!manifestCandidate || !isValidManifestPayload(manifestCandidate)) {
|
|
104
|
+
return createResponse({ error: 'Invalid manifest payload' }, 400);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const signature = await signManifest(manifestCandidate, env);
|
|
108
|
+
|
|
109
|
+
return createResponse({
|
|
110
|
+
success: true,
|
|
111
|
+
manifestVersion: FORENSIC_MANIFEST_VERSION,
|
|
112
|
+
signature
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('Manifest signing failed:', error);
|
|
116
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
117
|
+
return createResponse({ error: errorMessage }, 500);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function handleSignConfirmation(request: Request, env: Env): Promise<Response> {
|
|
122
|
+
try {
|
|
123
|
+
const requestBody = await request.json() as {
|
|
124
|
+
confirmationData?: Partial<ConfirmationSigningPayload>;
|
|
125
|
+
signatureVersion?: string;
|
|
126
|
+
} & Partial<ConfirmationSigningPayload>;
|
|
127
|
+
|
|
128
|
+
const requestedSignatureVersion =
|
|
129
|
+
typeof requestBody.signatureVersion === 'string' && requestBody.signatureVersion.trim().length > 0
|
|
130
|
+
? requestBody.signatureVersion
|
|
131
|
+
: CONFIRMATION_SIGNATURE_VERSION;
|
|
132
|
+
|
|
133
|
+
if (requestedSignatureVersion !== CONFIRMATION_SIGNATURE_VERSION) {
|
|
134
|
+
return createResponse(
|
|
135
|
+
{ error: `Unsupported confirmation signature version: ${requestedSignatureVersion}` },
|
|
136
|
+
400
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const confirmationCandidate: Partial<ConfirmationSigningPayload> = requestBody.confirmationData ?? requestBody;
|
|
141
|
+
|
|
142
|
+
if (!confirmationCandidate || !isValidConfirmationPayload(confirmationCandidate)) {
|
|
143
|
+
return createResponse({ error: 'Invalid confirmation payload' }, 400);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const signature = await signConfirmation(confirmationCandidate, env);
|
|
147
|
+
|
|
148
|
+
return createResponse({
|
|
149
|
+
success: true,
|
|
150
|
+
signatureVersion: CONFIRMATION_SIGNATURE_VERSION,
|
|
151
|
+
signature
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('Confirmation signing failed:', error);
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
156
|
+
return createResponse({ error: errorMessage }, 500);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function handleSignAuditExport(request: Request, env: Env): Promise<Response> {
|
|
161
|
+
try {
|
|
162
|
+
const requestBody = await request.json() as {
|
|
163
|
+
auditExport?: Partial<AuditExportSigningPayload>;
|
|
164
|
+
signatureVersion?: string;
|
|
165
|
+
} & Partial<AuditExportSigningPayload>;
|
|
166
|
+
|
|
167
|
+
const requestedSignatureVersion =
|
|
168
|
+
typeof requestBody.signatureVersion === 'string' && requestBody.signatureVersion.trim().length > 0
|
|
169
|
+
? requestBody.signatureVersion
|
|
170
|
+
: AUDIT_EXPORT_SIGNATURE_VERSION;
|
|
171
|
+
|
|
172
|
+
if (requestedSignatureVersion !== AUDIT_EXPORT_SIGNATURE_VERSION) {
|
|
173
|
+
return createResponse(
|
|
174
|
+
{ error: `Unsupported audit export signature version: ${requestedSignatureVersion}` },
|
|
175
|
+
400
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const auditExportCandidate: Partial<AuditExportSigningPayload> = requestBody.auditExport ?? requestBody;
|
|
180
|
+
|
|
181
|
+
if (!auditExportCandidate || !isValidAuditExportPayload(auditExportCandidate)) {
|
|
182
|
+
return createResponse({ error: 'Invalid audit export payload' }, 400);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const signature = await signAuditExport(auditExportCandidate, env);
|
|
186
|
+
|
|
187
|
+
return createResponse({
|
|
188
|
+
success: true,
|
|
189
|
+
signatureVersion: AUDIT_EXPORT_SIGNATURE_VERSION,
|
|
190
|
+
signature
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('Audit export signing failed:', error);
|
|
194
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
195
|
+
return createResponse({ error: errorMessage }, 500);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default {
|
|
200
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
201
|
+
if (request.method === 'OPTIONS') {
|
|
202
|
+
return new Response(null, { headers: corsHeaders });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!hasValidHeader(request, env)) {
|
|
206
|
+
return createResponse({ error: 'Forbidden' }, 403);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const url = new URL(request.url);
|
|
211
|
+
const pathname = url.pathname;
|
|
212
|
+
const bucket = env.STRIAE_DATA;
|
|
213
|
+
|
|
214
|
+
if (request.method === 'POST' && pathname === SIGN_MANIFEST_PATH) {
|
|
215
|
+
return await handleSignManifest(request, env);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (request.method === 'POST' && pathname === SIGN_CONFIRMATION_PATH) {
|
|
219
|
+
return await handleSignConfirmation(request, env);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (request.method === 'POST' && pathname === SIGN_AUDIT_EXPORT_PATH) {
|
|
223
|
+
return await handleSignAuditExport(request, env);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const filename = pathname.slice(1) || 'data.json';
|
|
227
|
+
|
|
228
|
+
if (!filename.endsWith('.json')) {
|
|
229
|
+
return createResponse({ error: 'Invalid file type. Only JSON files are allowed.' }, 400);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
switch (request.method) {
|
|
233
|
+
case 'GET': {
|
|
234
|
+
const file = await bucket.get(filename);
|
|
235
|
+
if (!file) {
|
|
236
|
+
return createResponse([], 200);
|
|
237
|
+
}
|
|
238
|
+
const fileText = await file.text();
|
|
239
|
+
const data = JSON.parse(fileText);
|
|
240
|
+
return createResponse(data);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case 'PUT': {
|
|
244
|
+
const newData = await request.json();
|
|
245
|
+
await bucket.put(filename, JSON.stringify(newData));
|
|
246
|
+
return createResponse({ success: true });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'DELETE': {
|
|
250
|
+
const file = await bucket.get(filename);
|
|
251
|
+
if (!file) {
|
|
252
|
+
return createResponse({ error: 'File not found' }, 404);
|
|
253
|
+
}
|
|
254
|
+
await bucket.delete(filename);
|
|
255
|
+
return createResponse({ success: true });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
default:
|
|
259
|
+
return createResponse({ error: 'Method not allowed' }, 405);
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('Worker error:', error);
|
|
263
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
264
|
+
return createResponse({ error: errorMessage }, 500);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export interface WorkerSignatureEnvelope {
|
|
2
|
+
algorithm: string;
|
|
3
|
+
keyId: string;
|
|
4
|
+
signedAt: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function base64UrlEncode(value: Uint8Array): string {
|
|
9
|
+
let binary = '';
|
|
10
|
+
for (const byte of value) {
|
|
11
|
+
binary += String.fromCharCode(byte);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return btoa(binary)
|
|
15
|
+
.replace(/\+/g, '-')
|
|
16
|
+
.replace(/\//g, '_')
|
|
17
|
+
.replace(/=+$/g, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parsePkcs8PrivateKey(privateKey: string): ArrayBuffer {
|
|
21
|
+
const normalizedKey = privateKey
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/^['"]|['"]$/g, '')
|
|
24
|
+
.replace(/\\n/g, '\n');
|
|
25
|
+
|
|
26
|
+
const pemBody = normalizedKey
|
|
27
|
+
.replace('-----BEGIN PRIVATE KEY-----', '')
|
|
28
|
+
.replace('-----END PRIVATE KEY-----', '')
|
|
29
|
+
.replace(/\s+/g, '');
|
|
30
|
+
|
|
31
|
+
if (!pemBody) {
|
|
32
|
+
throw new Error('Manifest signing private key is invalid');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const binary = atob(pemBody);
|
|
36
|
+
const bytes = new Uint8Array(binary.length);
|
|
37
|
+
|
|
38
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
39
|
+
bytes[index] = binary.charCodeAt(index);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return bytes.buffer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function signPayload(
|
|
46
|
+
payload: string,
|
|
47
|
+
privateKey: string | undefined,
|
|
48
|
+
keyId: string | undefined,
|
|
49
|
+
algorithm: string,
|
|
50
|
+
missingSecretsError: string = 'Manifest signing secrets are not configured'
|
|
51
|
+
): Promise<WorkerSignatureEnvelope> {
|
|
52
|
+
if (!privateKey || !keyId) {
|
|
53
|
+
throw new Error(missingSecretsError);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const signingKey = await crypto.subtle.importKey(
|
|
57
|
+
'pkcs8',
|
|
58
|
+
parsePkcs8PrivateKey(privateKey),
|
|
59
|
+
{
|
|
60
|
+
name: 'RSASSA-PKCS1-v1_5',
|
|
61
|
+
hash: 'SHA-256'
|
|
62
|
+
},
|
|
63
|
+
false,
|
|
64
|
+
['sign']
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const signature = await crypto.subtle.sign(
|
|
68
|
+
{ name: 'RSASSA-PKCS1-v1_5' },
|
|
69
|
+
signingKey,
|
|
70
|
+
new TextEncoder().encode(payload)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
algorithm,
|
|
75
|
+
keyId,
|
|
76
|
+
signedAt: new Date().toISOString(),
|
|
77
|
+
value: base64UrlEncode(new Uint8Array(signature))
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
export interface ForensicManifestPayload {
|
|
2
|
+
dataHash: string;
|
|
3
|
+
imageHashes: { [filename: string]: string };
|
|
4
|
+
manifestHash: string;
|
|
5
|
+
totalFiles: number;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ConfirmationSignatureMetadata {
|
|
10
|
+
caseNumber: string;
|
|
11
|
+
exportDate: string;
|
|
12
|
+
exportedBy: string;
|
|
13
|
+
exportedByUid: string;
|
|
14
|
+
exportedByName: string;
|
|
15
|
+
exportedByCompany: string;
|
|
16
|
+
totalConfirmations: number;
|
|
17
|
+
version: string;
|
|
18
|
+
hash: string;
|
|
19
|
+
originalExportCreatedAt?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ConfirmationRecord {
|
|
23
|
+
fullName: string;
|
|
24
|
+
badgeId: string;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
confirmationId: string;
|
|
27
|
+
confirmedBy: string;
|
|
28
|
+
confirmedByEmail: string;
|
|
29
|
+
confirmedByCompany: string;
|
|
30
|
+
confirmedAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ConfirmationSigningPayload {
|
|
34
|
+
metadata: ConfirmationSignatureMetadata;
|
|
35
|
+
confirmations: Record<string, ConfirmationRecord[]>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type AuditExportFormat = 'csv' | 'json' | 'txt';
|
|
39
|
+
export type AuditExportType = 'entries' | 'trail' | 'report';
|
|
40
|
+
export type AuditExportScopeType = 'case' | 'user';
|
|
41
|
+
|
|
42
|
+
export interface AuditExportSigningPayload {
|
|
43
|
+
signatureVersion: string;
|
|
44
|
+
exportFormat: AuditExportFormat;
|
|
45
|
+
exportType: AuditExportType;
|
|
46
|
+
scopeType: AuditExportScopeType;
|
|
47
|
+
scopeIdentifier: string;
|
|
48
|
+
generatedAt: string;
|
|
49
|
+
totalEntries: number;
|
|
50
|
+
hash: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const FORENSIC_MANIFEST_VERSION = '2.0';
|
|
54
|
+
export const CONFIRMATION_SIGNATURE_VERSION = '2.0';
|
|
55
|
+
export const AUDIT_EXPORT_SIGNATURE_VERSION = '1.0';
|
|
56
|
+
export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-PKCS1-v1_5-SHA-256';
|
|
57
|
+
|
|
58
|
+
const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
|
|
59
|
+
|
|
60
|
+
function normalizeImageHashes(imageHashes: { [filename: string]: string }): { [filename: string]: string } {
|
|
61
|
+
const normalized: { [filename: string]: string } = {};
|
|
62
|
+
const sortedFilenames = Object.keys(imageHashes).sort();
|
|
63
|
+
|
|
64
|
+
for (const filename of sortedFilenames) {
|
|
65
|
+
normalized[filename] = imageHashes[filename].toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasValidConfirmationRecord(entry: Partial<ConfirmationRecord>): entry is ConfirmationRecord {
|
|
72
|
+
return (
|
|
73
|
+
typeof entry.fullName === 'string' &&
|
|
74
|
+
typeof entry.badgeId === 'string' &&
|
|
75
|
+
typeof entry.timestamp === 'string' &&
|
|
76
|
+
typeof entry.confirmationId === 'string' &&
|
|
77
|
+
typeof entry.confirmedBy === 'string' &&
|
|
78
|
+
typeof entry.confirmedByEmail === 'string' &&
|
|
79
|
+
typeof entry.confirmedByCompany === 'string' &&
|
|
80
|
+
typeof entry.confirmedAt === 'string' &&
|
|
81
|
+
!Number.isNaN(Date.parse(entry.confirmedAt))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isValidManifestPayload(candidate: Partial<ForensicManifestPayload>): candidate is ForensicManifestPayload {
|
|
86
|
+
if (!candidate) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof candidate.dataHash !== 'string' || !SHA256_HEX_REGEX.test(candidate.dataHash)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!candidate.imageHashes || typeof candidate.imageHashes !== 'object') {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const hash of Object.values(candidate.imageHashes)) {
|
|
99
|
+
if (typeof hash !== 'string' || !SHA256_HEX_REGEX.test(hash)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof candidate.manifestHash !== 'string' || !SHA256_HEX_REGEX.test(candidate.manifestHash)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof candidate.totalFiles !== 'number' || candidate.totalFiles <= 0) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof candidate.createdAt !== 'string' || Number.isNaN(Date.parse(candidate.createdAt))) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isValidConfirmationPayload(
|
|
120
|
+
candidate: Partial<ConfirmationSigningPayload>
|
|
121
|
+
): candidate is ConfirmationSigningPayload {
|
|
122
|
+
if (!candidate || !candidate.metadata || !candidate.confirmations) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const metadata = candidate.metadata;
|
|
127
|
+
if (
|
|
128
|
+
typeof metadata.caseNumber !== 'string' ||
|
|
129
|
+
typeof metadata.exportDate !== 'string' ||
|
|
130
|
+
typeof metadata.exportedBy !== 'string' ||
|
|
131
|
+
typeof metadata.exportedByUid !== 'string' ||
|
|
132
|
+
typeof metadata.exportedByName !== 'string' ||
|
|
133
|
+
typeof metadata.exportedByCompany !== 'string' ||
|
|
134
|
+
typeof metadata.totalConfirmations !== 'number' ||
|
|
135
|
+
metadata.totalConfirmations < 0 ||
|
|
136
|
+
typeof metadata.version !== 'string' ||
|
|
137
|
+
typeof metadata.hash !== 'string' ||
|
|
138
|
+
!SHA256_HEX_REGEX.test(metadata.hash)
|
|
139
|
+
) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (Number.isNaN(Date.parse(metadata.exportDate))) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
typeof metadata.originalExportCreatedAt === 'string' &&
|
|
149
|
+
Number.isNaN(Date.parse(metadata.originalExportCreatedAt))
|
|
150
|
+
) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const [imageId, confirmationList] of Object.entries(candidate.confirmations)) {
|
|
155
|
+
if (!imageId || !Array.isArray(confirmationList)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const record of confirmationList) {
|
|
160
|
+
if (!record || typeof record !== 'object' || !hasValidConfirmationRecord(record)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function isValidAuditExportPayload(
|
|
170
|
+
candidate: Partial<AuditExportSigningPayload>
|
|
171
|
+
): candidate is AuditExportSigningPayload {
|
|
172
|
+
if (!candidate) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (candidate.signatureVersion !== AUDIT_EXPORT_SIGNATURE_VERSION) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (candidate.exportFormat !== 'csv' && candidate.exportFormat !== 'json' && candidate.exportFormat !== 'txt') {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (candidate.exportType !== 'entries' && candidate.exportType !== 'trail' && candidate.exportType !== 'report') {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (candidate.scopeType !== 'case' && candidate.scopeType !== 'user') {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (typeof candidate.scopeIdentifier !== 'string' || candidate.scopeIdentifier.trim().length === 0) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof candidate.generatedAt !== 'string' || Number.isNaN(Date.parse(candidate.generatedAt))) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof candidate.totalEntries !== 'number' || candidate.totalEntries < 0) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (typeof candidate.hash !== 'string' || !SHA256_HEX_REGEX.test(candidate.hash)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createManifestSigningPayload(manifest: ForensicManifestPayload): string {
|
|
212
|
+
const canonicalPayload = {
|
|
213
|
+
manifestVersion: FORENSIC_MANIFEST_VERSION,
|
|
214
|
+
dataHash: manifest.dataHash.toLowerCase(),
|
|
215
|
+
imageHashes: normalizeImageHashes(manifest.imageHashes),
|
|
216
|
+
manifestHash: manifest.manifestHash.toLowerCase(),
|
|
217
|
+
totalFiles: manifest.totalFiles,
|
|
218
|
+
createdAt: manifest.createdAt
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return JSON.stringify(canonicalPayload);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeConfirmationEntries(entries: ConfirmationRecord[]): ConfirmationRecord[] {
|
|
225
|
+
return [...entries]
|
|
226
|
+
.map((entry) => ({
|
|
227
|
+
fullName: entry.fullName,
|
|
228
|
+
badgeId: entry.badgeId,
|
|
229
|
+
timestamp: entry.timestamp,
|
|
230
|
+
confirmationId: entry.confirmationId,
|
|
231
|
+
confirmedBy: entry.confirmedBy,
|
|
232
|
+
confirmedByEmail: entry.confirmedByEmail,
|
|
233
|
+
confirmedByCompany: entry.confirmedByCompany,
|
|
234
|
+
confirmedAt: entry.confirmedAt
|
|
235
|
+
}))
|
|
236
|
+
.sort((left, right) => {
|
|
237
|
+
const leftKey = `${left.confirmationId}|${left.confirmedAt}|${left.confirmedBy}`;
|
|
238
|
+
const rightKey = `${right.confirmationId}|${right.confirmedAt}|${right.confirmedBy}`;
|
|
239
|
+
return leftKey.localeCompare(rightKey);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeConfirmations(confirmations: Record<string, ConfirmationRecord[]>): Record<string, ConfirmationRecord[]> {
|
|
244
|
+
const normalized: Record<string, ConfirmationRecord[]> = {};
|
|
245
|
+
const sortedImageIds = Object.keys(confirmations).sort();
|
|
246
|
+
|
|
247
|
+
for (const imageId of sortedImageIds) {
|
|
248
|
+
normalized[imageId] = normalizeConfirmationEntries(confirmations[imageId] || []);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createConfirmationSigningPayload(confirmationData: ConfirmationSigningPayload): string {
|
|
255
|
+
const canonicalPayload = {
|
|
256
|
+
signatureVersion: CONFIRMATION_SIGNATURE_VERSION,
|
|
257
|
+
metadata: {
|
|
258
|
+
caseNumber: confirmationData.metadata.caseNumber,
|
|
259
|
+
exportDate: confirmationData.metadata.exportDate,
|
|
260
|
+
exportedBy: confirmationData.metadata.exportedBy,
|
|
261
|
+
exportedByUid: confirmationData.metadata.exportedByUid,
|
|
262
|
+
exportedByName: confirmationData.metadata.exportedByName,
|
|
263
|
+
exportedByCompany: confirmationData.metadata.exportedByCompany,
|
|
264
|
+
totalConfirmations: confirmationData.metadata.totalConfirmations,
|
|
265
|
+
version: confirmationData.metadata.version,
|
|
266
|
+
hash: confirmationData.metadata.hash.toUpperCase(),
|
|
267
|
+
...(confirmationData.metadata.originalExportCreatedAt
|
|
268
|
+
? { originalExportCreatedAt: confirmationData.metadata.originalExportCreatedAt }
|
|
269
|
+
: {})
|
|
270
|
+
},
|
|
271
|
+
confirmations: normalizeConfirmations(confirmationData.confirmations)
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return JSON.stringify(canonicalPayload);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function createAuditExportSigningPayload(auditExportData: AuditExportSigningPayload): string {
|
|
278
|
+
const canonicalPayload = {
|
|
279
|
+
signatureVersion: auditExportData.signatureVersion,
|
|
280
|
+
exportFormat: auditExportData.exportFormat,
|
|
281
|
+
exportType: auditExportData.exportType,
|
|
282
|
+
scopeType: auditExportData.scopeType,
|
|
283
|
+
scopeIdentifier: auditExportData.scopeIdentifier,
|
|
284
|
+
generatedAt: auditExportData.generatedAt,
|
|
285
|
+
totalEntries: auditExportData.totalEntries,
|
|
286
|
+
hash: auditExportData.hash.toUpperCase()
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return JSON.stringify(canonicalPayload);
|
|
290
|
+
}
|