@striae-org/striae 5.3.1 → 5.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -111,6 +111,9 @@ IMAGES_WORKER_DOMAIN=your_images_worker_domain_here
111
111
  IMAGE_SIGNED_URL_SECRET=your_image_signed_url_secret_here
112
112
  # Optional: defaults to 3600 and max is 86400.
113
113
  IMAGE_SIGNED_URL_TTL_SECONDS=3600
114
+ # Optional: override the base URL used in signed URL responses to route delivery through the Pages proxy.
115
+ # Defaults to the image worker's own origin if unset (exposes worker domain to clients).
116
+ IMAGE_SIGNED_URL_BASE_URL=https://${PAGES_CUSTOM_DOMAIN}/api/image
114
117
 
115
118
  # ================================
116
119
  # PDF WORKER ENVIRONMENT VARIABLES
@@ -64,6 +64,28 @@ const resolvePdfImageUrl = async (selectedImage: string | undefined): Promise<st
64
64
  return await blobToDataUrl(imageBlob);
65
65
  }
66
66
 
67
+ // Signed image URLs routed through the Pages proxy contain a ?st= token.
68
+ // Pre-fetch the image client-side and embed as a data URL so the PDF worker's
69
+ // Puppeteer context doesn't need to make outbound requests for the image.
70
+ if (selectedImage.startsWith('http://') || selectedImage.startsWith('https://')) {
71
+ let parsedUrl: URL;
72
+ try {
73
+ parsedUrl = new URL(selectedImage);
74
+ } catch {
75
+ return selectedImage;
76
+ }
77
+
78
+ if (parsedUrl.searchParams.has('st')) {
79
+ const imageResponse = await fetch(selectedImage);
80
+ if (!imageResponse.ok) {
81
+ throw new Error('Failed to load selected image for PDF generation');
82
+ }
83
+
84
+ const imageBlob = await imageResponse.blob();
85
+ return await blobToDataUrl(imageBlob);
86
+ }
87
+ }
88
+
67
89
  return selectedImage;
68
90
  };
69
91
 
@@ -67,6 +67,14 @@ function resolveImageWorkerToken(env: Env): string {
67
67
  return typeof env.IMAGES_API_TOKEN === 'string' ? env.IMAGES_API_TOKEN.trim() : '';
68
68
  }
69
69
 
70
+ const BASE64URL_SEGMENT = /^[A-Za-z0-9_-]+$/;
71
+
72
+ function looksLikeSignedToken(value: string): boolean {
73
+ const parts = value.split('.');
74
+ if (parts.length !== 2) return false;
75
+ return parts.every(part => part.length > 0 && BASE64URL_SEGMENT.test(part));
76
+ }
77
+
70
78
  export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Response> => {
71
79
  if (!SUPPORTED_METHODS.has(request.method)) {
72
80
  return textResponse('Method not allowed', 405);
@@ -84,9 +92,17 @@ export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Re
84
92
 
85
93
  const requestUrl = new URL(request.url);
86
94
 
87
- const identity = await verifyFirebaseIdentityFromRequest(request, env);
88
- if (!identity) {
89
- return textResponse('Unauthorized', 401);
95
+ const signedToken = requestUrl.searchParams.get('st');
96
+ const isSignedTokenRequest =
97
+ request.method === 'GET' &&
98
+ signedToken !== null &&
99
+ looksLikeSignedToken(signedToken);
100
+
101
+ if (!isSignedTokenRequest) {
102
+ const identity = await verifyFirebaseIdentityFromRequest(request, env);
103
+ if (!identity) {
104
+ return textResponse('Unauthorized', 401);
105
+ }
90
106
  }
91
107
 
92
108
  const proxyPathResult = extractProxyPath(requestUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "5.3.1",
3
+ "version": "5.3.2",
4
4
  "private": false,
5
5
  "description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
6
6
  "license": "Apache-2.0",
@@ -14,6 +14,7 @@ interface Env {
14
14
  DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID?: string;
15
15
  IMAGE_SIGNED_URL_SECRET?: string;
16
16
  IMAGE_SIGNED_URL_TTL_SECONDS?: string;
17
+ IMAGE_SIGNED_URL_BASE_URL?: string;
17
18
  }
18
19
 
19
20
  interface KeyRegistryPayload {
@@ -383,6 +384,25 @@ function requireSignedUrlConfig(env: Env): void {
383
384
  }
384
385
  }
385
386
 
387
+ function parseSignedUrlBaseUrl(raw: string): string {
388
+ let parsed: URL;
389
+ try {
390
+ parsed = new URL(raw.trim());
391
+ } catch {
392
+ throw new Error(`IMAGE_SIGNED_URL_BASE_URL is not a valid absolute URL: "${raw}"`);
393
+ }
394
+
395
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
396
+ throw new Error(`IMAGE_SIGNED_URL_BASE_URL must use http or https, got: "${parsed.protocol}"`);
397
+ }
398
+
399
+ if (parsed.search || parsed.hash) {
400
+ throw new Error(`IMAGE_SIGNED_URL_BASE_URL must not include a query string or fragment: "${raw}"`);
401
+ }
402
+
403
+ return `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, '');
404
+ }
405
+
386
406
  async function getSignedUrlHmacKey(env: Env): Promise<CryptoKey> {
387
407
  const resolvedSecret = (env.IMAGE_SIGNED_URL_SECRET || env.IMAGES_API_TOKEN || '').trim();
388
408
  const keyBytes = new TextEncoder().encode(resolvedSecret);
@@ -592,8 +612,22 @@ async function handleSignedUrlMinting(request: Request, env: Env, fileId: string
592
612
  };
593
613
 
594
614
  const signedToken = await signSignedAccessPayload(payload, env);
595
- const signedPath = `/${encodeURIComponent(fileId)}?st=${encodeURIComponent(signedToken)}`;
596
- const signedUrl = new URL(signedPath, request.url).toString();
615
+
616
+ let baseUrl: string;
617
+ if (env.IMAGE_SIGNED_URL_BASE_URL) {
618
+ try {
619
+ baseUrl = parseSignedUrlBaseUrl(env.IMAGE_SIGNED_URL_BASE_URL);
620
+ } catch (error) {
621
+ console.error('Invalid IMAGE_SIGNED_URL_BASE_URL configuration', {
622
+ reason: error instanceof Error ? error.message : String(error)
623
+ });
624
+ return createJsonResponse({ error: 'Signed URL base URL is misconfigured' }, 500);
625
+ }
626
+ } else {
627
+ baseUrl = new URL(request.url).origin;
628
+ }
629
+
630
+ const signedUrl = `${baseUrl}/${encodeURIComponent(fileId)}?st=${encodeURIComponent(signedToken)}`;
597
631
 
598
632
  return createJsonResponse({
599
633
  success: true,