@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
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
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
|
-
|
|
596
|
-
|
|
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,
|