@striae-org/striae 4.0.1 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.env.example +1 -0
  2. package/app/components/actions/case-export/data-processing.ts +1 -1
  3. package/app/components/actions/case-export/download-handlers.ts +4 -3
  4. package/app/components/actions/case-export/metadata-helpers.ts +1 -1
  5. package/app/components/actions/case-import/confirmation-import.ts +1 -1
  6. package/app/components/actions/case-import/image-operations.ts +1 -1
  7. package/app/components/actions/case-import/orchestrator.ts +1 -1
  8. package/app/components/actions/case-import/storage-operations.ts +3 -3
  9. package/app/components/actions/case-import/validation.ts +3 -4
  10. package/app/components/actions/case-import/zip-processing.ts +1 -1
  11. package/app/components/actions/case-manage.ts +3 -5
  12. package/app/components/actions/confirm-export.ts +4 -5
  13. package/app/components/actions/generate-pdf.ts +1 -1
  14. package/app/components/actions/image-manage.ts +2 -3
  15. package/app/components/actions/notes-manage.ts +1 -1
  16. package/app/components/actions/signout.tsx +1 -1
  17. package/app/components/audit/user-audit-viewer.tsx +1 -1
  18. package/app/components/auth/auth-provider.tsx +1 -1
  19. package/app/components/auth/mfa-verification.tsx +1 -1
  20. package/app/components/button/button.tsx +1 -1
  21. package/app/components/canvas/box-annotations/box-annotations.tsx +1 -1
  22. package/app/components/canvas/confirmation/confirmation.tsx +1 -1
  23. package/app/components/icon/icon.tsx +1 -1
  24. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +1 -1
  25. package/app/components/sidebar/case-export/case-export.tsx +1 -1
  26. package/app/components/sidebar/cases/case-sidebar.tsx +3 -3
  27. package/app/components/sidebar/cases/cases-modal.tsx +1 -1
  28. package/app/components/sidebar/files/files-modal.tsx +1 -1
  29. package/app/components/sidebar/notes/notes-sidebar.tsx +1 -1
  30. package/app/components/sidebar/sidebar-container.tsx +2 -17
  31. package/app/components/sidebar/sidebar.module.css +0 -29
  32. package/app/components/theme-provider/theme-provider.tsx +1 -1
  33. package/app/components/theme-provider/theme.ts +1 -1
  34. package/app/components/user/delete-account.tsx +1 -1
  35. package/app/components/user/manage-profile.tsx +1 -1
  36. package/app/components/user/mfa-phone-update.tsx +1 -1
  37. package/app/root.tsx +18 -51
  38. package/app/routes/auth/emailActionHandler.tsx +1 -2
  39. package/app/routes/auth/emailVerification.tsx +1 -1
  40. package/app/routes/auth/login.tsx +3 -5
  41. package/app/routes/auth/passwordReset.tsx +1 -1
  42. package/app/routes/striae/striae.tsx +2 -2
  43. package/app/services/audit/audit-export-signing.ts +2 -2
  44. package/app/services/audit/audit-export.service.ts +1 -2
  45. package/app/services/audit/audit.service.ts +1 -1
  46. package/app/services/firebase/index.ts +1 -1
  47. package/app/utils/api/index.ts +4 -0
  48. package/app/utils/auth/index.ts +5 -0
  49. package/app/utils/common/index.ts +3 -0
  50. package/app/utils/{version.ts → common/version.ts} +1 -1
  51. package/app/utils/{data-operations.ts → data/data-operations.ts} +4 -4
  52. package/app/utils/data/index.ts +2 -0
  53. package/app/utils/{permissions.ts → data/permissions.ts} +1 -1
  54. package/app/utils/forensics/index.ts +5 -0
  55. package/app/utils/ui/index.ts +2 -0
  56. package/package.json +18 -20
  57. package/public/.well-known/keybase.txt +56 -0
  58. package/scripts/deploy-config.sh +2 -0
  59. package/scripts/deploy-worker-secrets.sh +1 -2
  60. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  61. package/workers/data-worker/wrangler.jsonc.example +1 -1
  62. package/workers/image-worker/wrangler.jsonc.example +1 -1
  63. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  64. package/workers/pdf-worker/src/pdf-worker.example.ts +144 -39
  65. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  66. package/workers/user-worker/wrangler.jsonc.example +1 -1
  67. package/wrangler.toml.example +1 -1
  68. package/public/.well-known/publickey.info@striae.org.asc +0 -17
  69. package/public/oin-badge.png +0 -0
  70. /package/app/utils/{data-api-client.ts → api/data-api-client.ts} +0 -0
  71. /package/app/utils/{image-api-client.ts → api/image-api-client.ts} +0 -0
  72. /package/app/utils/{pdf-api-client.ts → api/pdf-api-client.ts} +0 -0
  73. /package/app/utils/{user-api-client.ts → api/user-api-client.ts} +0 -0
  74. /package/app/utils/{auth-action-settings.ts → auth/auth-action-settings.ts} +0 -0
  75. /package/app/utils/{auth.ts → auth/auth.ts} +0 -0
  76. /package/app/utils/{mfa-phone.ts → auth/mfa-phone.ts} +0 -0
  77. /package/app/utils/{mfa.ts → auth/mfa.ts} +0 -0
  78. /package/app/utils/{password-policy.ts → auth/password-policy.ts} +0 -0
  79. /package/app/utils/{batch-operations.ts → common/batch-operations.ts} +0 -0
  80. /package/app/utils/{id-generator.ts → common/id-generator.ts} +0 -0
  81. /package/app/utils/{SHA256.ts → forensics/SHA256.ts} +0 -0
  82. /package/app/utils/{audit-export-signature.ts → forensics/audit-export-signature.ts} +0 -0
  83. /package/app/utils/{confirmation-signature.ts → forensics/confirmation-signature.ts} +0 -0
  84. /package/app/utils/{export-verification.ts → forensics/export-verification.ts} +0 -0
  85. /package/app/utils/{signature-utils.ts → forensics/signature-utils.ts} +0 -0
  86. /package/app/utils/{annotation-timestamp.ts → ui/annotation-timestamp.ts} +0 -0
  87. /package/app/utils/{style.ts → ui/style.ts} +0 -0
