@striae-org/striae 4.2.0 → 4.2.1
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/LICENSE +1 -1
- package/app/components/actions/case-manage.ts +50 -17
- package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
- package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +6 -2
- package/app/components/colors/colors.module.css +4 -3
- package/app/components/navbar/navbar.tsx +34 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +44 -70
- package/app/components/sidebar/cases/cases-modal.tsx +76 -35
- package/app/components/sidebar/cases/cases.module.css +20 -0
- package/app/components/sidebar/files/files-modal.tsx +37 -39
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +37 -74
- package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
- package/app/components/sidebar/notes/notes.module.css +27 -11
- package/app/components/sidebar/sidebar-container.tsx +1 -0
- package/app/components/sidebar/sidebar.tsx +3 -0
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useOverlayDismiss.ts +6 -4
- package/app/root.tsx +1 -1
- package/app/routes/striae/striae.tsx +6 -0
- package/app/services/audit/audit.service.ts +2 -2
- package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
- package/app/types/audit.ts +1 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
- package/app/utils/data/data-operations.ts +17 -861
- package/app/utils/data/index.ts +11 -1
- package/app/utils/data/operations/batch-operations.ts +113 -0
- package/app/utils/data/operations/case-operations.ts +168 -0
- package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
- package/app/utils/data/operations/file-annotation-operations.ts +196 -0
- package/app/utils/data/operations/index.ts +7 -0
- package/app/utils/data/operations/signing-operations.ts +225 -0
- package/app/utils/data/operations/types.ts +42 -0
- package/app/utils/data/operations/validation-operations.ts +48 -0
- package/app/utils/forensics/export-verification.ts +40 -111
- package/functions/api/_shared/firebase-auth.ts +2 -7
- package/functions/api/image/[[path]].ts +20 -23
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +5 -10
- package/scripts/deploy-primershear-emails.sh +1 -1
- package/worker-configuration.d.ts +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +16 -5
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +1 -7
- package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
- package/workers/pdf-worker/src/report-types.ts +3 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/user-worker.example.ts +17 -0
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/NOTICE +0 -13
- package/app/components/sidebar/notes/notes-modal.tsx +0 -52
- package/postcss.config.js +0 -6
- package/tailwind.config.ts +0 -22
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import firebaseConfig from '../../../app/config/firebase';
|
|
2
|
-
|
|
3
1
|
interface FirebaseJwtHeader {
|
|
4
2
|
alg?: string;
|
|
5
3
|
kid?: string;
|
|
@@ -31,8 +29,6 @@ const GOOGLE_SECURETOKEN_JWKS_URL =
|
|
|
31
29
|
'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com';
|
|
32
30
|
const DEFAULT_JWKS_CACHE_SECONDS = 300;
|
|
33
31
|
const CLOCK_SKEW_SECONDS = 300;
|
|
34
|
-
const FALLBACK_PROJECT_ID =
|
|
35
|
-
typeof firebaseConfig.projectId === 'string' ? firebaseConfig.projectId.trim() : '';
|
|
36
32
|
|
|
37
33
|
const textEncoder = new TextEncoder();
|
|
38
34
|
const textDecoder = new TextDecoder();
|
|
@@ -156,12 +152,11 @@ async function verifyTokenSignature(
|
|
|
156
152
|
|
|
157
153
|
function validateTokenClaims(payload: FirebaseJwtPayload, env: Env): boolean {
|
|
158
154
|
const configuredProjectId = typeof env.PROJECT_ID === 'string' ? env.PROJECT_ID.trim() : '';
|
|
159
|
-
|
|
160
|
-
if (allowedProjectIds.size === 0) {
|
|
155
|
+
if (configuredProjectId.length === 0) {
|
|
161
156
|
return false;
|
|
162
157
|
}
|
|
163
158
|
|
|
164
|
-
if (typeof payload.aud !== 'string' ||
|
|
159
|
+
if (typeof payload.aud !== 'string' || payload.aud !== configuredProjectId) {
|
|
165
160
|
return false;
|
|
166
161
|
}
|
|
167
162
|
|
|
@@ -30,44 +30,37 @@ function normalizeWorkerBaseUrl(workerDomain: string): string {
|
|
|
30
30
|
return `https://${trimmedDomain}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
type ProxyPathResult =
|
|
34
|
+
| { ok: true; path: string }
|
|
35
|
+
| { ok: false; reason: 'not-found' | 'bad-encoding' };
|
|
36
|
+
|
|
37
|
+
function extractProxyPath(url: URL): ProxyPathResult {
|
|
34
38
|
const routePrefix = '/api/image';
|
|
35
39
|
if (!url.pathname.startsWith(routePrefix)) {
|
|
36
|
-
return
|
|
40
|
+
return { ok: false, reason: 'not-found' };
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const remainder = url.pathname.slice(routePrefix.length);
|
|
40
44
|
if (remainder.length === 0) {
|
|
41
|
-
return '/';
|
|
45
|
+
return { ok: true, path: '/' };
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
const normalizedRemainder = remainder.startsWith('/') ? remainder : `/${remainder}`;
|
|
45
49
|
const encodedPath = normalizedRemainder.slice(1);
|
|
50
|
+
if (encodedPath.length === 0) {
|
|
51
|
+
return { ok: true, path: normalizedRemainder };
|
|
52
|
+
}
|
|
46
53
|
|
|
47
54
|
try {
|
|
48
55
|
const decodedPath = decodeURIComponent(encodedPath);
|
|
49
|
-
|
|
50
|
-
return decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
|
|
51
|
-
}
|
|
56
|
+
return { ok: true, path: decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}` };
|
|
52
57
|
} catch {
|
|
53
|
-
|
|
58
|
+
return { ok: false, reason: 'bad-encoding' };
|
|
54
59
|
}
|
|
55
|
-
|
|
56
|
-
return normalizedRemainder;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function resolveImageWorkerToken(env: Env): string {
|
|
60
|
-
|
|
61
|
-
if (imageToken.length > 0) {
|
|
62
|
-
return imageToken;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const apiToken = typeof env.API_TOKEN === 'string' ? env.API_TOKEN.trim() : '';
|
|
66
|
-
if (apiToken.length > 0) {
|
|
67
|
-
return apiToken;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return '';
|
|
63
|
+
return typeof env.IMAGES_API_TOKEN === 'string' ? env.IMAGES_API_TOKEN.trim() : '';
|
|
71
64
|
}
|
|
72
65
|
|
|
73
66
|
export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Response> => {
|
|
@@ -91,11 +84,15 @@ export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Re
|
|
|
91
84
|
}
|
|
92
85
|
|
|
93
86
|
const requestUrl = new URL(request.url);
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
96
|
-
return
|
|
87
|
+
const proxyPathResult = extractProxyPath(requestUrl);
|
|
88
|
+
if (!proxyPathResult.ok) {
|
|
89
|
+
return proxyPathResult.reason === 'bad-encoding'
|
|
90
|
+
? textResponse('Bad Request: malformed image path encoding', 400)
|
|
91
|
+
: textResponse('Not Found', 404);
|
|
97
92
|
}
|
|
98
93
|
|
|
94
|
+
const proxyPath = proxyPathResult.path;
|
|
95
|
+
|
|
99
96
|
const imageWorkerToken = resolveImageWorkerToken(env);
|
|
100
97
|
if (!env.IMAGES_WORKER_DOMAIN || !imageWorkerToken) {
|
|
101
98
|
return textResponse('Image service not configured', 502);
|
|
@@ -9,6 +9,10 @@ const SUPPORTED_METHODS = new Set(['POST', 'OPTIONS']);
|
|
|
9
9
|
const PRIMERSHEAR_FORMAT = 'primershear';
|
|
10
10
|
const DEFAULT_FORMAT = 'striae';
|
|
11
11
|
|
|
12
|
+
interface PdfProxyRequestBody {
|
|
13
|
+
data: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
function textResponse(message: string, status: number): Response {
|
|
13
17
|
return new Response(message, {
|
|
14
18
|
status,
|
|
@@ -48,6 +52,21 @@ function resolveReportFormat(email: string | null, primershearEmails: string): s
|
|
|
48
52
|
return allowed.includes(email.toLowerCase()) ? PRIMERSHEAR_FORMAT : DEFAULT_FORMAT;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
function parsePdfProxyRequestBody(payload: unknown): PdfProxyRequestBody | null {
|
|
56
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const record = payload as Record<string, unknown>;
|
|
61
|
+
if (!record.data || typeof record.data !== 'object' || Array.isArray(record.data)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
data: record.data as Record<string, unknown>
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
export const onRequest = async ({ request, env }: PdfProxyContext): Promise<Response> => {
|
|
52
71
|
if (!SUPPORTED_METHODS.has(request.method)) {
|
|
53
72
|
return textResponse('Method not allowed', 405);
|
|
@@ -103,15 +122,15 @@ export const onRequest = async ({ request, env }: PdfProxyContext): Promise<Resp
|
|
|
103
122
|
|
|
104
123
|
let upstreamBody: BodyInit;
|
|
105
124
|
try {
|
|
106
|
-
const payload = await request.json()
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
payload.reportFormat = reportFormat;
|
|
110
|
-
} else {
|
|
111
|
-
// Legacy flat payload shape
|
|
112
|
-
payload.reportFormat = reportFormat;
|
|
125
|
+
const payload = parsePdfProxyRequestBody(await request.json());
|
|
126
|
+
if (!payload) {
|
|
127
|
+
return textResponse('Invalid PDF request body', 400);
|
|
113
128
|
}
|
|
114
|
-
|
|
129
|
+
|
|
130
|
+
upstreamBody = JSON.stringify({
|
|
131
|
+
data: payload.data,
|
|
132
|
+
reportFormat
|
|
133
|
+
});
|
|
115
134
|
upstreamHeaders.set('Content-Type', 'application/json');
|
|
116
135
|
} catch {
|
|
117
136
|
return textResponse('Invalid request body', 400);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"app/entry.server.tsx",
|
|
45
45
|
"app/root.tsx",
|
|
46
46
|
"app/routes.ts",
|
|
47
|
-
"app/
|
|
47
|
+
"app/global.css",
|
|
48
48
|
"react-router.config.ts",
|
|
49
49
|
"load-context.ts",
|
|
50
50
|
"functions/",
|
|
@@ -64,14 +64,11 @@
|
|
|
64
64
|
".env.example",
|
|
65
65
|
"primershear.emails.example",
|
|
66
66
|
"firebase.json",
|
|
67
|
-
"postcss.config.js",
|
|
68
|
-
"tailwind.config.ts",
|
|
69
67
|
"tsconfig.json",
|
|
70
68
|
"vite.config.ts",
|
|
71
69
|
"worker-configuration.d.ts",
|
|
72
70
|
"wrangler.toml.example",
|
|
73
|
-
"LICENSE"
|
|
74
|
-
"NOTICE"
|
|
71
|
+
"LICENSE"
|
|
75
72
|
],
|
|
76
73
|
"sideEffects": false,
|
|
77
74
|
"type": "module",
|
|
@@ -129,15 +126,13 @@
|
|
|
129
126
|
"@types/react-dom": "^19.2.3",
|
|
130
127
|
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
131
128
|
"@typescript-eslint/parser": "^8.57.1",
|
|
132
|
-
"autoprefixer": "^10.4.27",
|
|
133
129
|
"eslint": "^9.39.4",
|
|
134
130
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
135
131
|
"eslint-plugin-import": "^2.32.0",
|
|
136
132
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
137
133
|
"eslint-plugin-react": "^7.37.5",
|
|
138
134
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
139
|
-
"
|
|
140
|
-
"tailwindcss": "^3.4.19",
|
|
135
|
+
"modern-normalize": "^3.0.1",
|
|
141
136
|
"typescript": "^5.9.3",
|
|
142
137
|
"vite": "^6.4.1",
|
|
143
138
|
"vite-tsconfig-paths": "^6.1.1",
|
|
@@ -157,4 +152,4 @@
|
|
|
157
152
|
"node": ">=20.0.0"
|
|
158
153
|
},
|
|
159
154
|
"packageManager": "npm@11.11.0"
|
|
160
|
-
}
|
|
155
|
+
}
|
|
@@ -82,7 +82,7 @@ fi
|
|
|
82
82
|
|
|
83
83
|
# Strip comment lines and blank lines, then join with commas
|
|
84
84
|
# Use || true to avoid failure if paste gets no input (handles empty file gracefully)
|
|
85
|
-
PRIMERSHEAR_EMAILS=$(grep -v '
|
|
85
|
+
PRIMERSHEAR_EMAILS=$(grep -v '^[[:space:]]*#' "$EMAILS_FILE" | grep -v '^[[:space:]]*$' | paste -sd ',' - || true)
|
|
86
86
|
|
|
87
87
|
if [ -z "$PRIMERSHEAR_EMAILS" ]; then
|
|
88
88
|
echo -e "${YELLOW}⚠️ primershear.emails contains no active email addresses.${NC}"
|
|
@@ -5485,7 +5485,7 @@ type AIGatewayHeaders = {
|
|
|
5485
5485
|
[key: string]: string | number | boolean | object;
|
|
5486
5486
|
};
|
|
5487
5487
|
type AIGatewayUniversalRequest = {
|
|
5488
|
-
provider: AIGatewayProviders | string;
|
|
5488
|
+
provider: AIGatewayProviders | string;
|
|
5489
5489
|
endpoint: string;
|
|
5490
5490
|
headers: Partial<AIGatewayHeaders>;
|
|
5491
5491
|
query: unknown;
|
|
@@ -5501,7 +5501,7 @@ declare abstract class AiGateway {
|
|
|
5501
5501
|
gateway?: UniversalGatewayOptions;
|
|
5502
5502
|
extraHeaders?: object;
|
|
5503
5503
|
}): Promise<Response>;
|
|
5504
|
-
getUrl(provider?: AIGatewayProviders | string): Promise<string>;
|
|
5504
|
+
getUrl(provider?: AIGatewayProviders | string): Promise<string>;
|
|
5505
5505
|
}
|
|
5506
5506
|
interface AutoRAGInternalError extends Error {
|
|
5507
5507
|
}
|
|
@@ -145,17 +145,28 @@ async function handleImageServing(request: Request, env: Env): Promise<Response>
|
|
|
145
145
|
|
|
146
146
|
const url = new URL(request.url);
|
|
147
147
|
const encodedPath = url.pathname.slice(1);
|
|
148
|
-
|
|
148
|
+
if (!encodedPath) {
|
|
149
|
+
return createResponse({ error: 'Image delivery URL is required' }, 400);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let decodedPath: string;
|
|
149
153
|
|
|
150
154
|
try {
|
|
151
155
|
decodedPath = decodeURIComponent(encodedPath);
|
|
152
156
|
} catch {
|
|
153
|
-
|
|
157
|
+
return createResponse({ error: 'Image delivery URL must be URL-encoded' }, 400);
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
let imageDeliveryURL: URL;
|
|
161
|
+
try {
|
|
162
|
+
imageDeliveryURL = new URL(decodedPath);
|
|
163
|
+
} catch {
|
|
164
|
+
return createResponse({ error: 'Image delivery URL is invalid' }, 400);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (imageDeliveryURL.protocol !== 'https:' || imageDeliveryURL.hostname !== 'imagedelivery.net') {
|
|
168
|
+
return createResponse({ error: 'Image delivery URL must target imagedelivery.net over HTTPS' }, 400);
|
|
169
|
+
}
|
|
159
170
|
|
|
160
171
|
return generateSignedUrl(imageDeliveryURL, env);
|
|
161
172
|
}
|
|
@@ -63,12 +63,6 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
63
63
|
return luminance < 0.5;
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
// Use passed currentDate or generate fallback
|
|
67
|
-
const displayDate = currentDate || (() => {
|
|
68
|
-
const now = new Date();
|
|
69
|
-
return `${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getDate().toString().padStart(2, '0')}/${now.getFullYear()}`;
|
|
70
|
-
})();
|
|
71
|
-
|
|
72
66
|
return `
|
|
73
67
|
<!DOCTYPE html>
|
|
74
68
|
<html lang="en">
|
|
@@ -405,7 +399,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
405
399
|
<div class="content-wrapper">
|
|
406
400
|
<div class="header">
|
|
407
401
|
<div class="header-content">
|
|
408
|
-
<div class="date">${
|
|
402
|
+
<div class="date">${currentDate}</div>
|
|
409
403
|
${caseNumber ? `<div class="case-number">${caseNumber}</div>` : '<div class="case-number"></div>'}
|
|
410
404
|
</div>
|
|
411
405
|
</div>
|
|
@@ -3,13 +3,10 @@ import type { PDFGenerationData, PDFGenerationRequest, ReportModule } from './re
|
|
|
3
3
|
interface Env {
|
|
4
4
|
BROWSER: Fetcher;
|
|
5
5
|
PDF_WORKER_AUTH: string;
|
|
6
|
-
ACCOUNT_ID
|
|
7
|
-
|
|
8
|
-
BROWSER_API_TOKEN?: string;
|
|
9
|
-
API_TOKEN?: string;
|
|
6
|
+
ACCOUNT_ID: string;
|
|
7
|
+
BROWSER_API_TOKEN: string;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
const DEFAULT_REPORT_FORMAT = 'striae';
|
|
13
10
|
const BROWSER_PDF_TIMEOUT_MS = 90_000;
|
|
14
11
|
const BROWSER_RENDERING_API_BASE = 'https://api.cloudflare.com/client/v4/accounts';
|
|
15
12
|
|
|
@@ -56,62 +53,47 @@ function jsonResponse(body: unknown, status: number): Response {
|
|
|
56
53
|
});
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
class MissingSecretError extends Error {
|
|
57
|
+
readonly secretKey: string;
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
constructor(key: string) {
|
|
60
|
+
super(`Worker is missing required secret: ${key}`);
|
|
61
|
+
this.name = 'MissingSecretError';
|
|
62
|
+
this.secretKey = key;
|
|
66
63
|
}
|
|
67
|
-
|
|
68
|
-
return '';
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
function
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
76
|
-
return candidate.trim();
|
|
77
|
-
}
|
|
66
|
+
function getRequiredSecret(value: string, name: string): string {
|
|
67
|
+
const normalized = value.trim();
|
|
68
|
+
if (!normalized) {
|
|
69
|
+
throw new MissingSecretError(name);
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
return
|
|
72
|
+
return normalized;
|
|
81
73
|
}
|
|
82
74
|
|
|
83
|
-
function
|
|
84
|
-
if (typeof
|
|
85
|
-
return DEFAULT_REPORT_FORMAT;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const normalized = format.trim().toLowerCase();
|
|
89
|
-
return normalized || DEFAULT_REPORT_FORMAT;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function resolveReportRequest(payload: unknown): { reportFormat: string; data: PDFGenerationData } {
|
|
93
|
-
if (!payload || typeof payload !== 'object') {
|
|
75
|
+
function resolveReportRequest(payload: unknown): PDFGenerationRequest {
|
|
76
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
94
77
|
throw new Error('Request body must be a JSON object');
|
|
95
78
|
}
|
|
96
79
|
|
|
97
80
|
const record = payload as Record<string, unknown>;
|
|
98
|
-
|
|
81
|
+
if (typeof record.reportFormat !== 'string' || record.reportFormat.trim().length === 0) {
|
|
82
|
+
throw new Error('Request body must include a non-empty reportFormat');
|
|
83
|
+
}
|
|
99
84
|
|
|
100
|
-
if (record.data
|
|
101
|
-
|
|
102
|
-
reportFormat,
|
|
103
|
-
data: record.data as PDFGenerationData,
|
|
104
|
-
};
|
|
85
|
+
if (!record.data || typeof record.data !== 'object' || Array.isArray(record.data)) {
|
|
86
|
+
throw new Error('Request body must include a data object');
|
|
105
87
|
}
|
|
106
88
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
89
|
+
const data = record.data as Record<string, unknown>;
|
|
90
|
+
if (typeof data.currentDate !== 'string' || data.currentDate.trim().length === 0) {
|
|
91
|
+
throw new Error('Request body data must include a non-empty currentDate');
|
|
92
|
+
}
|
|
111
93
|
|
|
112
94
|
return {
|
|
113
|
-
reportFormat,
|
|
114
|
-
data:
|
|
95
|
+
reportFormat: record.reportFormat.trim().toLowerCase(),
|
|
96
|
+
data: record.data as PDFGenerationData,
|
|
115
97
|
};
|
|
116
98
|
}
|
|
117
99
|
|
|
@@ -128,19 +110,8 @@ async function renderReport(reportFormat: string, data: PDFGenerationData): Prom
|
|
|
128
110
|
}
|
|
129
111
|
|
|
130
112
|
async function renderPdfViaRestEndpoint(env: Env, html: string): Promise<Response> {
|
|
131
|
-
const accountId =
|
|
132
|
-
const browserApiToken =
|
|
133
|
-
|
|
134
|
-
if (!accountId || !browserApiToken) {
|
|
135
|
-
return jsonResponse(
|
|
136
|
-
{
|
|
137
|
-
error: 'Missing required Browser Rendering credentials',
|
|
138
|
-
requiredSecrets: ['ACCOUNT_ID', 'BROWSER_API_TOKEN'],
|
|
139
|
-
note: 'Set ACCOUNT_ID and a Browser Rendering - Edit token (BROWSER_API_TOKEN) on this worker.',
|
|
140
|
-
},
|
|
141
|
-
502
|
|
142
|
-
);
|
|
143
|
-
}
|
|
113
|
+
const accountId = getRequiredSecret(env.ACCOUNT_ID, 'ACCOUNT_ID');
|
|
114
|
+
const browserApiToken = getRequiredSecret(env.BROWSER_API_TOKEN, 'BROWSER_API_TOKEN');
|
|
144
115
|
|
|
145
116
|
const endpoint = `${BROWSER_RENDERING_API_BASE}/${accountId}/browser-rendering/pdf`;
|
|
146
117
|
const requestBody = JSON.stringify({
|
|
@@ -217,12 +188,20 @@ export default {
|
|
|
217
188
|
|
|
218
189
|
if (request.method === 'POST') {
|
|
219
190
|
try {
|
|
220
|
-
const payload = await request.json() as
|
|
191
|
+
const payload = await request.json() as unknown;
|
|
221
192
|
const { reportFormat, data } = resolveReportRequest(payload);
|
|
222
193
|
const document = await renderReport(reportFormat, data);
|
|
223
194
|
|
|
224
195
|
return await renderPdfViaRestEndpoint(env, document);
|
|
225
196
|
} catch (error) {
|
|
197
|
+
if (error instanceof MissingSecretError) {
|
|
198
|
+
console.error(`[pdf-worker] Configuration error: ${error.message}`);
|
|
199
|
+
return jsonResponse(
|
|
200
|
+
{ error: 'Worker configuration error', missing_secret: error.secretKey },
|
|
201
|
+
502
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
226
205
|
if (isTimeoutError(error)) {
|
|
227
206
|
const timeoutMessage = error instanceof Error ? error.message : 'PDF generation timed out';
|
|
228
207
|
return jsonResponse({ error: timeoutMessage }, 504);
|
|
@@ -52,7 +52,7 @@ export interface PDFGenerationData {
|
|
|
52
52
|
caseNumber?: string;
|
|
53
53
|
annotationData?: AnnotationData;
|
|
54
54
|
activeAnnotations?: string[];
|
|
55
|
-
currentDate
|
|
55
|
+
currentDate: string;
|
|
56
56
|
notesUpdatedFormatted?: string;
|
|
57
57
|
userCompany?: string;
|
|
58
58
|
userFirstName?: string;
|
|
@@ -61,8 +61,8 @@ export interface PDFGenerationData {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export interface PDFGenerationRequest {
|
|
64
|
-
reportFormat
|
|
65
|
-
data
|
|
64
|
+
reportFormat: string;
|
|
65
|
+
data: PDFGenerationData;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export type ReportRenderer = (data: PDFGenerationData) => string;
|
|
@@ -433,6 +433,22 @@ async function deleteSingleCase(env: Env, userUid: string, caseNumber: string):
|
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
async function deleteUserConfirmationSummary(env: Env, userUid: string): Promise<void> {
|
|
437
|
+
const dataApiKey = env.R2_KEY_SECRET;
|
|
438
|
+
const dataWorkerBaseUrl = resolveDataWorkerBaseUrl(env);
|
|
439
|
+
const encodedUserId = encodeURIComponent(userUid);
|
|
440
|
+
const confirmationSummaryPath = `${dataWorkerBaseUrl}/${encodedUserId}/meta/confirmation-status.json`;
|
|
441
|
+
|
|
442
|
+
const response = await fetch(confirmationSummaryPath, {
|
|
443
|
+
method: 'DELETE',
|
|
444
|
+
headers: { 'X-Custom-Auth-Key': dataApiKey }
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (!response.ok && response.status !== 404) {
|
|
448
|
+
throw new Error(`Failed to delete confirmation summary metadata: ${response.status}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
436
452
|
async function executeUserDeletion(
|
|
437
453
|
env: Env,
|
|
438
454
|
userUid: string,
|
|
@@ -490,6 +506,7 @@ async function executeUserDeletion(
|
|
|
490
506
|
throw new Error(`Failed to fully delete all case data: ${caseCleanupErrors.join(' | ')}`);
|
|
491
507
|
}
|
|
492
508
|
|
|
509
|
+
await deleteUserConfirmationSummary(env, userUid);
|
|
493
510
|
await deleteFirebaseAuthUser(env, userUid);
|
|
494
511
|
|
|
495
512
|
// Delete the user account from the database
|
package/wrangler.toml.example
CHANGED
package/NOTICE
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
NOTICE
|
|
2
|
-
|
|
3
|
-
Striae – A Firearms Examiner’s Comparison Companion
|
|
4
|
-
|
|
5
|
-
Striae © 2025. All rights reserved.
|
|
6
|
-
|
|
7
|
-
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License in the repository’s LICENSE file.
|
|
8
|
-
|
|
9
|
-
Attributions for bundled components:
|
|
10
|
-
|
|
11
|
-
Portions of this product may include open-source software licensed under their respective licenses, including but not limited to: Remix, Cloudflare Workers, Firebase, Tailwind CSS, Vite, and other npm packages. License texts for third-party components are provided in their respective source files or in node_modules as applicable.
|
|
12
|
-
|
|
13
|
-
No trademark rights are granted for “Striae” or any associated names or logos.
|