@striae-org/striae 5.1.0 → 5.1.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/.env.example CHANGED
@@ -47,7 +47,6 @@ PAGES_CUSTOM_DOMAIN=your_custom_domain_here
47
47
  KEYS_WORKER_NAME=your_keys_worker_name_here
48
48
  KEYS_WORKER_DOMAIN=your_keys_worker_domain_here
49
49
  KEYS_AUTH=your_custom_keys_auth_token_here
50
- ACCOUNT_HASH=your_cloudflare_images_account_hash_here
51
50
 
52
51
  # ================================
53
52
  # USER WORKER ENVIRONMENT VARIABLES
@@ -70,7 +69,6 @@ MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
70
69
  EXPORT_ENCRYPTION_PRIVATE_KEY=your_export_encryption_private_key_here
71
70
  EXPORT_ENCRYPTION_KEY_ID=your_export_encryption_key_id_here
72
71
  EXPORT_ENCRYPTION_PUBLIC_KEY=your_export_encryption_public_key_here
73
- DATA_AT_REST_ENCRYPTION_ENABLED=true
74
72
  DATA_AT_REST_ENCRYPTION_PRIVATE_KEY=your_data_at_rest_encryption_private_key_here
75
73
  DATA_AT_REST_ENCRYPTION_KEY_ID=your_data_at_rest_encryption_key_id_here
76
74
  DATA_AT_REST_ENCRYPTION_PUBLIC_KEY=your_data_at_rest_encryption_public_key_here
@@ -88,6 +86,9 @@ AUDIT_WORKER_DOMAIN=your_audit_worker_domain_here
88
86
  # ================================
89
87
  IMAGES_WORKER_NAME=your_images_worker_name_here
90
88
  IMAGES_WORKER_DOMAIN=your_images_worker_domain_here
89
+ IMAGE_SIGNED_URL_SECRET=your_image_signed_url_secret_here
90
+ # Optional: defaults to 3600 and max is 86400.
91
+ IMAGE_SIGNED_URL_TTL_SECONDS=3600
91
92
 
92
93
  # ================================
93
94
  # PDF WORKER ENVIRONMENT VARIABLES
@@ -965,7 +965,24 @@ For questions about this export, contact your Striae system administrator.
965
965
  */
