@striae-org/striae 4.0.1 → 4.0.3
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 +1 -0
- package/app/components/actions/case-export/data-processing.ts +1 -1
- package/app/components/actions/case-export/download-handlers.ts +4 -3
- package/app/components/actions/case-export/metadata-helpers.ts +1 -1
- package/app/components/actions/case-import/confirmation-import.ts +1 -1
- package/app/components/actions/case-import/image-operations.ts +1 -1
- package/app/components/actions/case-import/orchestrator.ts +1 -1
- package/app/components/actions/case-import/storage-operations.ts +3 -3
- package/app/components/actions/case-import/validation.ts +3 -4
- package/app/components/actions/case-import/zip-processing.ts +1 -1
- package/app/components/actions/case-manage.ts +3 -5
- package/app/components/actions/confirm-export.ts +4 -5
- package/app/components/actions/generate-pdf.ts +1 -1
- package/app/components/actions/image-manage.ts +2 -3
- package/app/components/actions/notes-manage.ts +1 -1
- package/app/components/actions/signout.tsx +1 -1
- package/app/components/audit/user-audit-viewer.tsx +1 -1
- package/app/components/auth/auth-provider.tsx +1 -1
- package/app/components/auth/mfa-verification.tsx +1 -1
- package/app/components/button/button.tsx +1 -1
- package/app/components/canvas/box-annotations/box-annotations.tsx +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +1 -1
- package/app/components/icon/icon.tsx +1 -1
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +1 -1
- package/app/components/sidebar/case-export/case-export.tsx +1 -1
- package/app/components/sidebar/cases/case-sidebar.tsx +3 -3
- package/app/components/sidebar/cases/cases-modal.tsx +1 -1
- package/app/components/sidebar/files/files-modal.tsx +1 -1
- package/app/components/sidebar/notes/notes-sidebar.tsx +1 -1
- package/app/components/sidebar/sidebar-container.tsx +2 -17
- package/app/components/sidebar/sidebar.module.css +0 -29
- package/app/components/theme-provider/theme-provider.tsx +1 -1
- package/app/components/theme-provider/theme.ts +1 -1
- package/app/components/user/delete-account.tsx +1 -1
- package/app/components/user/manage-profile.tsx +1 -1
- package/app/components/user/mfa-phone-update.tsx +1 -1
- package/app/root.tsx +18 -51
- package/app/routes/auth/emailActionHandler.tsx +1 -2
- package/app/routes/auth/emailVerification.tsx +1 -1
- package/app/routes/auth/login.example.tsx +775 -0
- package/app/routes/auth/login.module.example.css +523 -0
- package/app/routes/auth/login.tsx +4 -6
- package/app/routes/auth/passwordReset.tsx +1 -1
- package/app/routes/striae/striae.tsx +2 -2
- package/app/services/audit/audit-export-signing.ts +2 -2
- package/app/services/audit/audit-export.service.ts +1 -2
- package/app/services/audit/audit.service.ts +1 -1
- package/app/services/firebase/index.ts +1 -1
- package/app/utils/api/index.ts +4 -0
- package/app/utils/auth/index.ts +5 -0
- package/app/utils/common/index.ts +3 -0
- package/app/utils/{version.ts → common/version.ts} +1 -1
- package/app/utils/{data-operations.ts → data/data-operations.ts} +4 -4
- package/app/utils/data/index.ts +2 -0
- package/app/utils/{permissions.ts → data/permissions.ts} +1 -1
- package/app/utils/forensics/index.ts +5 -0
- package/app/utils/ui/index.ts +2 -0
- package/package.json +18 -20
- package/public/.well-known/keybase.txt +56 -0
- package/scripts/deploy-config.sh +29 -0
- package/scripts/deploy-worker-secrets.sh +1 -2
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/pdf-worker.example.ts +144 -39
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/public/.well-known/publickey.info@striae.org.asc +0 -17
- package/public/oin-badge.png +0 -0
- /package/app/utils/{data-api-client.ts → api/data-api-client.ts} +0 -0
- /package/app/utils/{image-api-client.ts → api/image-api-client.ts} +0 -0
- /package/app/utils/{pdf-api-client.ts → api/pdf-api-client.ts} +0 -0
- /package/app/utils/{user-api-client.ts → api/user-api-client.ts} +0 -0
- /package/app/utils/{auth-action-settings.ts → auth/auth-action-settings.ts} +0 -0
- /package/app/utils/{auth.ts → auth/auth.ts} +0 -0
- /package/app/utils/{mfa-phone.ts → auth/mfa-phone.ts} +0 -0
- /package/app/utils/{mfa.ts → auth/mfa.ts} +0 -0
- /package/app/utils/{password-policy.ts → auth/password-policy.ts} +0 -0
- /package/app/utils/{batch-operations.ts → common/batch-operations.ts} +0 -0
- /package/app/utils/{id-generator.ts → common/id-generator.ts} +0 -0
- /package/app/utils/{SHA256.ts → forensics/SHA256.ts} +0 -0
- /package/app/utils/{audit-export-signature.ts → forensics/audit-export-signature.ts} +0 -0
- /package/app/utils/{confirmation-signature.ts → forensics/confirmation-signature.ts} +0 -0
- /package/app/utils/{export-verification.ts → forensics/export-verification.ts} +0 -0
- /package/app/utils/{signature-utils.ts → forensics/signature-utils.ts} +0 -0
- /package/app/utils/{annotation-timestamp.ts → ui/annotation-timestamp.ts} +0 -0
- /package/app/utils/{style.ts → ui/style.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
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.
|
|
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.
|
|
127
|
-
"@types/react-dom": "^19.
|
|
128
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
129
|
-
"@typescript-eslint/parser": "^8.
|
|
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.
|
|
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.
|
|
135
|
-
"eslint-plugin-react": "^7.37.
|
|
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.
|
|
138
|
-
"tailwindcss": "^3.4.
|
|
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.
|
|
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
|
+
==================================================================
|
package/scripts/deploy-config.sh
CHANGED
|
@@ -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"
|
|
@@ -696,6 +697,8 @@ validate_generated_configs() {
|
|
|
696
697
|
"app/config/config.json"
|
|
697
698
|
"app/config/firebase.ts"
|
|
698
699
|
"app/config/admin-service.json"
|
|
700
|
+
"app/routes/auth/login.tsx"
|
|
701
|
+
"app/routes/auth/login.module.css"
|
|
699
702
|
"workers/audit-worker/wrangler.jsonc"
|
|
700
703
|
"workers/data-worker/wrangler.jsonc"
|
|
701
704
|
"workers/image-worker/wrangler.jsonc"
|
|
@@ -740,6 +743,7 @@ validate_generated_configs() {
|
|
|
740
743
|
|
|
741
744
|
assert_contains_literal "app/config/config.json" "https://$PAGES_CUSTOM_DOMAIN" "PAGES_CUSTOM_DOMAIN missing in app/config/config.json"
|
|
742
745
|
assert_contains_literal "app/config/config.json" "$ACCOUNT_HASH" "ACCOUNT_HASH missing in app/config/config.json"
|
|
746
|
+
assert_contains_literal "app/routes/auth/login.tsx" "const APP_CANONICAL_ORIGIN = 'https://$PAGES_CUSTOM_DOMAIN';" "PAGES_CUSTOM_DOMAIN missing in app/routes/auth/login.tsx canonical origin"
|
|
743
747
|
|
|
744
748
|
assert_contains_literal "app/config/firebase.ts" "$API_KEY" "API_KEY missing in app/config/firebase.ts"
|
|
745
749
|
assert_contains_literal "app/config/firebase.ts" "$AUTH_DOMAIN" "AUTH_DOMAIN missing in app/config/firebase.ts"
|
|
@@ -775,6 +779,7 @@ validate_generated_configs() {
|
|
|
775
779
|
"workers/user-worker/src/user-worker.ts"
|
|
776
780
|
"app/config/config.json"
|
|
777
781
|
"app/config/firebase.ts"
|
|
782
|
+
"app/routes/auth/login.tsx"
|
|
778
783
|
)
|
|
779
784
|
|
|
780
785
|
for file_path in "${files_to_scan[@]}"; do
|
|
@@ -861,6 +866,23 @@ copy_example_configs() {
|
|
|
861
866
|
|
|
862
867
|
echo -e "${GREEN} ✅ app: copied $copied_config_files config file(s) from config-example${NC}"
|
|
863
868
|
fi
|
|
869
|
+
|
|
870
|
+
# Copy auth route template files
|
|
871
|
+
echo -e "${YELLOW} Copying auth route template files...${NC}"
|
|
872
|
+
|
|
873
|
+
if [ -f "app/routes/auth/login.example.tsx" ] && { [ "$update_env" = "true" ] || [ ! -f "app/routes/auth/login.tsx" ]; }; then
|
|
874
|
+
cp app/routes/auth/login.example.tsx app/routes/auth/login.tsx
|
|
875
|
+
echo -e "${GREEN} ✅ auth: login.tsx created from example${NC}"
|
|
876
|
+
elif [ -f "app/routes/auth/login.tsx" ]; then
|
|
877
|
+
echo -e "${YELLOW} ⚠️ auth: login.tsx already exists, skipping copy${NC}"
|
|
878
|
+
fi
|
|
879
|
+
|
|
880
|
+
if [ -f "app/routes/auth/login.module.example.css" ] && { [ "$update_env" = "true" ] || [ ! -f "app/routes/auth/login.module.css" ]; }; then
|
|
881
|
+
cp app/routes/auth/login.module.example.css app/routes/auth/login.module.css
|
|
882
|
+
echo -e "${GREEN} ✅ auth: login.module.css created from example${NC}"
|
|
883
|
+
elif [ -f "app/routes/auth/login.module.css" ]; then
|
|
884
|
+
echo -e "${YELLOW} ⚠️ auth: login.module.css already exists, skipping copy${NC}"
|
|
885
|
+
fi
|
|
864
886
|
|
|
865
887
|
# Navigate to each worker directory and copy the example file
|
|
866
888
|
echo -e "${YELLOW} Copying worker configuration files...${NC}"
|
|
@@ -1284,6 +1306,7 @@ prompt_for_secrets() {
|
|
|
1284
1306
|
prompt_for_var "PDF_WORKER_AUTH" "PDF worker authentication token (generate with: openssl rand -hex 16)"
|
|
1285
1307
|
prompt_for_var "ACCOUNT_HASH" "Cloudflare Images Account Hash"
|
|
1286
1308
|
prompt_for_var "API_TOKEN" "Cloudflare Images API token (for Images Worker)"
|
|
1309
|
+
prompt_for_var "BROWSER_API_TOKEN" "Cloudflare Browser Rendering API token (for PDF Worker)"
|
|
1287
1310
|
prompt_for_var "HMAC_KEY" "Cloudflare Images HMAC signing key"
|
|
1288
1311
|
|
|
1289
1312
|
configure_manifest_signing_credentials
|
|
@@ -1448,6 +1471,12 @@ update_wrangler_configs() {
|
|
|
1448
1471
|
sed -i "s|\"YOUR_FIREBASE_MEASUREMENT_ID\"|\"$MEASUREMENT_ID\"|g" app/config/firebase.ts
|
|
1449
1472
|
echo -e "${GREEN} ✅ app firebase.ts updated${NC}"
|
|
1450
1473
|
fi
|
|
1474
|
+
|
|
1475
|
+
if [ -f "app/routes/auth/login.tsx" ]; then
|
|
1476
|
+
echo -e "${YELLOW} Updating app/routes/auth/login.tsx...${NC}"
|
|
1477
|
+
sed -i "s|^const APP_CANONICAL_ORIGIN = .*;|const APP_CANONICAL_ORIGIN = 'https://$escaped_pages_custom_domain';|g" app/routes/auth/login.tsx
|
|
1478
|
+
echo -e "${GREEN} ✅ app login.tsx canonical origin updated${NC}"
|
|
1479
|
+
fi
|
|
1451
1480
|
|
|
1452
1481
|
echo -e "${GREEN}✅ All configuration files updated${NC}"
|
|
1453
1482
|
}
|
|
@@ -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
|
|
|
@@ -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
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
126
|
-
status: 405,
|
|
127
|
-
headers: { ...corsHeaders, 'content-type': 'application/json' },
|
|
128
|
-
});
|
|
233
|
+
return jsonResponse({ error: 'Method not allowed' }, 405);
|
|
129
234
|
},
|
|
130
235
|
};
|
package/wrangler.toml.example
CHANGED
|
@@ -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-----
|
package/public/oin-badge.png
DELETED
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|