@striae-org/striae 3.1.1 → 3.2.0

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 CHANGED
@@ -43,6 +43,7 @@ PAGES_CUSTOM_DOMAIN=your_custom_domain_here
43
43
  # ================================
44
44
  # KEYS WORKER ENVIRONMENT VARIABLES
45
45
  # ================================
46
+ # Worker domains can be entered manually or auto-generated as a 10-character subdomain of PAGES_CUSTOM_DOMAIN during scripts/deploy-config.sh.
46
47
  KEYS_WORKER_NAME=your_keys_worker_name_here
47
48
  KEYS_WORKER_DOMAIN=your_keys_worker_domain_here
48
49
  KEYS_AUTH=your_custom_keys_auth_token_here
@@ -86,6 +87,7 @@ HMAC_KEY=your_cloudflare_images_hmac_key_here
86
87
  # ================================
87
88
  PDF_WORKER_NAME=your_pdf_worker_name_here
88
89
  PDF_WORKER_DOMAIN=your_pdf_worker_domain_here
90
+ PDF_WORKER_AUTH=your_custom_pdf_worker_auth_token_here
89
91
 
90
92
  # ================================
91
93
  # QUICK MANUAL SETUP CHECKLIST
@@ -1,6 +1,7 @@
1
1
  import paths from '~/config/config.json';
2
2
  import { AnnotationData } from '~/types/annotations';
3
3
  import { auditService } from '~/services/audit.service';
4
+ import { getPdfApiKey } from '~/utils/auth';
4
5
  import { User } from 'firebase/auth';
5
6
 