966
966
  async function fetchImageAsBlob(user: User, fileData: FileData, caseNumber: string): Promise<Blob | null> {
967
967
  try {
968
- const { blob, revoke } = await getImageUrl(user, fileData, caseNumber, 'Export Package');
968
+ const imageAccess = await getImageUrl(user, fileData, caseNumber, 'Export Package');
969
+ const { blob, revoke, url } = imageAccess;
970
+
971
+ if (!blob) {
972
+ const signedResponse = await fetch(url, {
973
+ method: 'GET',
974
+ headers: {
975
+ 'Accept': 'application/octet-stream,image/*'
976
+ }
977
+ });
978
+
979
+ if (!signedResponse.ok) {
980
+ throw new Error(`Signed URL fetch failed with status ${signedResponse.status}`);
981
+ }
982
+
983
+ return await signedResponse.blob();
984
+ }
985
+
969
986
  try {
970
987
  return blob;
971
988
  } finally {
@@ -685,7 +685,23 @@ const getVerificationPublicSigningKey = (preferredKeyId?: string): { keyId: stri
685
685
 
686
686
  const fetchImageAsBlob = async (user: User, fileData: FileData, caseNumber: string): Promise<Blob | null> => {
687
687
  try {
688
- const { blob, revoke } = await getImageUrl(user, fileData, caseNumber, 'Archive Package');
688
+ const imageAccess = await getImageUrl(user, fileData, caseNumber, 'Archive Package');
689
+ const { blob, revoke, url } = imageAccess;
690
+
691
+ if (!blob) {
692
+ const signedResponse = await fetch(url, {
693
+ method: 'GET',
694
+ headers: {
695
+ 'Accept': 'application/octet-stream,image/*'
696
+ }
697
+ });
698
+
699
+ if (!signedResponse.ok) {
700
+ throw new Error(`Signed URL fetch failed with status ${signedResponse.status}`);
701
+ }
702
+
703
+ return await signedResponse.blob();
704
+ }
689
705
 
690
706
  try {
691
707
  return blob;
@@ -6,6 +6,7 @@ import { fetchPdfApi } from '~/utils/api';
6
6
  interface GeneratePDFParams {
7
7
  user: User;
8
8
  selectedImage: string | undefined;
9
+ sourceImageId?: string;
9
10
  selectedFilename: string | undefined;
10
11
  userCompany: string;
11
12
  userFirstName: string;
@@ -44,6 +45,10 @@ const resolvePdfImageUrl = async (selectedImage: string | undefined): Promise<st
44
45
  return selectedImage;
45
46
  }
46
47
 
48
+ if (selectedImage.startsWith('/')) {
49
+ return new URL(selectedImage, window.location.origin).toString();
50
+ }
51
+
47
52
  if (selectedImage.startsWith('data:')) {
48
53
  return selectedImage;
49
54
  }
@@ -64,6 +69,7 @@ const resolvePdfImageUrl = async (selectedImage: string | undefined): Promise<st
64
69
  export const generatePDF = async ({
65
70
  user,
66
71
  selectedImage,
72
+ sourceImageId,
67
73
  selectedFilename,
68
74
  userCompany,
69
75
  userFirstName,
@@ -187,7 +193,7 @@ export const generatePDF = async ({
187
193
  processingTime,
188
194
  blob.size,
189
195
  [],
190
- selectedImage, // Source file ID
196
+ sourceImageId, // Source file ID
191
197
  selectedFilename // Source original filename
192
198
  );
193
199
  } catch (auditError) {
@@ -215,7 +221,7 @@ export const generatePDF = async ({
215
221
  processingTime,
216
222
  0, // No file size for failed generation
217
223
  [errorText || 'PDF generation failed'],
218
- selectedImage, // Source file ID
224
+ sourceImageId, // Source file ID
219
225
  selectedFilename // Source original filename
220
226
  );
221
227
  } catch (auditError) {
@@ -242,7 +248,7 @@ export const generatePDF = async ({
242
248
  processingTime,
243
249
  0, // No file size for failed generation
244
250
  [error instanceof Error ? error.message : 'Unknown error generating PDF'],
245
- selectedImage, // Source file ID
251
+ sourceImageId, // Source file ID
246
252
  selectedFilename // Source original filename
247
253
  );
248
254
  } catch (auditError) {
@@ -1,7 +1,7 @@
1
1
  import type { User } from 'firebase/auth';
2
- import { fetchImageApi, uploadImageApi } from '~/utils/api';
2
+ import { createSignedImageUrlApi, fetchImageApi, uploadImageApi } from '~/utils/api';
3
3
  import { canUploadFile, getCaseData, updateCaseData, deleteFileAnnotations } from '~/utils/data';
4
- import type { CaseData, FileData, ImageUploadResponse } from '~/types';
4
+ import type { CaseData, FileData, ImageAccessResult, ImageUploadResponse } from '~/types';
5
5
  import { auditService } from '~/services/audit';
6
6
 
7
7
  export interface DeleteFileResult {
@@ -255,20 +255,49 @@ export const deleteFile = async (
255
255
  }
256
256
  };
257
257
 
258
- export const getImageUrl = async (user: User, fileData: FileData, caseNumber: string, accessReason?: string): Promise<{ blob: Blob; url: string; revoke: () => void }> => {
258
+ export const getImageUrl = async (
259
+ user: User,
260
+ fileData: FileData,
261
+ caseNumber: string,
262
+ accessReason?: string
263
+ ): Promise<ImageAccessResult> => {
259
264
  const startTime = Date.now();
260
265
  const defaultAccessReason = accessReason || 'Image viewer access';
261
-
266
+
262
267
  try {
268
+ try {
269
+ const signedUrlResponse = await createSignedImageUrlApi(user, fileData.id);
270
+
271
+ await auditService.logFileAccess(
272
+ user,
273
+ fileData.originalFilename || fileData.id,
274
+ fileData.id,
275
+ 'signed-url',
276
+ caseNumber,
277
+ 'success',
278
+ Date.now() - startTime,
279
+ defaultAccessReason,
280
+ fileData.originalFilename
281
+ );
282
+
283
+ return {
284
+ url: signedUrlResponse.result.url,
285
+ revoke: () => {},
286
+ urlType: 'signed',
287
+ expiresAt: signedUrlResponse.result.expiresAt
288
+ };
289
+ } catch {
290
+ // Fallback to direct blob retrieval during migration.
291
+ }
292
+
263
293
  const workerResponse = await fetchImageApi(user, `/${encodeURIComponent(fileData.id)}`, {
264
294
  method: 'GET',
265
295
  headers: {
266
296
  'Accept': 'application/octet-stream,image/*'
267
297
  }
268
298
  });
269
-
299
+
270
300
  if (!workerResponse.ok) {
271
- // Log failed image access
272
301
  await auditService.logFileAccess(
273
302
  user,
274
303
  fileData.originalFilename || fileData.id,
@@ -285,8 +314,7 @@ export const getImageUrl = async (user: User, fileData: FileData, caseNumber: st
285
314
 
286
315
  const blob = await workerResponse.blob();
287
316
  const objectUrl = URL.createObjectURL(blob);
288
-
289
- // Log successful image access
317
+
290
318
  await auditService.logFileAccess(
291
319
  user,
292
320
  fileData.originalFilename || fileData.id,
@@ -298,10 +326,14 @@ export const getImageUrl = async (user: User, fileData: FileData, caseNumber: st
298
326
  defaultAccessReason,
299
327
  fileData.originalFilename
300
328
  );
301
-
302
- return { blob, url: objectUrl, revoke: () => URL.revokeObjectURL(objectUrl) };
329
+
330
+ return {
331
+ blob,
332
+ url: objectUrl,
333
+ revoke: () => URL.revokeObjectURL(objectUrl),
334
+ urlType: 'blob'
335
+ };
303
336
  } catch (error) {
304
- // Log any unexpected errors if not already logged
305
337
  if (!(error instanceof Error && error.message.includes('Failed to retrieve image'))) {
306
338
  await auditService.logFileAccess(
307
339
  user,
@@ -241,6 +241,7 @@ export const Striae = ({ user }: StriaePage) => {
241
241
  await generatePDF({
242
242
  user,
243
243
  selectedImage,
244
+ sourceImageId: imageId,
244
245
  selectedFilename,
245
246
  userCompany,
246
247
  userFirstName,
package/app/types/file.ts CHANGED
@@ -12,8 +12,6 @@ export interface FileUploadResponse {
12
12
  id: string;
13
13
  filename: string;
14
14
  uploaded: string;
15
- requireSignedURLs: boolean;
16
- variants: string[];
17
15
  };
18
16
  errors: Array<{
19
17
  code: number;
@@ -27,4 +25,22 @@ export interface ImageUploadResponse {
27
25
  result: FileUploadResponse['result'];
28
26
  errors: FileUploadResponse['errors'];
29
27
  messages: FileUploadResponse['messages'];
28
+ }
29
+
30
+ export interface SignedImageUrlResponse {
31
+ success: boolean;
32
+ result: {
33
+ fileId: string;
34
+ url: string;
35
+ expiresAt: string;
36
+ expiresInSeconds: number;
37
+ };
38
+ }
39
+
40
+ export interface ImageAccessResult {
41
+ url: string;
42
+ revoke: () => void;
43
+ blob?: Blob;
44
+ urlType: 'signed' | 'blob';
45
+ expiresAt?: string;
30
46
  }
@@ -1,5 +1,5 @@
1
1
  import type { User } from 'firebase/auth';
2
- import { type ImageUploadResponse } from '~/types';
2
+ import { type ImageUploadResponse, type SignedImageUrlResponse } from '~/types';
3
3
 
4
4
  const IMAGE_API_BASE = '/api/image';
5
5
 
@@ -93,6 +93,54 @@ function parseUploadResponse(payload: string): ImageUploadResponse {
93
93
  return parsed;
94
94
  }
95
95
 
96
+ function parseSignedUrlResponse(payload: string): SignedImageUrlResponse {
97
+ const parsed = JSON.parse(payload) as SignedImageUrlResponse;
98
+ if (!parsed.success || !parsed.result?.url || !parsed.result?.fileId || !parsed.result?.expiresAt) {
99
+ throw new Error('Signed URL response is invalid');
100
+ }
101
+
102
+ return parsed;
103
+ }
104
+
105
+ export async function createSignedImageUrlApi(
106
+ user: User,
107
+ fileId: string,
108
+ expiresInSeconds?: number
109
+ ): Promise<SignedImageUrlResponse> {
110
+ const response = await fetchImageApi(user, `/${encodeURIComponent(fileId)}/signed-url`, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ 'Accept': 'application/json'
115
+ },
116
+ body: JSON.stringify(
117
+ typeof expiresInSeconds === 'number'
118
+ ? { expiresInSeconds }
119
+ : {}
120
+ )
121
+ });
122
+
123
+ if (!response.ok) {
124
+ throw new Error(`Signed URL request failed with status ${response.status}`);
125
+ }
126
+
127
+ const parsed = parseSignedUrlResponse(await response.text());
128
+ const rawUrl = parsed.result.url;
129
+ let normalizedUrl = rawUrl;
130
+
131
+ if (rawUrl.startsWith('/')) {
132
+ normalizedUrl = new URL(rawUrl, window.location.origin).toString();
133
+ }
134
+
135
+ return {
136
+ ...parsed,
137
+ result: {
138
+ ...parsed.result,
139
+ url: normalizedUrl
140
+ }
141
+ };
142
+ }
143
+
96
144
  export async function uploadImageApi(
97
145
  user: User,
98
146
  file: File,
@@ -82,12 +82,13 @@ export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Re
82
82
  });
83
83
  }
84
84
 
85
+ const requestUrl = new URL(request.url);
86
+
85
87
  const identity = await verifyFirebaseIdentityFromRequest(request, env);
86
88
  if (!identity) {
87
89
  return textResponse('Unauthorized', 401);
88
90
  }
89
91
 
90
- const requestUrl = new URL(request.url);
91
92
  const proxyPathResult = extractProxyPath(requestUrl);
92
93
  if (!proxyPathResult.ok) {
93
94
  return proxyPathResult.reason === 'bad-encoding'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "5.1.0",
3
+ "version": "5.1.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",
@@ -81,13 +81,23 @@ require_command grep
81
81
 
82
82
  is_placeholder() {
83
83
  local value="$1"
84
- local normalized=$(echo "$value" | tr '[:upper:]' '[:lower:]')
84
+ local normalized
85
+
86
+ normalized=$(printf '%s' "$value" | tr -d '\r' | tr '[:upper:]' '[:lower:]')
87
+ normalized=$(printf '%s' "$normalized" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
88
+ normalized=${normalized#\"}
89
+ normalized=${normalized%\"}
85
90
 
86
91
  if [ -z "$normalized" ]; then
87
92
  return 1
88
93
  fi
89
94
 
90
- [[ "$normalized" == your_*_here ]]
95
+ [[ "$normalized" =~ ^your_[a-z0-9_]+_here$ || \
96
+ "$normalized" =~ ^your-[a-z0-9-]+-here$ || \
97
+ "$normalized" == "placeholder" || \
98
+ "$normalized" == "changeme" || \
99
+ "$normalized" == "replace_me" || \
100
+ "$normalized" == "replace-me" ]]
91
101
  }
92
102
 
93
103
  # Check if .env file exists
@@ -626,38 +636,8 @@ configure_data_at_rest_encryption_credentials() {
626
636
  echo -e "${BLUE}🗃️ DATA-AT-REST ENCRYPTION CONFIGURATION${NC}"
627
637
  echo "========================================"
628
638
 
629
- local current_enabled="${DATA_AT_REST_ENCRYPTION_ENABLED:-}"
630
- local normalized_enabled=""
631
- local enable_choice=""
632
-
633
- current_enabled=$(strip_carriage_returns "$current_enabled")
634
- normalized_enabled=$(printf '%s' "$current_enabled" | tr '[:upper:]' '[:lower:]')
635
-
636
- if [ -z "$normalized_enabled" ] || is_placeholder "$normalized_enabled"; then
637
- DATA_AT_REST_ENCRYPTION_ENABLED="false"
638
- elif [ "$normalized_enabled" = "1" ] || [ "$normalized_enabled" = "true" ] || [ "$normalized_enabled" = "yes" ] || [ "$normalized_enabled" = "on" ]; then
639
- DATA_AT_REST_ENCRYPTION_ENABLED="true"
640
- else
641
- DATA_AT_REST_ENCRYPTION_ENABLED="false"
642
- fi
643
-
644
- if [ "$update_env" != "true" ]; then
645
- read -p "Enable data-at-rest encryption for new writes? (y/N, Enter keeps current): " enable_choice
646
- enable_choice=$(strip_carriage_returns "$enable_choice")
647
- case "$enable_choice" in
648
- y|Y|yes|YES)
649
- DATA_AT_REST_ENCRYPTION_ENABLED="true"
650
- ;;
651
- n|N|no|NO)
652
- DATA_AT_REST_ENCRYPTION_ENABLED="false"
653
- ;;
654
- "")
655
- ;;
656
- *)
657
- echo -e "${YELLOW}⚠️ Unrecognized choice '$enable_choice'; keeping current setting${NC}"
658
- ;;
659
- esac
660
- fi
639
+ # Data-at-rest encryption is mandatory for all environments.
640
+ DATA_AT_REST_ENCRYPTION_ENABLED="true"
661
641
 
662
642
  export DATA_AT_REST_ENCRYPTION_ENABLED
663
643
  write_env_var "DATA_AT_REST_ENCRYPTION_ENABLED" "$DATA_AT_REST_ENCRYPTION_ENABLED"
@@ -781,7 +761,7 @@ required_vars=(
781
761
  # Worker-Specific Secrets (required for deployment)
782
762
  "KEYS_AUTH"
783
763
  "PDF_WORKER_AUTH"
784
- "ACCOUNT_HASH"
764
+ "IMAGE_SIGNED_URL_SECRET"
785
765
  "BROWSER_API_TOKEN"
786
766
  "MANIFEST_SIGNING_PRIVATE_KEY"
787
767
  "MANIFEST_SIGNING_KEY_ID"
@@ -968,7 +948,6 @@ validate_generated_configs() {
968
948
  assert_contains_literal "workers/user-worker/wrangler.jsonc" "$KV_STORE_ID" "KV_STORE_ID missing in user worker config"
969
949
 
970
950
  assert_contains_literal "app/config/config.json" "https://$PAGES_CUSTOM_DOMAIN" "PAGES_CUSTOM_DOMAIN missing in app/config/config.json"
971
- assert_contains_literal "app/config/config.json" "$ACCOUNT_HASH" "ACCOUNT_HASH missing in app/config/config.json"
972
951
  assert_contains_literal "app/config/config.json" "$EXPORT_ENCRYPTION_KEY_ID" "EXPORT_ENCRYPTION_KEY_ID missing in app/config/config.json"
973
952
  assert_contains_literal "app/config/config.json" "\"export_encryption_public_key\":" "export_encryption_public_key missing in app/config/config.json"
974
953
  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"
@@ -989,7 +968,7 @@ validate_generated_configs() {
989
968
  assert_contains_literal "workers/user-worker/src/user-worker.ts" "https://$PAGES_CUSTOM_DOMAIN" "PAGES_CUSTOM_DOMAIN missing in user-worker source"
990
969
 
991
970
  local placeholder_pattern
992
- placeholder_pattern="(\"(ACCOUNT_ID|PAGES_PROJECT_NAME|PAGES_CUSTOM_DOMAIN|KEYS_WORKER_NAME|USER_WORKER_NAME|DATA_WORKER_NAME|AUDIT_WORKER_NAME|IMAGES_WORKER_NAME|PDF_WORKER_NAME|KEYS_WORKER_DOMAIN|USER_WORKER_DOMAIN|DATA_WORKER_DOMAIN|AUDIT_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN|PDF_WORKER_DOMAIN|DATA_BUCKET_NAME|AUDIT_BUCKET_NAME|FILES_BUCKET_NAME|KV_STORE_ID|ACCOUNT_HASH|MANIFEST_SIGNING_KEY_ID|MANIFEST_SIGNING_PUBLIC_KEY|EXPORT_ENCRYPTION_KEY_ID|EXPORT_ENCRYPTION_PUBLIC_KEY|YOUR_FIREBASE_API_KEY|YOUR_FIREBASE_AUTH_DOMAIN|YOUR_FIREBASE_PROJECT_ID|YOUR_FIREBASE_STORAGE_BUCKET|YOUR_FIREBASE_MESSAGING_SENDER_ID|YOUR_FIREBASE_APP_ID|YOUR_FIREBASE_MEASUREMENT_ID)\"|'(PAGES_CUSTOM_DOMAIN|DATA_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN)')"
971
+ placeholder_pattern="(\"(ACCOUNT_ID|PAGES_PROJECT_NAME|PAGES_CUSTOM_DOMAIN|KEYS_WORKER_NAME|USER_WORKER_NAME|DATA_WORKER_NAME|AUDIT_WORKER_NAME|IMAGES_WORKER_NAME|PDF_WORKER_NAME|KEYS_WORKER_DOMAIN|USER_WORKER_DOMAIN|DATA_WORKER_DOMAIN|AUDIT_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN|PDF_WORKER_DOMAIN|DATA_BUCKET_NAME|AUDIT_BUCKET_NAME|FILES_BUCKET_NAME|KV_STORE_ID|MANIFEST_SIGNING_KEY_ID|MANIFEST_SIGNING_PUBLIC_KEY|EXPORT_ENCRYPTION_KEY_ID|EXPORT_ENCRYPTION_PUBLIC_KEY|YOUR_FIREBASE_API_KEY|YOUR_FIREBASE_AUTH_DOMAIN|YOUR_FIREBASE_PROJECT_ID|YOUR_FIREBASE_STORAGE_BUCKET|YOUR_FIREBASE_MESSAGING_SENDER_ID|YOUR_FIREBASE_APP_ID|YOUR_FIREBASE_MEASUREMENT_ID)\"|'(PAGES_CUSTOM_DOMAIN|DATA_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN)')"
993
972
 
994
973
  local files_to_scan=(
995
974
  "wrangler.toml"
@@ -1250,6 +1229,58 @@ prompt_for_secrets() {
1250
1229
  fi
1251
1230
 
1252
1231
  # Function to prompt for a variable
1232
+ is_auto_generated_secret_var() {
1233
+ local var_name=$1
1234
+ case "$var_name" in
1235
+ USER_DB_AUTH|R2_KEY_SECRET|KEYS_AUTH|PDF_WORKER_AUTH|IMAGES_API_TOKEN|IMAGE_SIGNED_URL_SECRET)
1236
+ return 0
1237
+ ;;
1238
+ *)
1239
+ return 1
1240
+ ;;
1241
+ esac
1242
+ }
1243
+
1244
+ is_secret_placeholder_value() {
1245
+ local var_name=$1
1246
+ local value=$2
1247
+ case "$var_name" in
1248
+ USER_DB_AUTH)
1249
+ [ "$value" = "your_custom_user_db_auth_token_here" ]
1250
+ ;;
1251
+ R2_KEY_SECRET)
1252
+ [ "$value" = "your_custom_r2_secret_here" ]
1253
+ ;;
1254
+ KEYS_AUTH)
1255
+ [ "$value" = "your_custom_keys_auth_token_here" ]
1256
+ ;;
1257
+ PDF_WORKER_AUTH)
1258
+ [ "$value" = "your_custom_pdf_worker_auth_token_here" ]
1259
+ ;;
1260
+ IMAGES_API_TOKEN)
1261
+ [ "$value" = "your_cloudflare_images_api_token_here" ]
1262
+ ;;
1263
+ IMAGE_SIGNED_URL_SECRET)
1264
+ [ "$value" = "your_image_signed_url_secret_here" ]
1265
+ ;;
1266
+ *)
1267
+ return 1
1268
+ ;;
1269
+ esac
1270
+ }
1271
+
1272
+ generate_secret_value() {
1273
+ local var_name=$1
1274
+ case "$var_name" in
1275
+ IMAGE_SIGNED_URL_SECRET)
1276
+ openssl rand -base64 48 2>/dev/null | tr '+/' '-_' | tr -d '='
1277
+ ;;
1278
+ *)
1279
+ openssl rand -hex 32 2>/dev/null
1280
+ ;;
1281
+ esac
1282
+ }
1283
+
1253
1284
  prompt_for_var() {
1254
1285
  local var_name=$1
1255
1286
  local description=$2
@@ -1263,19 +1294,19 @@ prompt_for_secrets() {
1263
1294
  current_value=$(resolve_existing_domain_value "$var_name" "$current_value")
1264
1295
  fi
1265
1296
 
1266
- # Auto-generate specific authentication secrets - but allow keeping current
1267
- if [ "$var_name" = "USER_DB_AUTH" ] || [ "$var_name" = "R2_KEY_SECRET" ] || [ "$var_name" = "KEYS_AUTH" ] || [ "$var_name" = "PDF_WORKER_AUTH" ]; then
1297
+ # Auto-generate selected secrets - but allow keeping current.
1298
+ if is_auto_generated_secret_var "$var_name"; then
1268
1299
  echo -e "${BLUE}$var_name${NC}"
1269
1300
  echo -e "${YELLOW}$description${NC}"
1270
1301
 
1271
- 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
1302
+ if [ "$update_env" != "true" ] && [ -n "$current_value" ] && ! is_placeholder "$current_value" && ! is_secret_placeholder_value "$var_name" "$current_value"; then
1272
1303
  # Current value exists and is not a placeholder
1273
1304
  echo -e "${GREEN}Current value: [HIDDEN]${NC}"
1274
1305
  read -p "Generate new secret? (press Enter to keep current, or type 'y' to generate): " gen_choice
1275
1306
  gen_choice=$(strip_carriage_returns "$gen_choice")
1276
1307
 
1277
1308
  if [ "$gen_choice" = "y" ] || [ "$gen_choice" = "Y" ]; then
1278
- new_value=$(openssl rand -hex 32 2>/dev/null || echo "")
1309
+ new_value=$(generate_secret_value "$var_name" || echo "")
1279
1310
  if [ -n "$new_value" ]; then
1280
1311
  echo -e "${GREEN}✅ $var_name auto-generated${NC}"
1281
1312
  else
@@ -1302,7 +1333,7 @@ prompt_for_secrets() {
1302
1333
  else
1303
1334
  # No current value or placeholder value - auto-generate
1304
1335
  echo -e "${YELLOW}Auto-generating secret...${NC}"
1305
- new_value=$(openssl rand -hex 32 2>/dev/null || echo "")
1336
+ new_value=$(generate_secret_value "$var_name" || echo "")
1306
1337
  if [ -n "$new_value" ]; then
1307
1338
  echo -e "${GREEN}✅ $var_name auto-generated${NC}"
1308
1339
  else
@@ -1428,7 +1459,7 @@ prompt_for_secrets() {
1428
1459
  echo "==================================="
1429
1460
  prompt_for_var "USER_DB_AUTH" "Custom user database authentication token (generate with: openssl rand -hex 16)"
1430
1461
  prompt_for_var "R2_KEY_SECRET" "Custom R2 storage authentication token (generate with: openssl rand -hex 16)"
1431
- prompt_for_var "IMAGES_API_TOKEN" "Cloudflare Images API token (shared between workers)"
1462
+ prompt_for_var "IMAGES_API_TOKEN" "Image worker API token (shared between workers)"
1432
1463
 
1433
1464
  echo -e "${BLUE}🔥 FIREBASE AUTH CONFIGURATION${NC}"
1434
1465
  echo "==============================="
@@ -1534,7 +1565,7 @@ prompt_for_secrets() {
1534
1565
  echo "============================"
1535
1566
  prompt_for_var "KEYS_AUTH" "Keys worker authentication token (generate with: openssl rand -hex 16)"
1536
1567
  prompt_for_var "PDF_WORKER_AUTH" "PDF worker authentication token (generate with: openssl rand -hex 16)"
1537
- prompt_for_var "ACCOUNT_HASH" "Cloudflare Images Account Hash"
1568
+ prompt_for_var "IMAGE_SIGNED_URL_SECRET" "Image signed URL secret (generate with: openssl rand -base64 48 | tr '+/' '-_' | tr -d '=')"
1538
1569
  prompt_for_var "BROWSER_API_TOKEN" "Cloudflare Browser Rendering API token (for PDF Worker)"
1539
1570
 
1540
1571
  configure_manifest_signing_credentials
@@ -1680,15 +1711,12 @@ update_wrangler_configs() {
1680
1711
  local escaped_manifest_signing_public_key
1681
1712
  local escaped_export_encryption_key_id
1682
1713
  local escaped_export_encryption_public_key
1683
- local escaped_account_hash
1684
1714
  escaped_manifest_signing_key_id=$(escape_for_sed_replacement "$MANIFEST_SIGNING_KEY_ID")
1685
1715
  escaped_manifest_signing_public_key=$(escape_for_sed_replacement "$MANIFEST_SIGNING_PUBLIC_KEY")
1686
1716
  escaped_export_encryption_key_id=$(escape_for_sed_replacement "$EXPORT_ENCRYPTION_KEY_ID")
1687
1717
  escaped_export_encryption_public_key=$(escape_for_sed_replacement "$EXPORT_ENCRYPTION_PUBLIC_KEY")
1688
- escaped_account_hash=$(escape_for_sed_replacement "$ACCOUNT_HASH")
1689
1718
 
1690
1719
  sed -i "s|\"url\": \"[^\"]*\"|\"url\": \"https://$escaped_pages_custom_domain\"|g" app/config/config.json
1691
- sed -i "s|\"account_hash\": \"[^\"]*\"|\"account_hash\": \"$escaped_account_hash\"|g" app/config/config.json
1692
1720
  sed -i "s|\"MANIFEST_SIGNING_KEY_ID\"|\"$escaped_manifest_signing_key_id\"|g" app/config/config.json
1693
1721
  sed -i "s|\"MANIFEST_SIGNING_PUBLIC_KEY\"|\"$escaped_manifest_signing_public_key\"|g" app/config/config.json
1694
1722
  sed -i "s|\"EXPORT_ENCRYPTION_KEY_ID\"|\"$escaped_export_encryption_key_id\"|g" app/config/config.json
@@ -231,7 +231,7 @@ fi
231
231
 
232
232
  # Keys Worker
233
233
  if ! set_worker_secrets "Keys Worker" "workers/keys-worker" \
234
- "KEYS_AUTH" "USER_DB_AUTH" "R2_KEY_SECRET" "ACCOUNT_HASH" "IMAGES_API_TOKEN" "PDF_WORKER_AUTH"; then
234
+ "KEYS_AUTH" "USER_DB_AUTH" "R2_KEY_SECRET" "IMAGES_API_TOKEN" "PDF_WORKER_AUTH"; then
235
235
  echo -e "${YELLOW}⚠️ Skipping Keys Worker (not configured)${NC}"
236
236
  fi
237
237
 
@@ -253,7 +253,7 @@ fi
253
253
 
254
254
  # Images Worker
255
255
  if ! set_worker_secrets "Images Worker" "workers/image-worker" \
256
- "IMAGES_API_TOKEN" "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" "DATA_AT_REST_ENCRYPTION_KEY_ID"; then
256
+ "IMAGES_API_TOKEN" "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" "DATA_AT_REST_ENCRYPTION_KEY_ID" "IMAGE_SIGNED_URL_SECRET"; then
257
257
  echo -e "${YELLOW}⚠️ Skipping Images Worker (not configured)${NC}"
258
258
  fi
259
259
 
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable */
2
- // Generated by Wrangler by running `wrangler types` (hash: f4bdacbf5374924fcd3b9a294158d34f)
2
+ // Generated by Wrangler by running `wrangler types` (hash: abf7efe3b0d9f2d66e012084f057d73b)
3
3
  // Runtime types generated with workerd@1.20250823.0 2026-03-24 nodejs_compat
4
4
  declare namespace Cloudflare {
5
5
  interface Env {
@@ -35,7 +35,6 @@ declare namespace Cloudflare {
35
35
  EXPORT_ENCRYPTION_PRIVATE_KEY: string;
36
36
  EXPORT_ENCRYPTION_KEY_ID: string;
37
37
  EXPORT_ENCRYPTION_PUBLIC_KEY: string;
38
- DATA_AT_REST_ENCRYPTION_ENABLED: string;
39
38
  DATA_AT_REST_ENCRYPTION_PRIVATE_KEY: string;
40
39
  DATA_AT_REST_ENCRYPTION_KEY_ID: string;
41
40
  DATA_AT_REST_ENCRYPTION_PUBLIC_KEY: string;
@@ -44,11 +43,14 @@ declare namespace Cloudflare {
44
43
  AUDIT_WORKER_DOMAIN: string;
45
44
  IMAGES_WORKER_NAME: string;
46
45
  IMAGES_WORKER_DOMAIN: string;
46
+ IMAGE_SIGNED_URL_SECRET: string;
47
+ IMAGE_SIGNED_URL_TTL_SECONDS: string;
47
48
  PDF_WORKER_NAME: string;
48
49
  PDF_WORKER_DOMAIN: string;
49
50
  PDF_WORKER_AUTH: string;
50
51
  BROWSER_API_TOKEN: string;
51
52
  PRIMERSHEAR_EMAILS: string;
53
+ DATA_AT_REST_ENCRYPTION_ENABLED: string;
52
54
  }
53
55
  }
54
56
  interface Env extends Cloudflare.Env {}
@@ -56,7 +58,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
56
58
  [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
57
59
  };
58
60
  declare namespace NodeJS {
59
- interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ACCOUNT_ID" | "USER_DB_AUTH" | "R2_KEY_SECRET" | "IMAGES_API_TOKEN" | "API_KEY" | "AUTH_DOMAIN" | "PROJECT_ID" | "STORAGE_BUCKET" | "MESSAGING_SENDER_ID" | "APP_ID" | "MEASUREMENT_ID" | "FIREBASE_SERVICE_ACCOUNT_EMAIL" | "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "KEYS_WORKER_NAME" | "KEYS_WORKER_DOMAIN" | "KEYS_AUTH" | "ACCOUNT_HASH" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "DATA_AT_REST_ENCRYPTION_ENABLED" | "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" | "DATA_AT_REST_ENCRYPTION_KEY_ID" | "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" | "AUDIT_WORKER_NAME" | "AUDIT_BUCKET_NAME" | "AUDIT_WORKER_DOMAIN" | "IMAGES_WORKER_NAME" | "IMAGES_WORKER_DOMAIN" | "PDF_WORKER_NAME" | "PDF_WORKER_DOMAIN" | "PDF_WORKER_AUTH" | "BROWSER_API_TOKEN" | "PRIMERSHEAR_EMAILS">> {}
61
+ interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ACCOUNT_ID" | "USER_DB_AUTH" | "R2_KEY_SECRET" | "IMAGES_API_TOKEN" | "API_KEY" | "AUTH_DOMAIN" | "PROJECT_ID" | "STORAGE_BUCKET" | "MESSAGING_SENDER_ID" | "APP_ID" | "MEASUREMENT_ID" | "FIREBASE_SERVICE_ACCOUNT_EMAIL" | "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "KEYS_WORKER_NAME" | "KEYS_WORKER_DOMAIN" | "KEYS_AUTH" | "ACCOUNT_HASH" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" | "DATA_AT_REST_ENCRYPTION_KEY_ID" | "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" | "AUDIT_WORKER_NAME" | "AUDIT_BUCKET_NAME" | "AUDIT_WORKER_DOMAIN" | "IMAGES_WORKER_NAME" | "IMAGES_WORKER_DOMAIN" | "IMAGE_SIGNED_URL_SECRET" | "IMAGE_SIGNED_URL_TTL_SECONDS" | "PDF_WORKER_NAME" | "PDF_WORKER_DOMAIN" | "PDF_WORKER_AUTH" | "BROWSER_API_TOKEN" | "PRIMERSHEAR_EMAILS" | "DATA_AT_REST_ENCRYPTION_ENABLED">> {}
60
62
  }
61
63
 
62
64
  // Begin runtime types
@@ -1,7 +1,5 @@
1
1
  {
2
- // Required secrets: R2_KEY_SECRET, MANIFEST_SIGNING_PRIVATE_KEY, MANIFEST_SIGNING_KEY_ID, EXPORT_ENCRYPTION_PRIVATE_KEY, EXPORT_ENCRYPTION_KEY_ID
3
- // Optional data-at-rest secrets/vars:
4
- // - DATA_AT_REST_ENCRYPTION_ENABLED=true
2
+ // Required secrets: R2_KEY_SECRET, MANIFEST_SIGNING_PRIVATE_KEY, MANIFEST_SIGNING_KEY_ID, EXPORT_ENCRYPTION_PRIVATE_KEY, EXPORT_ENCRYPTION_KEY_ID
5
3
  // - DATA_AT_REST_ENCRYPTION_PRIVATE_KEY (required for decrypting encrypted records)
6
4
  // - DATA_AT_REST_ENCRYPTION_PUBLIC_KEY and DATA_AT_REST_ENCRYPTION_KEY_ID (required when encrypt-on-write is enabled)
7
5
  "name": "DATA_WORKER_NAME",