@@ -9,7 +9,7 @@ import type {
9
9
  AuditResult,
10
10
  PerformanceMetrics
11
11
  } from '~/types';
12
- import { generateWorkflowId } from '../../utils/id-generator';
12
+ import { generateWorkflowId } from '~/utils/common';
13
13
  import {
14
14
  fetchAuditEntriesForUser,
15
15
  persistAuditEntryForUser
@@ -6,7 +6,7 @@ import {
6
6
  //connectAuthEmulator,
7
7
  } from 'firebase/auth';
8
8
  import firebaseConfig from '~/config/firebase';
9
- import { getAppVersion } from '~/utils/version';
9
+ import { getAppVersion } from '~/utils/common';
10
10
 
11
11
  export const app = initializeApp(firebaseConfig, "Striae");
12
12
  export const auth = getAuth(app);
@@ -0,0 +1,4 @@
1
+ export * from './data-api-client';
2
+ export * from './image-api-client';
3
+ export * from './pdf-api-client';
4
+ export * from './user-api-client';
@@ -0,0 +1,5 @@
1
+ export * from './auth';
2
+ export * from './auth-action-settings';
3
+ export * from './mfa';
4
+ export * from './mfa-phone';
5
+ export * from './password-policy';
@@ -0,0 +1,3 @@
1
+ export * from './batch-operations';
2
+ export * from './id-generator';
3
+ export * from './version';
@@ -1,4 +1,4 @@
1
- import packageJson from '../../package.json';
1
+ import packageJson from '../../../package.json';
2
2
 
3
3
  export const getAppVersion = () => {
4
4
  return packageJson.version;
@@ -6,19 +6,19 @@
6
6
 
7
7
  import type { User } from 'firebase/auth';
8
8
  import { type CaseData, type AnnotationData, type ConfirmationImportData } from '~/types';
9
- import { fetchDataApi } from './data-api-client';
9
+ import { fetchDataApi } from '../api';
10
10
  import { validateUserSession, canAccessCase, canModifyCase } from './permissions';
11
11
  import {
12
12
  type ForensicManifestData,
13
13
  type ForensicManifestSignature,
14
14
  FORENSIC_MANIFEST_VERSION
15
- } from './SHA256';
16
- import { CONFIRMATION_SIGNATURE_VERSION } from './confirmation-signature';
15
+ } from '../forensics/SHA256';
16
+ import { CONFIRMATION_SIGNATURE_VERSION } from '../forensics/confirmation-signature';
17
17
  import {
18
18
  AUDIT_EXPORT_SIGNATURE_VERSION,
19
19
  type AuditExportSigningPayload,
20
20
  isValidAuditExportSigningPayload
21
- } from './audit-export-signature';
21
+ } from '../forensics/audit-export-signature';
22
22
 