6
7
  interface GeneratePDFParams {
@@ -74,10 +75,13 @@ export const generatePDF = async ({
74
75
  data: pdfData,
75
76
  };
76
77
 
78
+ const pdfAuthKey = await getPdfApiKey();
79
+
77
80
  const response = await fetch(paths.pdf_worker_url, {
78
81
  method: 'POST',
79
82
  headers: {
80
83
  'Content-Type': 'application/json',
84
+ 'X-Custom-Auth-Key': pdfAuthKey,
81
85
  },
82
86
  body: JSON.stringify(pdfRequest)
83
87
  });
@@ -1,5 +1,5 @@
1
1
  export const INACTIVITY_CONFIG = {
2
- TIMEOUT_MINUTES: 60,
2
+ TIMEOUT_MINUTES: 30,
3
3
  WARNING_MINUTES: 5,
4
4
  TRACKED_ACTIVITIES: [
5
5
  'mousedown',
@@ -1,6 +1,8 @@
1
1
  import { initializeApp } from 'firebase/app';
2
2
  import {
3
3
  getAuth,
4
+ setPersistence,
5
+ browserSessionPersistence,
4
6
  //connectAuthEmulator,
5
7
  } from 'firebase/auth';
6
8
  import firebaseConfig from '~/config/firebase';
@@ -9,6 +11,8 @@ import { getAppVersion } from '~/utils/version';
9
11
  export const app = initializeApp(firebaseConfig, "Striae");
10
12
  export const auth = getAuth(app);
11
13
 
14
+ setPersistence(auth, browserSessionPersistence);
15
+
12
16
  console.log(`Welcome to ${app.name} v${getAppVersion()}`);
13
17
 
14
18
  //Connect to the Firebase Auth emulator if running locally
package/app/utils/auth.ts CHANGED
@@ -3,7 +3,7 @@ import paths from '~/config/config.json';
3
3
  const KEYS_URL = paths.keys_url;
4
4
  const KEYS_AUTH = paths.keys_auth;
5
5
 
6
- type KeyType = 'USER_DB_AUTH' | 'R2_KEY_SECRET' | 'IMAGES_API_TOKEN' | 'ACCOUNT_HASH';
6
+ type KeyType = 'USER_DB_AUTH' | 'R2_KEY_SECRET' | 'IMAGES_API_TOKEN' | 'ACCOUNT_HASH' | 'PDF_WORKER_AUTH';
7
7
 
8
8
  async function getApiKey(keyType: KeyType): Promise<string> {
9
9
  const keyResponse = await fetch(`${KEYS_URL}/${keyType}`, {
@@ -31,4 +31,8 @@ export async function getImageApiKey(): Promise<string> {
31
31
 
32
32
  export async function getAccountHash(): Promise<string> {
33
33
  return getApiKey('ACCOUNT_HASH');
34
+ }
35
+
36
+ export async function getPdfApiKey(): Promise<string> {
37
+ return getApiKey('PDF_WORKER_AUTH');
34
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "private": false,
5
5
  "description": "Cloud-native forensic annotation application for firearms identification (Remix + Cloudflare Workers).",
6
6
  "license": "Apache-2.0",
@@ -77,7 +77,7 @@
77
77
  "publish:github": "npm publish --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
78
78
  "publish:github:dry-run": "npm publish --dry-run --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
79
79
  "publish:all": "npm run publish:npm && npm run publish:github",
80
- "publish:all:dry-run": "npm run publish:npm:dry-run && npm run publish:github:dry-run",
80
+ "publish:all:dry-run": "npm run publish:npm:dry-run && npm run publish:github:dry-run",
81
81
  "lint": "node ./scripts/run-eslint.cjs",
82
82
  "start": "node ./scripts/dev.cjs && wrangler pages dev",
83
83
  "typecheck": "tsc",
@@ -8,6 +8,7 @@
8
8
  "/*.css",
9
9
  "/*.js",
10
10
  "/*.pdf",
11
+ "/*.png",
11
12
  "/*.zip"
12
13
  ]
13
14
  }
Binary file
@@ -57,9 +57,16 @@ is_placeholder() {
57
57
 
58
58
  # Check if .env file exists
59
59
  env_created_from_example=false
60
+ preserved_domain_env_file=""
61
+
62
+ if [ -f ".env" ]; then
63
+ preserved_domain_env_file=".env"
64
+ fi
65
+
60
66
  if [ "$update_env" = "true" ]; then
61
67
  if [ -f ".env" ]; then
62
68
  cp .env .env.backup
69
+ preserved_domain_env_file=".env.backup"
63
70
  echo -e "${GREEN}📄 Existing .env backed up to .env.backup${NC}"
64
71
  fi
65
72
 
@@ -133,11 +140,151 @@ normalize_domain_value() {
133
140
  printf '%s' "$domain"
134
141
  }
135
142
 
143
+ strip_carriage_returns() {
144
+ printf '%s' "$1" | tr -d '\r'
145
+ }
146
+
147
+ read_env_var_from_file() {
148
+ local env_file=$1
149
+ local var_name=$2
150
+
151
+ if [ ! -f "$env_file" ]; then
152
+ return 0
153
+ fi
154
+
155
+ awk -v key="$var_name" '
156
+ index($0, key "=") == 1 {
157
+ value = substr($0, length(key) + 2)
158
+ }
159
+ END {
160
+ if (value != "") {
161
+ gsub(/\r/, "", value)
162
+ gsub(/^"/, "", value)
163
+ gsub(/"$/, "", value)
164
+ print value
165
+ }
166
+ }
167
+ ' "$env_file"
168
+ }
169
+
170
+ worker_domain_wrangler_path() {
171
+ case "$1" in
172
+ KEYS_WORKER_DOMAIN)
173
+ printf '%s' "workers/keys-worker/wrangler.jsonc"
174
+ ;;
175
+ USER_WORKER_DOMAIN)
176
+ printf '%s' "workers/user-worker/wrangler.jsonc"
177
+ ;;
178
+ DATA_WORKER_DOMAIN)
179
+ printf '%s' "workers/data-worker/wrangler.jsonc"
180
+ ;;
181
+ AUDIT_WORKER_DOMAIN)
182
+ printf '%s' "workers/audit-worker/wrangler.jsonc"
183
+ ;;
184
+ IMAGES_WORKER_DOMAIN)
185
+ printf '%s' "workers/image-worker/wrangler.jsonc"
186
+ ;;
187
+ PDF_WORKER_DOMAIN)
188
+ printf '%s' "workers/pdf-worker/wrangler.jsonc"
189
+ ;;
190
+ esac
191
+ }
192
+
193
+ read_worker_domain_from_wrangler() {
194
+ local wrangler_file=$1
195
+
196
+ if [ ! -f "$wrangler_file" ]; then
197
+ return 0
198
+ fi
199
+
200
+ sed -n 's/.*"pattern"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$wrangler_file" | head -n 1
201
+ }
202
+
203
+ resolve_existing_domain_value() {
204
+ local var_name=$1
205
+ local current_value=$2
206
+ local preserved_value=""
207
+ local wrangler_file=""
208
+
209
+ current_value=$(normalize_domain_value "$current_value")
210
+
211
+ if [ "$current_value" = "$var_name" ]; then
212
+ current_value=""
213
+ fi
214
+
215
+ if [ -n "$current_value" ] && ! is_placeholder "$current_value"; then
216
+ printf '%s' "$current_value"
217
+ return 0
218
+ fi
219
+
220
+ if [ -n "$preserved_domain_env_file" ] && [ -f "$preserved_domain_env_file" ]; then
221
+ preserved_value=$(read_env_var_from_file "$preserved_domain_env_file" "$var_name")
222
+ preserved_value=$(normalize_domain_value "$preserved_value")
223
+
224
+ if [ "$preserved_value" = "$var_name" ]; then
225
+ preserved_value=""
226
+ fi
227
+
228
+ if [ -n "$preserved_value" ] && ! is_placeholder "$preserved_value"; then
229
+ printf '%s' "$preserved_value"
230
+ return 0
231
+ fi
232
+ fi
233
+
234
+ if [[ "$var_name" == *_WORKER_DOMAIN ]]; then
235
+ wrangler_file=$(worker_domain_wrangler_path "$var_name")
236
+
237
+ if [ -n "$wrangler_file" ] && [ -f "$wrangler_file" ]; then
238
+ preserved_value=$(read_worker_domain_from_wrangler "$wrangler_file")
239
+ preserved_value=$(normalize_domain_value "$preserved_value")
240
+
241
+ if [ "$preserved_value" = "$var_name" ]; then
242
+ preserved_value=""
243
+ fi
244
+
245
+ if [ -n "$preserved_value" ] && ! is_placeholder "$preserved_value"; then
246
+ printf '%s' "$preserved_value"
247
+ return 0
248
+ fi
249
+ fi
250
+ fi
251
+
252
+ printf '%s' "$current_value"
253
+ }
254
+
255
+ generate_worker_subdomain_label() {
256
+ node -e "const { randomInt } = require('crypto'); const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; let value = ''; for (let index = 0; index < 10; index += 1) { value += alphabet[randomInt(alphabet.length)]; } process.stdout.write(value);" 2>/dev/null
257
+ }
258
+
259
+ generate_worker_subdomain() {
260
+ local pages_domain=$1
261
+ local subdomain_label=""
262
+
263
+ pages_domain=$(normalize_domain_value "$pages_domain")
264
+
265
+ if [ -z "$pages_domain" ] || is_placeholder "$pages_domain"; then
266
+ return 1
267
+ fi
268
+
269
+ if ! subdomain_label=$(generate_worker_subdomain_label); then
270
+ return 1
271
+ fi
272
+
273
+ if [ ${#subdomain_label} -ne 10 ]; then
274
+ return 1
275
+ fi
276
+
277
+ printf '%s.%s' "$subdomain_label" "$pages_domain"
278
+ }
279
+
136
280
  write_env_var() {
137
281
  local var_name=$1
138
282
  local var_value=$2
139
283
  local env_file_value="$var_value"
140
284
 
285
+ var_value=$(strip_carriage_returns "$var_value")
286
+ env_file_value="$var_value"
287
+
141
288
  if [ "$var_name" = "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PUBLIC_KEY" ]; then
142
289
  # Store as a quoted string so sourced .env preserves escaped newline markers (\n)
143
290
  env_file_value=${env_file_value//\"/\\\"}
@@ -276,6 +423,7 @@ configure_manifest_signing_credentials() {
276
423
  else
277
424
  echo -e "${GREEN}Current manifest signing key pair: [HIDDEN]${NC}"
278
425
  read -p "Generate new manifest signing key pair? (press Enter to keep current, or type 'y' to regenerate): " regenerate_choice
426
+ regenerate_choice=$(strip_carriage_returns "$regenerate_choice")
279
427
  if [ "$regenerate_choice" = "y" ] || [ "$regenerate_choice" = "Y" ]; then
280
428
  should_generate="true"
281
429
  fi
@@ -353,6 +501,7 @@ required_vars=(
353
501
 
354
502
  # Worker-Specific Secrets (required for deployment)
355
503
  "KEYS_AUTH"
504
+ "PDF_WORKER_AUTH"
356
505
  "ACCOUNT_HASH"
357
506
  "API_TOKEN"
358
507
  "HMAC_KEY"
@@ -579,16 +728,23 @@ prompt_for_secrets() {
579
728
  local current_value="${!var_name}"
580
729
  local new_value=""
581
730
  local allow_keep="false"
731
+
732
+ current_value=$(strip_carriage_returns "$current_value")
733
+
734
+ if [ "$var_name" = "PAGES_CUSTOM_DOMAIN" ] || [[ "$var_name" == *_WORKER_DOMAIN ]]; then
735
+ current_value=$(resolve_existing_domain_value "$var_name" "$current_value")
736
+ fi
582
737
 
583
738
  # Auto-generate specific authentication secrets - but allow keeping current
584
- if [ "$var_name" = "USER_DB_AUTH" ] || [ "$var_name" = "R2_KEY_SECRET" ] || [ "$var_name" = "KEYS_AUTH" ]; then
739
+ if [ "$var_name" = "USER_DB_AUTH" ] || [ "$var_name" = "R2_KEY_SECRET" ] || [ "$var_name" = "KEYS_AUTH" ] || [ "$var_name" = "PDF_WORKER_AUTH" ]; then
585
740
  echo -e "${BLUE}$var_name${NC}"
586
741
  echo -e "${YELLOW}$description${NC}"
587
742
 
588
- if [ "$update_env" != "true" ] && [ -n "$current_value" ] && ! is_placeholder "$current_value" && [ "$current_value" != "your_custom_user_db_auth_token_here" ] && [ "$current_value" != "your_custom_r2_secret_here" ] && [ "$current_value" != "your_custom_keys_auth_token_here" ]; then
743
+ if [ "$update_env" != "true" ] && [ -n "$current_value" ] && ! is_placeholder "$current_value" && [ "$current_value" != "your_custom_user_db_auth_token_here" ] && [ "$current_value" != "your_custom_r2_secret_here" ] && [ "$current_value" != "your_custom_keys_auth_token_here" ] && [ "$current_value" != "your_custom_pdf_worker_auth_token_here" ]; then
589
744
  # Current value exists and is not a placeholder
590
745
  echo -e "${GREEN}Current value: [HIDDEN]${NC}"
591
746
  read -p "Generate new secret? (press Enter to keep current, or type 'y' to generate): " gen_choice
747
+ gen_choice=$(strip_carriage_returns "$gen_choice")
592
748
 
593
749
  if [ "$gen_choice" = "y" ] || [ "$gen_choice" = "Y" ]; then
594
750
  new_value=$(openssl rand -hex 32 2>/dev/null || echo "")
@@ -598,6 +754,7 @@ prompt_for_secrets() {
598
754
  while true; do
599
755
  echo -e "${RED}❌ Failed to auto-generate, please enter manually:${NC}"
600
756
  read -p "Enter value: " new_value
757
+ new_value=$(strip_carriage_returns "$new_value")
601
758
  if [ -z "$new_value" ]; then
602
759
  echo -e "${RED}❌ A value is required.${NC}"
603
760
  continue
@@ -624,6 +781,7 @@ prompt_for_secrets() {
624
781
  while true; do
625
782
  echo -e "${RED}❌ Failed to auto-generate, please enter manually:${NC}"
626
783
  read -p "Enter value: " new_value
784
+ new_value=$(strip_carriage_returns "$new_value")
627
785
  if [ -z "$new_value" ]; then
628
786
  echo -e "${RED}❌ A value is required.${NC}"
629
787
  continue
@@ -637,6 +795,68 @@ prompt_for_secrets() {
637
795
  done
638
796
  fi
639
797
  fi
798
+ elif [[ "$var_name" == *_WORKER_DOMAIN ]]; then
799
+ local pages_domain
800
+ local domain_choice=""
801
+
802
+ pages_domain=$(resolve_existing_domain_value "PAGES_CUSTOM_DOMAIN" "$PAGES_CUSTOM_DOMAIN")
803
+
804
+ echo -e "${BLUE}$var_name${NC}"
805
+ echo -e "${YELLOW}$description${NC}"
806
+
807
+ if [ -n "$current_value" ] && ! is_placeholder "$current_value"; then
808
+ echo -e "${GREEN}Current value: $current_value${NC}"
809
+ else
810
+ while true; do
811
+ if [ -n "$pages_domain" ] && ! is_placeholder "$pages_domain"; then
812
+ echo -e "${YELLOW}Choose how to configure this worker domain:${NC}"
813
+ echo " A) Auto-generate a 10-character subdomain of $pages_domain"
814
+ echo " M) Manually enter the worker domain"
815
+ read -p "Selection (A/M): " domain_choice
816
+ domain_choice=$(strip_carriage_returns "$domain_choice")
817
+ domain_choice=$(printf '%s' "$domain_choice" | tr '[:upper:]' '[:lower:]')
818
+
819
+ if [ -z "$domain_choice" ] || [ "$domain_choice" = "a" ]; then
820
+ new_value=$(generate_worker_subdomain "$pages_domain" || echo "")
821
+
822
+ if [ -n "$new_value" ]; then
823
+ echo -e "${GREEN}✅ Generated worker domain: $new_value${NC}"
824
+ break
825
+ fi
826
+
827
+ echo -e "${RED}❌ Failed to auto-generate a worker subdomain. Please try manual entry.${NC}"
828
+ continue
829
+ fi
830
+
831
+ if [ "$domain_choice" = "m" ]; then
832
+ read -p "Enter value: " new_value
833
+ new_value=$(strip_carriage_returns "$new_value")
834
+ else
835
+ echo -e "${RED}❌ Please choose 'A' for automatic or 'M' for manual.${NC}"
836
+ continue
837
+ fi
838
+ else
839
+ echo -e "${YELLOW}PAGES_CUSTOM_DOMAIN is required for auto-generated worker subdomains.${NC}"
840
+ read -p "Enter value: " new_value
841
+ new_value=$(strip_carriage_returns "$new_value")
842
+ fi
843
+
844
+ if [ -z "$new_value" ]; then
845
+ echo -e "${RED}❌ A value is required.${NC}"
846
+ continue
847
+ fi
848
+
849
+ new_value=$(normalize_domain_value "$new_value")
850
+
851
+ if is_placeholder "$new_value"; then
852
+ echo -e "${RED}❌ Placeholder values are not allowed.${NC}"
853
+ new_value=""
854
+ continue
855
+ fi
856
+
857
+ break
858
+ done
859
+ fi
640
860
  else
641
861
  # Normal prompt for other variables
642
862
  echo -e "${BLUE}$var_name${NC}"
@@ -653,11 +873,13 @@ prompt_for_secrets() {
653
873
  while true; do
654
874
  if [ "$allow_keep" = "true" ]; then
655
875
  read -p "New value (or press Enter to keep current): " new_value
876
+ new_value=$(strip_carriage_returns "$new_value")
656
877
  if [ -z "$new_value" ]; then
657
878
  break
658
879
  fi
659
880
  else
660
881
  read -p "Enter value: " new_value
882
+ new_value=$(strip_carriage_returns "$new_value")
661
883
  if [ -z "$new_value" ]; then
662
884
  echo -e "${RED}❌ A value is required.${NC}"
663
885
  continue
@@ -687,6 +909,7 @@ prompt_for_secrets() {
687
909
  elif [ -n "$current_value" ]; then
688
910
  # Keep values aligned with .env.example ordering and remove stale duplicates.
689
911
  write_env_var "$var_name" "$current_value"
912
+ export "$var_name=$current_value"
690
913
  echo -e "${GREEN}✅ Keeping current value for $var_name${NC}"
691
914
  fi
692
915
  echo ""
@@ -741,6 +964,7 @@ prompt_for_secrets() {
741
964
  echo -e "${BLUE}🔐 SERVICE-SPECIFIC SECRETS${NC}"
742
965
  echo "============================"
743
966
  prompt_for_var "KEYS_AUTH" "Keys worker authentication token (generate with: openssl rand -hex 16)"
967
+ prompt_for_var "PDF_WORKER_AUTH" "PDF worker authentication token (generate with: openssl rand -hex 16)"
744
968
  prompt_for_var "ACCOUNT_HASH" "Cloudflare Images Account Hash"
745
969
  prompt_for_var "API_TOKEN" "Cloudflare Images API token (for Images Worker)"
746
970
  prompt_for_var "HMAC_KEY" "Cloudflare Images HMAC signing key"
@@ -175,7 +175,7 @@ fi
175
175
 
176
176
  # Keys Worker
177
177
  if ! set_worker_secrets "Keys Worker" "workers/keys-worker" \
178
- "KEYS_AUTH" "USER_DB_AUTH" "R2_KEY_SECRET" "ACCOUNT_HASH" "IMAGES_API_TOKEN"; then
178
+ "KEYS_AUTH" "USER_DB_AUTH" "R2_KEY_SECRET" "ACCOUNT_HASH" "IMAGES_API_TOKEN" "PDF_WORKER_AUTH"; then
179
179
  echo -e "${YELLOW}⚠️ Skipping Keys Worker (not configured)${NC}"
180
180
  fi
181
181
 
@@ -198,7 +198,11 @@ if ! set_worker_secrets "Images Worker" "workers/image-worker" \
198
198
  fi
199
199
 
200
200
  # PDF Worker (no secrets needed)
201
- echo -e "\n${BLUE}📄 PDF Worker: No environment variables needed${NC}"
201
+ # PDF Worker
202
+ if ! set_worker_secrets "PDF Worker" "workers/pdf-worker" \
203
+ "PDF_WORKER_AUTH"; then
204
+ echo -e "${YELLOW}⚠️ Skipping PDF Worker (not configured)${NC}"
205
+ fi
202
206
 
203
207
  echo -e "\n${GREEN}🎉 Worker secrets deployment completed!${NC}"
204
208
 
@@ -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-12",
5
+ "compatibility_date": "2026-03-13",
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-12",
6
+ "compatibility_date": "2026-03-13",
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-12",
5
+ "compatibility_date": "2026-03-13",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -3,6 +3,7 @@ interface Env {
3
3
  ACCOUNT_HASH: string;
4
4
  IMAGES_API_TOKEN: string;
5
5
  USER_DB_AUTH: string;
6
+ PDF_WORKER_AUTH: string;
6
7
  KEYS_AUTH: string;
7
8
  }
8
9
 
@@ -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-12",
5
+ "compatibility_date": "2026-03-13",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -3,6 +3,7 @@
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
5
  "scripts": {
6
+ "generate:assets": "node scripts/generate-assets.js",
6
7
  "deploy": "wrangler deploy",
7
8
  "dev": "wrangler dev",
8
9
  "start": "wrangler dev",
@@ -1,4 +1,5 @@
1
1
  import type { PDFGenerationData, ReportRenderer } from './report-types';
2
+ import { ICON_256 } from './generated-assets';
2
3
 
3
4
  export const renderReport: ReportRenderer = (data: PDFGenerationData): string => {
4
5
  const { imageUrl, caseNumber, annotationData, activeAnnotations, currentDate, notesUpdatedFormatted, userCompany } = data;
@@ -519,7 +520,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
519
520
  <div class="footer">
520
521
  <div class="footer-left">
521
522
  <span>Notes formatted by Striae</span>
522
- <img class="footer-brand-icon" src="https://app.striae.org/icon-256.png" alt="Striae icon" />
523
+ <img class="footer-brand-icon" src="${ICON_256}" alt="Striae icon" />
523
524
  </div>
524
525
  <div class="footer-center">
525
526
  ${userCompany ? userCompany : ''}
@@ -0,0 +1,117 @@
1
+ // Auto-generated by scripts/generate-assets.js
2
+ // Do not edit manually — run `npm run generate:assets` to regenerate.
3
+
4
+ // Source: src/assets/icon-256.png
5
+ export const ICON_256 =
6
+ "data:image/png;base64," +
7
+ "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVY" +
8
+ "dFNvZnR3YXJlAFBhaW50Lk5FVCA1LjEuMvu8A7YAAAC2ZVhJZklJKgAIAAAABQAaAQUAAQAAAEoAAAAbAQUAAQAAAFIAAAAoAQMAAQAAAAIAAAAxAQIAEAAA" +
9
+ "AFoAAABphwQAAQAAAGoAAAAAAAAAYAAAAAEAAABgAAAAAQAAAFBhaW50Lk5FVCA1LjEuMgADAACQBwAEAAAAMDIzMAGgAwABAAAAAQAAAAWgBAABAAAAlAAA" +
10
+ "AAAAAAACAAEAAgAEAAAAUjk4AAIABwAEAAAAMDEwMAAAAADp1fY4ytpsegAAJW1JREFUeF7tnQecVdW1/9ftbXpv9KaYKFHMM3+RZzRoNAYFJY/yBGOMIu/p" +
11
+ "U3z2gJ/YRYLGmIiRYsooSIvGqMSnWGLJx5K8hygCQxkYhun99nv3f68zexQGpt/T9lnfz+d8zt7nUu49Z6/fLmevtYAgCIIgCIIgCIIgCIIgCIIgCEmxiTMx" +
12
+ "RB5//HHW0NAgaoSajBgxAq655hpquymAbuIQuP/++9n27dshFouJK4SW+P1+OP3002Hx4sXUjgcJ3bgBsGXLFvbhhx/C7t27xRXCSEyaNAmWLl1KbXoA0M3q" +
13
+ "Bw8//DD75JNPIJFIiCuEkfF6vTBlyhRYtGgRte8+oBvUA+Xl5Wzbtm3Q2NgorhBmZNiwYXD++efD9OnTqa0TBEEQBEEo0LDIhKx9/T1W29wKR/hR19IO9fxo" +
14
+ "C4UhHItBPJGARCIJ0XgCkowB48fR4AO32+38sIGDn/FwORzgdPCz0wFpfP5ckJUOZXnZUJiVAf916fnURiSGHq5BeWTDq+yLQ0dgz+FaCIajijEnkknlHOcG" +
15
+ "joaNdTVBkbDbbIpAuJ1OcHCRcHLBCHg9kJeZBhNKi+Dn/05zazNDD88ALNv4GjtY3wSHG5qhtrkNmjqCEIpExafGB0cLJTlZMKIwFx6+aia1KRNBD0sHVmx5" +
16
+ "nW3ffwg+raiE9lBEXJUHGx81jC8pgIkjSmD5T2ZRGzMw9HA04qoVa9kHOyuU4bsVGZafA1MmjoX7519Gbc5A0MNQkSV/eJG9u2M3VPHhPc7dCVDWEnLSA3BS" +
17
+ "WRGcPXEMXH3BFGqDOmKpm79u3TpVrbA1EoOKxnb4ZzWfz7eFIRiNi0+InsjxuWFcbjqcUZoD+QEPuB128Yk+OLlAXXHFFZaxC0v80JdffpmVl5dDJKLCfJvP" +
18
+ "d4N2DzR7MqHFlQZRu0t8QAwEbIiBeBCyIy2QEe8AZ1I/8czKyoI1a9ZYwjak/5F33303++KLL0QtdcTtTmhxp0OtJ4eMXgWyom1QGG4AbyIsrmjP1KlT4aab" +
19
+ "bpLaRqT+cfPmzWOhUEjUUkPY7oYGTxY08R4/AXy4ykcAhDo4WAIyo+1QEGkAT0Kf16IFBQWwcuVKaR+ylD9s1apV7JVXXhG11JDgt6rOm8MNP4t6fI3xJGN8" +
20
+ "atCsTA/cTPupgcvlgvXr10tpK9R99YPH17/EXvj7Z1DT0i6uEHpQmJkGl0+eCLfMpVeJqYJuZB+ctfhBhvvtCeMwsjAX3njwFmq7KYBuYg9c/+s/sjf/90vF" +
21
+ "uYYwHui49KMpk+HeKy+lNjwE6OadgHNuXcYONzaLGmFkJo0eBpvuvp7a8SChG3cUi35dzrZ+ukPUCLOAHotXnncWLJ37Q2rPA4RumOD/3fIwq2luFTXCjIwo" +
22
+ "yIU3H6K1gYFg+Zv14PpX2B+3fQiRGG3blYH8zHT4cMWdJAL9xNI36rY1G9mm9z4VNUIWcIFw59P3kQj0A8PfpCVLlqTcgSfK/8VtDUlowAIhLRMCNjg9065b" +
23
+ "I3e73dh+DW1jhv5yM2fOTLmFdji8UBUohpDDI64QMpMZa4eyjmpwMn1e52ZnZ8Pq1asNa2f6+l72wpw5c1Ju/G2uAFSmlZLxWwj00DzoL4KYzSGuaEtTUxNc" +
24
+ "d911hh1qGlIArrrqKpZq190Opx8OBEppH78FaXWnQ7W/EBI6iUBdXR16FRpSBAwnAAsWLGCtral9HdfOjX8/7/kTNsMOeAiVaXJnQJWvAPSywsrKSrj55psN" +
25
+ "JwKGmptg4I729tTuu99V3wZbPj+kROshrA029slluTD9pBJl85AezJ4921A2Z9jFiVQx5dZHWHVji6gRVgcjFt84/Tx+UMITROqb8J3FD7HaljZRI4hOsNHv" +
26
+ "Wf0gCQBH2knx9HufJOMnTghOxE+/4T7DrsxriZQCcPXjz7IdBw6LGkEcT0swBOff9QvLi4B0ArD0jy+yt7fvEjWC6Jn9NQ1KwhZRtSTSCUD5tr+LEkH0DSZu" +
27
+ "Wb31XcuKgFQCcMr199C8jhgwv3zpTVGyHtIIwL8/uoqFo/Sunxg4HeEIzF32jCU7DykE4Dcvb2Mf7NwragQxcP7+5T54dNNWy4mAZu9CMS/fjh2pD7fVFgd4" +
28
+ "rS4BMRr8E0PEwa3hB/l2SHPqu0Xgvvu0i2Wg2X+khmsvsj9QqqToIohUkB7rgNHtB0VNH7RMRKLJFEAN114k6PBy408TNYIYOugy3urSt03FYjHNXIhVF4Ab" +
29
+ "brgh5a69COblq/YX8JK+wzVCPqq9eUryVz1BF+IVK1aoLgKqCsDatWtZVVWVqKUWHPajmy9BpJqw02uItvW3v/1NlNRDVQF4+eWXRSm1MJsNGt2ZokYQqafG" +
30
+ "mwNJA8SPmD9/vqqjANOOn0++bimLximUN6EODrsd7vjRRXD1tLOlnmPqL3GDYMGKNWT8hKokkknY8O7HoiYvphSAj3cfECWCUI9dVTWiJC+mE4DFz7xAW34J" +
31
+ "zbj8gac0eR2nF6YTgNf/8bkoEYT6/HOvvpuC1MZ0AhCMREWJILThgfV/kXYUYCoB+PFjz0o9HCOMybuf7RYl+TDVK47xP72bJZKkAUeTm5EGJTmZkJ3mh3Sf" +
32
+ "F7ICfkjzecDt7HknG65wh/hIqjkYgsbWDmho64CKI3XKNeLEVEgaRNRUP2rMT+6yrPVjHPuA1wNZ3NALMtNhQmkh3Df/spQ/P3SJ3X24Fg7WNUJTewc/QhBP" +
33
+ "6JNXz0hM/5fT4LFr/006ERjyD1q9ejX7y1/+ImrqkORf80BaCbS6rOf1h0ktvfEwBOIhSI93gI+X7Vrkt+GCE7K7ocPpU+47no2wM04vnMk4jGmrBG/SOKOk" +
34
+ "zZs3D9l+h/wPzJo1iyVU7iFCDq+S2ssqef1sjEFWrB1yI03g54Zv0y2h1dcw/i2a3enQ5M5UPOasB4OSUB3khxtFXX/y8vLgt7/97ZBseEiS/sQTT6hu/Aj2" +
35
+ "PlYwfnyS6dzwJ7Tug+HBw7zXDxrC+BH8HtnRVhjZfkjpCbOibWBnSfGpFbBBm9NYwldfXy9Kg2dIAvDWW2+Jkrp0uOT3+vMkolAcrIFR3MA8OMzkowAjgtOP" +
36
+ "NC5MwzuqYCQ/fImw+ER+gk6vMhIyEldfffWQGsqgBeDRRx/VrIUGJc/nnxdphrFtByCfD/mN1bx6pnO00gHjWg9AAR8Wm+V7DwVMLx7ko1Ej0dzcDH/6058G" +
37
+ "bYuDFoAPPvhAlNQFF56YTnnd1YdBWagGSoNHlMU+M4JTg+JQrSJguEApO60GXP/gAiBKA2fQwo1BPkVRVapag7Dq470QTcg133Ta7XDJSSVwZmmOuGJ+8Blt" +
38
+ "/Owg7KiVNxtzSYYPrj59NPhcxuqUBpt23BQjtwnXLmGyvYuWdWMJ8pPHf8fe2v6lqMkFxgnY9cz90jw7w7/Yvfe5PzPcuSYTp40eJkpysvqmBTbcrCQjsrVF" +
39
+ "wwvAjsrDwAy6Ij4YHHYbbL77eml7/y4+WHGntL/xqb+8JU2DNLwAVNYaZ+NFKjh1lNy9/9FMHF4sSnKx67A8gUIMLwCN7UFRMj9Ohx023rVQ+t6/iz/fc4Mt" +
40
+ "M2Cs12ap4IBEnZLhBUCmxb+xxZjHwFp8/4xviJI8HKojASAGwSkjSkTJOjy4YIbN55ZrGze6T8sCCYCGjCrKEyVrcfGZ3xQleVjyh8HvvjMS/ZqP3nPPPWz7" +
41
+ "9u2iph1hhwe+zBglauZnWLAaciLybpLpCfQe3Jsm1+In+kOgU5RRufTSS2HBggV92ne/RgCfffaZKGmL0RwvhopNnreZAwK9Gr0JuaINoV+Akdm2bZso9U6f" +
42
+ "AlBeXs70eg+PKcBkIm6X1aehd+y8/WRHm0VNDhK8czJygJTW1lZR6p0+f0F/lUQNZBsBxGz6ZpzVE4wfINPTxBFA1ODP86GHHuqz5+5TABob9XvlIdsIIKZz" +
43
+ "ymk9cSdjfCQgkT8Hb5oxh1tUjMnHH/ed2qxXAVi2bJmus1YMjSUTUd5g9M47ryfFoXol2AlGEjLz4cA4jYmI+FXGBafuW7Zs6dWIeu1if/7zn7O4jkk4G6IM" +
44
+ "ttbL43yBajs50wZjA8adO6pNhD/OcIJB51M15wgPoyJ5HTbwmOAxlpSUwPXX9+x7YvgnIFso8FNHlsGWJYvM2fIJ6bBuV6QT/7f/EDz1yttyzW0I00ICoAPL" +
45
+ "N22VOt8cYR4MLwCYEUdG1vz1PfjPp54jESB0xfAC4HLKu3nm1Y8/g+8sfog9uP4VEgJCFwzfvX7rhntZa1D+aLOYzHPat06GJxbOoQVCQjMM39guXvpL9mWV" +
46
+ "PBFY+gKnPONLC+F7k06Gm2dMIzEgVMXwDWz+IyvZe7uM63WlJll+LxRmpkERP75RVgCL56Q+GzBhbY5rULfffjvbvXu3qOlPjTcXjvjQj57aPm5ACcQ6wJeI" +
47
+ "gJ8fHn64knFld5pRcggSxuXCCy+E66677hhDOm4RcM+ePaJkDLChozcZ0Zkmvc2VBrVcFPcHSpRYCV9kjIaKtGFQ7cuHZle6st1YNicqIjXs2LFDlL7mOAEw" +
48
+ "Wghu7OWod+uZhN2hJE9FUahMK4FdGSPh88wx/DwKDgSKlRFUizvd0j4IRCeHDh0Spa85RgDWrl1rOEtThrgkAP0Ce/4Ev1to7CGHB5rdmcr06aCvUBkl7E0r" +
49
+ "gypfATR6Mr9KuY5+7YR1OUYAvvzSeOmcsPd3chEgBgsXBS4IGF4Npw/13hw46C+GivQRsD+tFA4HiqDekwUhLghKgAtJN14RnaxZs+aY3vQYAaioqBAlY+GP" +
50
+ "h0SJSBXYCkIOLzTyUUKVvwh2cUH4LGs87EwfxQWiCJr49Yjd2P7uxMDp3skfIwAJg8bgT+MCQP2S+qAoRBxuPkXIgoOBYtidMUJZaDwQKIE6PnLAEUSMTxsI" +
51
+ "87J3715R6sQ0dnX+7cvY/nq54sqZjQyfB4qz0mF4biaMKciBW+fNIF02OaZ5gIufeYG9+OE/RY0wCrhr8cLTT4GbLvseiYEJMdVDky04iGxk+L1K8tNzvzke" +
52
+ "fjztbBIEE2CqhzT+pz9jsuVnlxGHww6Zfh9kp/nhrAmj4d4rLyUxMCimejBXLl/N3v/CmG8qiJ4JeD3KVOGMsSPgzh9dRGJgIEz3MGgaYH6G5+fAd0+dAEvn" +
53
+ "/pDEQGdM9wDOvOkB1ihRdlark8GnCt85aTT85j/mkRjogOlu+m0r/8g2ffS5qBGy4HTYIZuLwcSyfDh73HD4yaXfJ0HQAOUm33rrrcyouwC7g+m10OGFnFvk" +
54
+ "BX0/PIkopMU7IDPaDv540Hw9lcG54IILYOHChZ3ZDWtqzBNxx8XikM4bBiEv6PaMzkx1nhzYkz5c8Vtodmco14nU0GXzigC0t7crFbOQF25SvAQJa4Cei7gd" +
55
+ "eWfmGKjyFyriQAyN2tpa5XyML4BZ8CfCUBhuEDXCKmBy1XpPNuzlo4JKLgjom0CjgsFRXV2tnE0pAEh2pBm8fJ5IWI+4zQFNfEqA8Q0qA8UQ5CMEejc8OEwr" +
56
+ "ALhQVBiqEzXCqrRwIditxDYoU2IeEAPD/vzzz5tWPLNibZAWD4oaYWVa+XQAhQCnBkGHV1wl+sL20ksvsY8++khUzUdDjME7DUkIkYsAIcC03d9It8NInzlS" +
57
+ "eOvFfffdJ0f8p9Vb/8Ye2fgakKMQcTSnjSqDzT+jVOy9Ic3NWfrHF1n5tr+LGkF8zeypZ8IDCyh4yYmQ6qbMenAl+7TCmlmEiN4ZWZALbzx0C4lAN6S7IWct" +
58
+ "fojVtbSJGkF8DWaavnjyN2HFT39EQiCQ8kac8V/3s+Z2ejtAnJgpp4yD3y3+MYkAR9qbMJmLQBOJANEDZXnZ8PYjt1peBGzr1q2TchNVXUcYVn+8F9qi5DNA" +
59
+ "nJiA2wk/mFACpxVliSvWYvbs2TbbzJkzpd1FiTHu96YNU1JgEcSJwB2lZe3VkB1rFVesQ2lpqXm3AvcH9Ckf21YJnmRMXCGIY0FnooNpnf4EVgMTAUm/T8rF" +
60
+ "jX9c2wHIiJnL5ZnQDkyqio5Fba6AuGINksmk/AKAOJJxGN5+GHKirfKuehJDImFzdPoRWGgkwBizhgAgDj7YK+s4DHmRJmXeRxDdQTfjfYFSJe6AFbDb7dYR" +
61
+ "AAR7/5JgDZTyw8nIb4A4How1ia7FVog56XA4rCUAXeREmmF8617I5lMCgugOuhNX+QpETV4sKwAIxhQc1lENZaEa5W0BQRxNsytdSYkuMy6Xi9bEulj42Gr2" +
62
+ "xo69kGS0PkB04ve4YPtvfi61jZAAdOMH9zzBdh46ImqE1SnKzoD3lt8hrZ2QAPTABT97jFVUU8xBAuCWmRfAoh+cK6WtWHYNoC/+ev/Nthunnw9jS+RfDCJ6" +
63
+ "57m35A00QyOAfvD0q2+zLe//A/YeqaewYxblmgunwJ0/ulg6eyEBGCD/vWoDe+3THRCK0JsDK+F1uWDHSvkWBJUfJKtLsBrgS4K2aAy2H2mGL+raYF8T+RhY" +
64
+ "heFZfrh28hiwyRFLt9MduLy8nG3atElcIgYC7h8POr3Q6gxAszsdEnYXbTKWGgbjWw+ALxEWdXOzefNmm33evHk0DRgkDpaA9FgHlIZqYVxbpZKpyJukqYG8" +
65
+ "2KDRk6mcZYHeAqQIdzKmJCyd0LIXJrZUwLDgEUUcyPFILjAnYdzuEDXzQwKgAhiDAP0NRnVUwfiWfYoYYBozSmlufnDah2nIzE7XOgYJgIrYWBI8fEqAYjC8" +
66
+ "vQpGdhyCIj5N8MdD4ORiQHMvc4J+Aug6bGays7OVsyIA6BRAqAsauz8eVqYJGKFofOt+KBXTBCdLdL5eIExBh8tv+sAhhYWFylkRgKKiIqVCaIeLxSGXjwxG" +
67
+ "tx+Eic27YWLLHhjDhaEkVAsZ0TZlhEAYE4wjaHYBKC4uVs6KAHRVCH3A0YGLjwLS+NQgP9wIwzuqvxoljGo/BAWhesjkouDjIwhHko8WCN3pkEQAvpqGLlmy" +
68
+ "hMagBgYfTlucQQe3/+YYg8YYQCsfJESSDHgV4snOP0Nog98BcEmBA5wmXcjB1OB4NunXJ7rz21ffYZ/sOQCfV1ZDdVOLEvCRUI+A1wP/9+t7TG8/JAAS88sX" +
69
+ "/4eLQiWgMISjlBshlaDhLLrku7B4xjRT2xAJgMW497k/s0MNzXCEjxIO1TdBS0dIfEIMlDPHj4R1t19LAkCYlyf//Cb7sqoG9h2p46LQCsFIFKKxOK0n9IPM" +
70
+ "gA8+fWIJCQAhF49teZ19zKcNXxysphFCL+Buuj2rHpBHAOhNANFFkrcEbAxtCYCGSBLqY6AcLfjKgeiEMbgo3wHZbnNpQNcbAOSYby5zpmBiaCR5bxe1uZSs" +
71
+ "ORGnF8ION0TtbgjxwwpJNHoC92mYKe8k5gLYsGHDV3Z/jC+A2+0WJYI4Fjvv7dDVOT0ehLxwI5R1HOGN/6BiAIWhesW/wWbBV48Rk6WeHzdunCh1cowAdP+Q" +
72
+ "IHoDDd6fCENRuF7ZuTihdS8vNyhxEqxC1OFRtgabhQkTJohSJ8cIwPjx40WJIAaOB2MihOrgpNb9UMBHCVbwZ4jy6U/SdowZGZoFCxYco1bHfPMrr7zSPFJG" +
73
+ "GBYnF4LiUC2M4VOEzvyL8k4NInY3JEwkAN057pvjIoGuYKACMx/EV3gTERjecRjKgrXKirmM4KIoM4kAjBw5UpS+5rgWe9ddd7GdO3eKmnaE+Vyq3RUwf1/B" +
74
+ "Gzr692fEOiw1F+6LJk8mVPrl8zq18RY7jk95fFzsjM706dPhqquuOsbmjxMADBH+wgsviJp2VPvyodabK2rmBuMAFvNeLy/SJK4QSJWvEOq9nZFo5AEFoBL8" +
75
+ "CeNvmMIowKL4FceNXTBWuChqipnnUd3BVeFGT5apFoe0oDRUA+kmemfeP2zKHgmj09PU3jDf/NHntrCVb3wkanJQsfpB47cMjXnu1TfYko1viJoc3Hv5eTDv" +
76
+ "4u+Z8lkbpou6de4M6Yzl8T/9D+2s7Mbci863jSiQY6rXhVmNHzHUGDXN5xElOXj/iwpRIo7mW2OGixKhN4YSgDFF+aIkB59WVIoScTTjKOW6YTCUAAzLzxEl" +
77
+ "OcCwXHc+u5mmAd1YePG/0tqIQej1QWjtHvx5O4N/tsqVf9/NJfb7eXZIM2v0SBWojjDY1iDPc56aY4cyr3Gf79Huv93p9VvPnTuXhcPaZUJtc/phb7p880PM" +
78
+ "DITpwYhOWtwZsD9QImrmB3M7YIIXIzJ27FhYtmxZj3be6xTgrLPOEiVtQGcSGbeMNisJJa3rM9+dqMnTanXHyG7QvRk/0qsA3HjjjZqOa3ALrYfJ50GGG4Jq" +
79
+ "PdkSu8T0H9wkJZsY4nZgI9KVALQ3+lwEzMzEfOjagMk0MbCEjDR4cxRfB6vD7A4IObyiZn7Q+I06Apg8ebIo9UyfAnDeeeeJkvqgXqXxuZSmww6NwJ7viDdP" +
80
+ "8R+3MhGbE9pcflEzPzZu+0YdAdx55519mlKfAoAxAvozlEgVGHIKRwIyggkla3xy7XUYKLjQ2yn1cmDn01Z0/jIaWVlZotQ7fQoAMmnSJFFSH1cyBp5EVNTk" +
81
+ "o9GdCXUeufY79Je43QVNnv41TLPg5u0V4yUajWnTpolS7/RLAJYs0Tb5QcAErpVD4bC/AJq4EFiNal8eRBxyBZ7FQKlGi/uAI/Y5c+b0y2b7JQBIf4cUqQCn" +
82
+ "AQ5JpwFdHAoUQb1kvWFvtLrSoMmVIWrygFGP7AZrq2eeeaYo9c2AenatdgZi7ok36pPQaIEkFKem2+Eb6fLMiU9EKMHglbokRCTU9PNy7VDkMdbz623nX3cM" +
83
+ "2/JuWbWB/emDf4ia3Jw2qgw2/2yRtCow9bZlrKqhWdTkwuwxH/o9BdCaX1wzS1qD6M7/7jsE37nlIbb29fekG/KcedMD0hp/ht/8+xkMKwDIhLIiUZKf2uY2" +
84
+ "eHjDa/AfvymXQgSWb9rKTll4D2tsM+Ye+VQwXALvVUMLwCXfPlWUrEE8kYDXPtkBJ123hM1Z9owpheC3r77DvrP4IfbUK29DOBYTV+VEhg5qUMPsOXPmsEhE" +
85
+ "/TDIGCh0R9Z4S++hxweE3oQ50RYlDZfRnKVwh2PQ6YVWd4ayx0Gm4K69wp9Dacg4kZ9LSkrgySefHLA9D0oAnn/+ebZhwwZRU5eKtGG0h56Dr5rcLA6uZBx8" +
86
+ "8ZDy/hk3obgSMeWsBbjnLeFwKgkxcT9/mytN8exD5x6rRUB2JBMwtr1SeQ1oBE4U8rs/DOovIfPnz2ft7eqHeMb3x/vTyiw9CugJNHzMv+fmYuDlZXciquyi" +
87
+ "xK2puJ0aTbLLWQXv3wkfNr+Igwr+p3j5613tSqhrbtRo9GFu8LiBJ8bPUX4OOTydf97CBOJBGNtmjJBvo0ePhuXLlw/qgQz6Kb700kvs2WefFTX1wIb2ReYY" +
88
+ "JQUT0T/woeKIAXeooYs19lZ2HKx3mz5gLYm9N/8baPAJ3pvj/cYyprvC68SJwYzImBbdCAy290cGPW6bPn26LTdX/fDO2CfhHJjoP2jYOBeP8h47qAzVA9Di" +
89
+ "SlcCkxx9YGQedM7pcPqUIT3+eRRaFAIy/p5BcQ0YxG29Py6/vTHkpzxz5sxjuxUVwIa5N22YdPvICXOCW9VHtFcZwgdgKL0/MuSVm1GjRomSeuBcN0tJM00Q" +
90
+ "+oMxK4xg/BdeeKEoDZ6UjPO0GAXE+LC0ImOksgJNEHoyXskGrF2w3BPh8/mgvLx8yPabknc35557riiph4srbi5l2yV0Rnn9aoB4FakwfiQlAoDBQz0e9dN6" +
91
+ "4WIgrmgThF7khRoAl0j1pLS0VJSGTkoEAJk1a5YoqQfGCMiPNPKS6jMOgjgOfKWabYC1qF/96lcp6f2RlP1DWvLdO5azyjoUAoLQjp9+/xy4Y9ZFprSZnjDt" +
92
+ "jxl7zd0Mc+8RhBbkZaTB3x+7SyrjR1I2BdCauf/6bVEiCPW5aPI3REkuTK1oZ//3I+xIU4uoEYQ6FGVnwnvLb5eu90dU+1Fa7A3AyLqVgWJRIwh1yI00Q2nw" +
93
+ "iG69pcvlgvXr16vy36s2BbjgggtEST2yoy2QFg+KGkGkHkcyDkXhBl2HymoZP6KaACxcuNCGQQrUpowrsycpbyIRQk+YktbdqVG8hRNxxhlniJI6qC5ss2bN" +
94
+ "YomEupt30NMNpwJWC0pBqEtGrB1GtR8SNe3BXBxr1qxR1UZVt5i5c+eKknpkxtqgkA/TCCJVYKCV0mCNqGkPZvdR2/gR1QVgxowZtlNOOUXU1CMv0miIXVqE" +
95
+ "HJSG6jQLtXYipk+fLkrqosmYGTOV5OXliZo6YIJGVGx01iCIoZAXboRMHTuTCRMmwIIFCzRZd9TkP+mCTwdYOKyuGyVGwMHgIQm7Q1whiP7jSUSUWH+4718P" +
96
+ "CgoKYOXKlZrZpaYCgFx++eWqb+Ftd/pgX/pwCmtFDAiMuDy27YBuQ/9U+fgPBM2XzefPny9K6pEWDykhm7oHwSSInsAev4xPIfUyfrvdrrnxI5oLwKWXXmqb" +
97
+ "OnWqqKkHvsIZFqzmP5BEgOgD3lEUhep5m2kTF7Rn48aNugxXdRsjb9q0icU0SB31cVUjvLSzChJJEgLixEwdmQ8XjtNvS/ns2bN1s0Pd/mOtGXfN3SxJUwKi" +
98
+ "G+jl9+T1cy1jB93RfAqgF7tXPWALeNUPW0aYA9xoM2vKGZY2fsQyAoDceckUyJIgpzsxNND4L588ER7+8eWWNn7EMDdAq1yDmMjygL+YEo5aGHTw0TPbFAb1" +
99
+ "TGVcv6FgKAWcN28eC4XUT7mETkM13jyo9WRjdyCuErLTtb8/S8fV/sGm8VYLw7V+LbwHEVwOrPfmQK03F+I22jUoO/5EGIqCtUpaL73QwrtvoBiy+9Nit2AX" +
100
+ "HS4/HPYVKFuICTnJjLVDSahW14QemDfj+eefN5y9GVIAEC1FAFNiH/YXQD1OCQiJYNzw6yA/rG8IeT22+PYXwwoAotV0QMFmgyZXOtT48in/oAR442EoDdcp" +
101
+ "iTz1JBAIwB/+8AfD2pmhBQDRVAQ4YacXary50MzFgDAfuPU7nRt9cbAGPDr68yOZmZmwdu1aQ9uY4QUAmT17NotGtZu/4ZSg1psNR/howCS3iODgHL8oXK+k" +
102
+ "ktf7qeXk5MCqVasM33hM07q12idwNDG7U3lLUO/OUqYIhDFxJ2Lc8OsgG1/vGWC798iRI2HFihWmaDCmatW33HIL27dvn6hpR9jhgWpvHrS70iBJQmAY3Mko" +
103
+ "5ERaID/SBHamb8beLs466yy47bbbTNNITNealy9fzt5//31R0w40/HZnAOo82dDhCij7CAh9wA09udEWyIo0g1fneX4XuL14zpw5cMUVV5jKpkzZnb344ovs" +
104
+ "97//PR/t6WOGTZ5MZaEwYneLK4QWYMQezNKD23hdLC6u6o+RX/P1hSm/tJFY/MwLbOsnOyCsQWwDq3LaqDLY/LNF1FZVgG5qivjF5r+yt7Z/CZ9XVosrxFDI" +
105
+ "9Ptgyilj4YmFc6iNqgjd3BTz2LoX2btfHoA9NQ0QisaBgpD0HzufRxdnpcM5E0bAAz8lw9cCqW6yVt6E/SHscEObKw1aXenQ4fAAo7RlvYI79nB+j/v2bQZe" +
106
+ "YjXL+/3+Ip3K3nbbbWzPnj2ipj/YlBM2BwRdPmhzBrggpEGUthor0XcD8aDinReIhZRXekbnnHPOgZtvvlkqm5FymLVu3Tq2ceNGSCaN8W64CxSDODf+MD/a" +
107
+ "3WkQsnsUMYjbHYpIyAyu4KORp8dD4Oe9PQoAruRjRiejY+ZV/r6Q8kd1ccMNN7CqqipRMx7Y9MMOL0T4dCHEzyGnR3FESoADmB13tRt5MHw82JjwG9tYUjFs" +
108
+ "NHLMtOPjh1ccemXcGSyTJk2CpUuXSmsnUgsA8sILL7BNmzaBFiHIUwWOBnD3YYfTBx0OnyIQyigBQzgabCciGrQzwQ2dxRQD98fD/BzmPX7CZPJ1LBkZGfDs" +
109
+ "s89Kbx/S/8Au7rjjDrZr1y5RMw/KKIAbvZLmTJwTNrsiEhjJKGF3KmcUiDi/nuTlEBePJL/elRptsGaIBoxG7ebDdwc/XNzY8ezmQ3fcf985hOfTLN7bKz2/" +
110
+ "+Htm59xzz4Ubb7zRErZhGQHowkhvClJNjBs/xjvENw5o/Cgc+IiVMp9S4Bm3NDM+kkjgGd9McONFI+aTDjEfx087h/EOHMqj8fPr2NPj35SZ/Px8ePrppy1l" +
111
+ "E5YTAOTxxx9n77zzjqgRVsflcsGMGTN0zdCjF5YUgC4WLVrEjhw5ImqEFZk4cSLcf//9lrUDSwsAsn79erZlyxbQMuAIoT+ybegZLJa/AV387ne/Y6+99hpE" +
112
+ "IhFxhZCR7OxsZbh/ySWXUNvn0E3oxtq1a9nWrVtpRCAZGJP/sssug+nTp1ObPwq6GT2wefNmTeMQEuphxcU9giCIPiFlHCC4aKhXJCKid5xOp+lCcukN3axB" +
113
+ "gAuGb775JrS16ZdkkviawsJCmDZtGsycOZPa8wChGzZE7rrrLrZz505RI7TCbrfD5MmTcYs3teEhQDcvRTz33HPK7sLa2lpxhVCDMWPGwNSpU+GHP/whtd0U" +
114
+ "QDdRBZ5++mkldDlNEVIDDvExGMfcuXOpvaYYuqEq89RTT7EPP/yQxGCAFBQUwNlnnw1XXnkltVEVoZurIbh4+NFHH8Hhw4fFFaILTKwxbtw4+Pa3v02LeRpC" +
115
+ "N1pH0CsRFxCtum4wYsQIOPnkk+Haa6+ldqgTdOMNBG5D3r17N1RUVJgqglF/wDz548ePV3p52plnHOhBGJzy8nIlIer+/fuhsbFRXDUuOJQvLi6GUaNGKQcN" +
116
+ "540NPRwTg/EOGxoa4OhD7RTq6Eabl5ennHNzc5Xjsssuo3ZkUujBSQ46NXV0dCjejSc6sMd2u93K4fF4vip7vV5IT08nt1mCIAiCIAiCIAiCIAjTA/D/Abmz" +
117
+ "ZXM4My2jAAAAAElFTkSuQmCC";
@@ -3,6 +3,7 @@ import type { PDFGenerationData, PDFGenerationRequest, ReportModule } from './re
3
3
 
4
4
  interface Env {
5
5
  BROWSER: Fetcher;
6
+ PDF_WORKER_AUTH: string;
6
7
  }
7
8
 
8
9
  const DEFAULT_REPORT_FORMAT = 'striae';
@@ -15,9 +16,12 @@ const reportModuleLoaders: Record<string, () => Promise<ReportModule>> = {
15
16
  const corsHeaders: Record<string, string> = {
16
17
  'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
17
18
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
18
- 'Access-Control-Allow-Headers': 'Content-Type',
19
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key',
19
20
  };
20
21
 
22
+ const hasValidHeader = (request: Request, env: Env): boolean =>
23
+ request.headers.get('X-Custom-Auth-Key') === env.PDF_WORKER_AUTH;
24
+
21
25
  function normalizeReportFormat(format: unknown): string {
22
26
  if (typeof format !== 'string') {
23
27
  return DEFAULT_REPORT_FORMAT;
@@ -71,6 +75,13 @@ export default {
71
75
  return new Response(null, { headers: corsHeaders });
72
76
  }
73
77
 
78
+ if (!hasValidHeader(request, env)) {
79
+ return new Response(JSON.stringify({ error: 'Forbidden' }), {
80
+ status: 403,
81
+ headers: { ...corsHeaders, 'content-type': 'application/json' },
82
+ });
83
+ }
84
+
74
85
  if (request.method === 'POST') {
75
86
  let browser: Awaited<ReturnType<typeof launch>> | undefined;
76
87
 
@@ -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-12",
5
+ "compatibility_date": "2026-03-13",
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-12",
5
+ "compatibility_date": "2026-03-13",
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-12"
3
+ compatibility_date = "2026-03-13"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6