23
23
  // ============================================================================
24
24
  // INTERFACES AND TYPES
@@ -0,0 +1,2 @@
1
+ export * from './data-operations';
2
+ export * from './permissions';
@@ -1,7 +1,7 @@
1
1
  import type { User } from 'firebase/auth';
2
2
  import type { UserData, ExtendedUserData, UserLimits, ReadOnlyCaseMetadata } from '~/types';
3
3
  import paths from '~/config/config.json';
4
- import { fetchUserApi } from './user-api-client';
4
+ import { fetchUserApi } from '../api';
5
5
 
6
6
  const MAX_CASES_REVIEW = paths.max_cases_review;
7
7
  const MAX_FILES_PER_CASE_REVIEW = paths.max_files_per_case_review;
@@ -0,0 +1,5 @@
1
+ export * from './SHA256';
2
+ export * from './audit-export-signature';
3
+ export * from './confirmation-signature';
4
+ export * from './export-verification';
5
+ export * from './signature-utils';
@@ -0,0 +1,2 @@
1
+ export * from './annotation-timestamp';
2
+ export * from './style';
package/package.json CHANGED
@@ -1,20 +1,18 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
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",
7
- "homepage": "https://striae.app",
7
+ "homepage": "https://github.com/striae-org/striae/wiki",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/striae-org/striae.git"
11
11
  },
12
- "funding": [
13
- {
14
- "type": "patreon",
15
- "url": "https://www.patreon.com/striae"
16
- }
17
- ],
12
+ "funding": {
13
+ "type": "github",
14
+ "url": "https://github.com/sponsors/striae-org"
15
+ },
18
16
  "bugs": {
19
17
  "url": "https://github.com/striae-org/striae/issues"
20
18
  },
@@ -114,7 +112,7 @@
114
112
  "@react-router/cloudflare": "^7.13.1",
115
113
  "exceljs": "^4.4.0",
116
114
  "firebase": "^12.10.0",
117
- "isbot": "^5.1.35",
115
+ "isbot": "^5.1.36",
118
116
  "jszip": "^3.10.1",
119
117
  "react": "^19.2.4",
120
118
  "react-dom": "^19.2.4",
@@ -123,23 +121,23 @@
123
121
  "devDependencies": {
124
122
  "@react-router/dev": "^7.13.1",
125
123
  "@react-router/fs-routes": "^7.13.1",
126
- "@types/react": "^19.1.10",
127
- "@types/react-dom": "^19.1.7",
128
- "@typescript-eslint/eslint-plugin": "^8.54.0",
129
- "@typescript-eslint/parser": "^8.56.1",
124
+ "@types/react": "^19.2.14",
125
+ "@types/react-dom": "^19.2.3",
126
+ "@typescript-eslint/eslint-plugin": "^8.57.1",
127
+ "@typescript-eslint/parser": "^8.57.1",
130
128
  "autoprefixer": "^10.4.27",
131
- "eslint": "^9.39.2",
129
+ "eslint": "^9.39.4",
132
130
  "eslint-import-resolver-typescript": "^4.4.4",
133
131
  "eslint-plugin-import": "^2.32.0",
134
- "eslint-plugin-jsx-a11y": "^6.7.1",
135
- "eslint-plugin-react": "^7.37.4",
132
+ "eslint-plugin-jsx-a11y": "^6.10.2",
133
+ "eslint-plugin-react": "^7.37.5",
136
134
  "eslint-plugin-react-hooks": "^7.0.1",
137
- "postcss": "^8.5.6",
138
- "tailwindcss": "^3.4.0",
135
+ "postcss": "^8.5.8",
136
+ "tailwindcss": "^3.4.19",
139
137
  "typescript": "^5.9.3",
140
138
  "vite": "^6.4.1",
141
139
  "vite-tsconfig-paths": "^6.1.1",
142
- "wrangler": "^4.73.0"
140
+ "wrangler": "^4.74.0"
143
141
  },
144
142
  "overrides": {
145
143
  "tar": "7.5.11",
@@ -155,4 +153,4 @@
155
153
  "node": ">=20.0.0"
156
154
  },
157
155
  "packageManager": "npm@11.11.0"
158
- }
156
+ }
@@ -0,0 +1,56 @@
1
+ ==================================================================
2
+ https://keybase.io/stephenjlu
3
+ --------------------------------------------------------------------
4
+
5
+ I hereby claim:
6
+
7
+ * I am an admin of https://striae.app
8
+ * I am stephenjlu (https://keybase.io/stephenjlu) on keybase.
9
+ * I have a public key ASAKWYuLxhqhdePAuDulLzWWUusZk7mQi-1lMyjF8lsSbgo
10
+
11
+ To do so, I am signing this object:
12
+
13
+ {
14
+ "body": {
15
+ "key": {
16
+ "eldest_kid": "01200a598b8bc61aa175e3c0b83ba52f359652eb1993b9908bed653328c5f25b126e0a",
17
+ "host": "keybase.io",
18
+ "kid": "01200a598b8bc61aa175e3c0b83ba52f359652eb1993b9908bed653328c5f25b126e0a",
19
+ "uid": "ef43479353eb3b8be30c76fd0919c219",
20
+ "username": "stephenjlu"
21
+ },
22
+ "merkle_root": {
23
+ "ctime": 1773711528,
24
+ "hash": "9519e4709ef4b2ab7ea27c6633fd91e00989841d63cf53dd15500ccbec43152fdb37efd0cefcae90edcae7d84e2584620026334e98ef2feb36241c6aa38aca11",
25
+ "hash_meta": "a8ed0ac295c8a433c99b4606da6e569399fae2b052a5f8d053a2d735d110b265",
26
+ "seqno": 27471249
27
+ },
28
+ "service": {
29
+ "entropy": "e8nGKp/3/msv+czv8R/pIZOA",
30
+ "hostname": "striae.app",
31
+ "protocol": "https:"
32
+ },
33
+ "type": "web_service_binding",
34
+ "version": 2
35
+ },
36
+ "client": {
37
+ "name": "keybase.io go client",
38
+ "version": "6.6.0"
39
+ },
40
+ "ctime": 1773711539,
41
+ "expire_in": 504576000,
42
+ "prev": "20fa9a69a64b9008e2ba1d18171c4eda04d49fc68c2b419cf5156b78faa8b75b",
43
+ "seqno": 26,
44
+ "tag": "signature"
45
+ }
46
+
47
+ which yields the signature:
48
+
49
+ hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEgClmLi8YaoXXjwLg7pS81llLrGZO5kIvtZTMoxfJbEm4Kp3BheWxvYWTESpcCGsQgIPqaaaZLkAjiuh0YFxxO2gTUn8aMK0Gc9RVrePqot1vEIOlO7j2uG/S+PR0TZqsul0pFPx7wKWbK+YYm6cWb9OQYAgHCo3NpZ8RAUVB1DF+DBdsDrp5BVL0eqUuueayhIrABHB63O9f9e03e8MJpZUAWdv8r7eLRcOoSf5p3I+CtsDgeyklUXy6oCKhzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEIH2d3PmZB6Yydgvkp783wyinfEsZBLRlgM2WXItXvkvTo3RhZ80CAqd2ZXJzaW9uAQ==
50
+
51
+ And finally, I am proving ownership of this host by posting or
52
+ appending to this document.
53
+
54
+ View my publicly-auditable identity here: https://keybase.io/stephenjlu
55
+
56
+ ==================================================================
@@ -559,6 +559,7 @@ required_vars=(
559
559
  "PDF_WORKER_AUTH"
560
560
  "ACCOUNT_HASH"
561
561
  "API_TOKEN"
562
+ "BROWSER_API_TOKEN"
562
563
  "HMAC_KEY"
563
564
  "MANIFEST_SIGNING_PRIVATE_KEY"
564
565
  "MANIFEST_SIGNING_KEY_ID"
@@ -1284,6 +1285,7 @@ prompt_for_secrets() {
1284
1285
  prompt_for_var "PDF_WORKER_AUTH" "PDF worker authentication token (generate with: openssl rand -hex 16)"
1285
1286
  prompt_for_var "ACCOUNT_HASH" "Cloudflare Images Account Hash"
1286
1287
  prompt_for_var "API_TOKEN" "Cloudflare Images API token (for Images Worker)"
1288
+ prompt_for_var "BROWSER_API_TOKEN" "Cloudflare Browser Rendering API token (for PDF Worker)"
1287
1289
  prompt_for_var "HMAC_KEY" "Cloudflare Images HMAC signing key"
1288
1290
 
1289
1291
  configure_manifest_signing_credentials
@@ -197,10 +197,9 @@ if ! set_worker_secrets "Images Worker" "workers/image-worker" \
197
197
  echo -e "${YELLOW}⚠️ Skipping Images Worker (not configured)${NC}"
198
198
  fi
199
199
 
200
- # PDF Worker (no secrets needed)
201
200
  # PDF Worker
202
201
  if ! set_worker_secrets "PDF Worker" "workers/pdf-worker" \
203
- "PDF_WORKER_AUTH"; then
202
+ "PDF_WORKER_AUTH" "ACCOUNT_ID" "BROWSER_API_TOKEN"; then
204
203
  echo -e "${YELLOW}⚠️ Skipping PDF Worker (not configured)${NC}"
205
204
  fi
206
205
 
@@ -2,7 +2,7 @@
2
2
  "name": "AUDIT_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/audit-worker.ts",
5
- "compatibility_date": "2026-03-15",
5
+ "compatibility_date": "2026-03-17",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -3,7 +3,7 @@
3
3
  "name": "DATA_WORKER_NAME",
4
4
  "account_id": "ACCOUNT_ID",
5
5
  "main": "src/data-worker.ts",
6
- "compatibility_date": "2026-03-15",
6
+ "compatibility_date": "2026-03-17",
7
7
  "compatibility_flags": [
8
8
  "nodejs_compat"
9
9
  ],
@@ -2,7 +2,7 @@
2
2
  "name": "IMAGES_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/image-worker.ts",
5
- "compatibility_date": "2026-03-15",
5
+ "compatibility_date": "2026-03-17",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -2,7 +2,7 @@
2
2
  "name": "KEYS_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/keys.ts",
5
- "compatibility_date": "2026-03-15",
5
+ "compatibility_date": "2026-03-17",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,12 +1,28 @@
1
- import { launch } from "@cloudflare/puppeteer";
2
1
  import type { PDFGenerationData, PDFGenerationRequest, ReportModule } from './report-types';
3
2
 
4
3
  interface Env {
5
4
  BROWSER: Fetcher;
6
5
  PDF_WORKER_AUTH: string;
6
+ ACCOUNT_ID?: string;
7
+ CLOUDFLARE_ACCOUNT_ID?: string;
8
+ BROWSER_API_TOKEN?: string;
9
+ API_TOKEN?: string;
7
10
  }
8
11
 
9
12
  const DEFAULT_REPORT_FORMAT = 'striae';
13
+ const BROWSER_PDF_TIMEOUT_MS = 90_000;
14
+ const BROWSER_RENDERING_API_BASE = 'https://api.cloudflare.com/client/v4/accounts';
15
+
16
+ const DEFAULT_PDF_OPTIONS = {
17
+ printBackground: true,
18
+ format: 'letter',
19
+ margin: {
20
+ top: '0.5in',
21
+ bottom: '0.5in',
22
+ left: '0.5in',
23
+ right: '0.5in',
24
+ },
25
+ };
10
26
 
11
27
  const reportModuleLoaders: Record<string, () => Promise<ReportModule>> = {
12
28
  // Default Striae report format module
@@ -22,6 +38,45 @@ const corsHeaders: Record<string, string> = {
22
38
  const hasValidHeader = (request: Request, env: Env): boolean =>
23
39
  request.headers.get('X-Custom-Auth-Key') === env.PDF_WORKER_AUTH;
24
40
 
41
+ function isTimeoutError(error: unknown): boolean {
42
+ return error instanceof Error && (
43
+ error.name === 'AbortError' ||
44
+ error.name === 'TimeoutError' ||
45
+ /timed out/i.test(error.message)
46
+ );
47
+ }
48
+
49
+ function jsonResponse(body: unknown, status: number): Response {
50
+ return new Response(JSON.stringify(body), {
51
+ status,
52
+ headers: { ...corsHeaders, 'content-type': 'application/json' },
53
+ });
54
+ }
55
+
56
+ function resolveBrowserApiToken(env: Env): string {
57
+ const candidates = [env.BROWSER_API_TOKEN, env.API_TOKEN];
58
+
59
+ for (const candidate of candidates) {
60
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
61
+ return candidate.trim();
62
+ }
63
+ }
64
+
65
+ return '';
66
+ }
67
+
68
+ function resolveAccountId(env: Env): string {
69
+ const candidates = [env.ACCOUNT_ID, env.CLOUDFLARE_ACCOUNT_ID];
70
+
71
+ for (const candidate of candidates) {
72
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
73
+ return candidate.trim();
74
+ }
75
+ }
76
+
77
+ return '';
78
+ }
79
+
25
80
  function normalizeReportFormat(format: unknown): string {
26
81
  if (typeof format !== 'string') {
27
82
  return DEFAULT_REPORT_FORMAT;
@@ -69,6 +124,84 @@ async function renderReport(reportFormat: string, data: PDFGenerationData): Prom
69
124
  return reportModule.renderReport(data);
70
125
  }
71
126
 
127
+ async function renderPdfViaRestEndpoint(env: Env, html: string): Promise<Response> {
128
+ const accountId = resolveAccountId(env);
129
+ const browserApiToken = resolveBrowserApiToken(env);
130
+
131
+ if (!accountId || !browserApiToken) {
132
+ return jsonResponse(
133
+ {
134
+ error: 'Missing required Browser Rendering credentials',
135
+ requiredSecrets: ['ACCOUNT_ID', 'BROWSER_API_TOKEN'],
136
+ note: 'Set ACCOUNT_ID and a Browser Rendering - Edit token (BROWSER_API_TOKEN) on this worker.',
137
+ },
138
+ 502
139
+ );
140
+ }
141
+
142
+ const endpoint = `${BROWSER_RENDERING_API_BASE}/${accountId}/browser-rendering/pdf`;
143
+ const requestBody = JSON.stringify({
144
+ html,
145
+ pdfOptions: DEFAULT_PDF_OPTIONS,
146
+ });
147
+
148
+ let endpointResponse: Response;
149
+
150
+ try {
151
+ endpointResponse = await fetch(endpoint, {
152
+ method: 'POST',
153
+ headers: {
154
+ Authorization: `Bearer ${browserApiToken}`,
155
+ 'Content-Type': 'application/json',
156
+ },
157
+ body: requestBody,
158
+ signal: AbortSignal.timeout(BROWSER_PDF_TIMEOUT_MS),
159
+ });
160
+ } catch (error) {
161
+ const message = error instanceof Error ? error.message : 'Unknown browser endpoint error';
162
+ return jsonResponse(
163
+ {
164
+ error: 'Unable to reach Browser Rendering endpoint',
165
+ endpoint,
166
+ message,
167
+ },
168
+ isTimeoutError(error) ? 504 : 502
169
+ );
170
+ }
171
+
172
+ if (!endpointResponse.ok) {
173
+ const failureText = await endpointResponse.text().catch(() => '');
174
+ return jsonResponse(
175
+ {
176
+ error: 'Browser Rendering endpoint returned an error',
177
+ endpoint,
178
+ status: endpointResponse.status,
179
+ details: failureText.slice(0, 512) || endpointResponse.statusText || 'Unknown endpoint failure',
180
+ },
181
+ endpointResponse.status === 504 ? 504 : 502
182
+ );
183
+ }
184
+
185
+ const responseHeaders = new Headers(endpointResponse.headers);
186
+ if (!responseHeaders.has('content-type')) {
187
+ responseHeaders.set('content-type', 'application/pdf');
188
+ }
189
+
190
+ if (!responseHeaders.has('cache-control')) {
191
+ responseHeaders.set('cache-control', 'no-store');
192
+ }
193
+
194
+ for (const [headerName, headerValue] of Object.entries(corsHeaders)) {
195
+ responseHeaders.set(headerName, headerValue);
196
+ }
197
+
198
+ return new Response(endpointResponse.body, {
199
+ status: endpointResponse.status,
200
+ statusText: endpointResponse.statusText,
201
+ headers: responseHeaders,
202
+ });
203
+ }
204
+
72
205
  export default {
73
206
  async fetch(request: Request, env: Env): Promise<Response> {
74
207
  if (request.method === 'OPTIONS') {
@@ -76,55 +209,27 @@ export default {
76
209
  }
77
210
 
78
211
  if (!hasValidHeader(request, env)) {
79
- return new Response(JSON.stringify({ error: 'Forbidden' }), {
80
- status: 403,
81
- headers: { ...corsHeaders, 'content-type': 'application/json' },
82
- });
212
+ return jsonResponse({ error: 'Forbidden' }, 403);
83
213
  }
84
214
 
85
215
  if (request.method === 'POST') {
86
- let browser: Awaited<ReturnType<typeof launch>> | undefined;
87
-
88
216
  try {
89
217
  const payload = await request.json() as PDFGenerationData | PDFGenerationRequest;
90
218
  const { reportFormat, data } = resolveReportRequest(payload);
91
-
92
- browser = await launch(env.BROWSER);
93
- const page = await browser.newPage();
94
-
95
- // Render report from module selected by report format name.
96
219
  const document = await renderReport(reportFormat, data);
97
- await page.setContent(document);
98
-
99
- const pdfBuffer = await page.pdf({
100
- printBackground: true,
101
- format: 'letter',
102
- margin: { top: '0.5in', bottom: '0.5in', left: '0.5in', right: '0.5in' },
103
- });
104
-
105
- return new Response(new Uint8Array(pdfBuffer), {
106
- headers: {
107
- ...corsHeaders,
108
- 'content-type': 'application/pdf',
109
- },
110
- });
111
- } catch (error) {
112
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
113
220
 
114
- return new Response(JSON.stringify({ error: errorMessage }), {
115
- status: 500,
116
- headers: { ...corsHeaders, 'content-type': 'application/json' },
117
- });
118
- } finally {
119
- if (browser) {
120
- await browser.close();
221
+ return await renderPdfViaRestEndpoint(env, document);
222
+ } catch (error) {
223
+ if (isTimeoutError(error)) {
224
+ const timeoutMessage = error instanceof Error ? error.message : 'PDF generation timed out';
225
+ return jsonResponse({ error: timeoutMessage }, 504);
121
226
  }
227
+
228
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
229
+ return jsonResponse({ error: errorMessage }, 500);
122
230
  }
123
231
  }
124
232
 
125
- return new Response(JSON.stringify({ error: 'Method not allowed' }), {
126
- status: 405,
127
- headers: { ...corsHeaders, 'content-type': 'application/json' },
128
- });
233
+ return jsonResponse({ error: 'Method not allowed' }, 405);
129
234
  },
130
235
  };
@@ -2,7 +2,7 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-03-15",
5
+ "compatibility_date": "2026-03-17",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -2,7 +2,7 @@
2
2
  "name": "USER_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/user-worker.ts",
5
- "compatibility_date": "2026-03-15",
5
+ "compatibility_date": "2026-03-17",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  #:schema node_modules/wrangler/config-schema.json
2
2
  name = "PAGES_PROJECT_NAME"
3
- compatibility_date = "2026-03-15"
3
+ compatibility_date = "2026-03-17"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6
 
@@ -1,17 +0,0 @@
1
- -----BEGIN PGP PUBLIC KEY BLOCK-----
2
-
3
- xjMEaJ+SaRYJKwYBBAHaRw8BAQdAtWcW9OcCVtFNTBf+4lwASGgVBGNIGb6z
4
- GNIq37cw83zNIWluZm9Ac3RyaWFlLm9yZyA8aW5mb0BzdHJpYWUub3JnPsLA
5
- EQQTFgoAgwWCaJ+SaQMLCQcJkC2UdiWEnC1iRRQAAAAAABwAIHNhbHRAbm90
6
- YXRpb25zLm9wZW5wZ3Bqcy5vcmfhki1wbo0xW0HoW6jsyBK/iWN/u01JBBJd
7
- YGiCbEL+sAMVCggEFgACAQIZAQKbAwIeARYhBCw6mxgiRn72TJictC2UdiWE
8
- nC1iAADDlgD+Jlmez0pVz0u3kllpSt6S3MOuUCJla6p4ZEbjTniCJaUA/R7X
9
- kykgKaNm/PUolRZhnUwhcNWT48EMHo1jMkyGKUQEzjgEaJ+SaRIKKwYBBAGX
10
- VQEFAQEHQNEibB8TSeDgzURCgYgXn32lYJ6vVLl87FeJRsEQY4BfAwEIB8K+
11
- BBgWCgBwBYJon5JpCZAtlHYlhJwtYkUUAAAAAAAcACBzYWx0QG5vdGF0aW9u
12
- cy5vcGVucGdwanMub3Jn5sADmb+wNJJLschl/7wW5OxuG8sTK8hrYrYAMu8f
13
- lsgCmwwWIQQsOpsYIkZ+9kyYnLQtlHYlhJwtYgAADjQBAPLqpZXfz6LF3QCT
14
- ljTevmSNC+/XPUPUe0pWwmt3O+CFAQCoYARnr6O+e9K2RKTGiji5yY5EvHIo
15
- GsOM90I360PhCw==
16
- =MSt/
17
- -----END PGP PUBLIC KEY BLOCK-----
Binary file
File without changes
File without changes
File without changes
File without changes
File without changes