@striae-org/striae 6.1.7 → 7.0.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 +0 -26
- package/README.md +1 -2
- package/app/components/actions/image-manage.ts +17 -67
- package/functions/api/audit/[[path]].ts +9 -24
- package/functions/api/data/[[path]].ts +9 -24
- package/functions/api/image/[[path]].ts +14 -30
- package/functions/api/pdf/[[path]].ts +9 -24
- package/functions/api/user/[[path]].ts +20 -36
- package/package.json +143 -137
- package/scripts/deploy-all.sh +29 -10
- package/scripts/deploy-config/modules/env-utils.sh +0 -68
- package/scripts/deploy-config/modules/prompt.sh +4 -110
- package/scripts/deploy-config/modules/scaffolding.sh +5 -68
- package/scripts/deploy-config/modules/validation.sh +1 -30
- package/scripts/deploy-pages-secrets.sh +0 -9
- package/scripts/deploy-worker-secrets.sh +2 -8
- package/tsconfig.json +1 -1
- package/workers/audit-worker/package.json +2 -2
- package/workers/audit-worker/src/{audit-worker.example.ts → audit-worker.ts} +1 -17
- package/workers/audit-worker/src/config.ts +1 -6
- package/workers/audit-worker/src/types.ts +0 -1
- package/workers/audit-worker/wrangler.jsonc.example +2 -6
- package/workers/data-worker/package.json +3 -2
- package/workers/data-worker/src/config.ts +1 -6
- package/workers/data-worker/src/{data-worker.example.ts → data-worker.ts} +2 -18
- package/workers/data-worker/src/types.ts +0 -1
- package/workers/data-worker/wrangler.jsonc.example +2 -4
- package/workers/image-worker/package.json +2 -2
- package/workers/image-worker/src/handlers/delete-image.ts +0 -5
- package/workers/image-worker/src/handlers/mint-signed-url.ts +0 -5
- package/workers/image-worker/src/handlers/serve-image.ts +2 -5
- package/workers/image-worker/src/handlers/upload-image.ts +0 -5
- package/workers/image-worker/src/{image-worker.example.ts → image-worker.ts} +2 -15
- package/workers/image-worker/src/router.ts +2 -3
- package/workers/image-worker/src/security/signed-url.ts +2 -2
- package/workers/image-worker/src/types.ts +0 -1
- package/workers/image-worker/wrangler.jsonc.example +2 -1
- package/workers/pdf-worker/package.json +2 -2
- package/workers/pdf-worker/src/{pdf-worker.example.ts → pdf-worker.ts} +1 -23
- package/workers/pdf-worker/wrangler.jsonc.example +2 -1
- package/workers/user-worker/package.json +2 -2
- package/workers/user-worker/src/auth.ts +0 -7
- package/workers/user-worker/src/handlers/user-routes.ts +25 -39
- package/workers/user-worker/src/types.ts +0 -2
- package/workers/user-worker/src/{user-worker.example.ts → user-worker.ts} +15 -30
- package/workers/user-worker/wrangler.jsonc.example +2 -1
- package/wrangler.toml.example +22 -2
- package/worker-configuration.d.ts +0 -7509
- package/workers/image-worker/src/auth.ts +0 -7
package/.env.example
CHANGED
|
@@ -8,13 +8,6 @@
|
|
|
8
8
|
# ================================
|
|
9
9
|
ACCOUNT_ID=your_cloudflare_account_id_here
|
|
10
10
|
|
|
11
|
-
# ================================
|
|
12
|
-
# SHARED AUTHENTICATION & STORAGE
|
|
13
|
-
# ================================
|
|
14
|
-
USER_DB_AUTH=your_custom_user_db_auth_token_here
|
|
15
|
-
R2_KEY_SECRET=your_custom_r2_secret_here
|
|
16
|
-
IMAGES_API_TOKEN=your_cloudflare_images_api_token_here
|
|
17
|
-
|
|
18
11
|
# ================================
|
|
19
12
|
# FIREBASE AUTH CONFIGURATION
|
|
20
13
|
# ================================
|
|
@@ -83,9 +76,7 @@ PAGES_CUSTOM_DOMAIN=your_custom_domain_here
|
|
|
83
76
|
# ================================
|
|
84
77
|
# USER WORKER ENVIRONMENT VARIABLES
|
|
85
78
|
# ================================
|
|
86
|
-
# Worker domains can be entered manually or auto-generated as a shared subdomain during scripts/deploy-config.sh.
|
|
87
79
|
USER_WORKER_NAME=your_user_worker_name_here
|
|
88
|
-
USER_WORKER_DOMAIN=your_user_worker_domain_here
|
|
89
80
|
KV_STORE_ID=your_kv_store_id_here
|
|
90
81
|
|
|
91
82
|
# ================================
|
|
@@ -94,20 +85,17 @@ KV_STORE_ID=your_kv_store_id_here
|
|
|
94
85
|
DATA_WORKER_NAME=your_data_worker_name_here
|
|
95
86
|
DATA_BUCKET_NAME=your_data_bucket_name_here
|
|
96
87
|
FILES_BUCKET_NAME=your_files_bucket_name_here
|
|
97
|
-
DATA_WORKER_DOMAIN=your_data_worker_domain_here
|
|
98
88
|
|
|
99
89
|
# ================================
|
|
100
90
|
# AUDIT WORKER ENVIRONMENT VARIABLES
|
|
101
91
|
# ================================
|
|
102
92
|
AUDIT_WORKER_NAME=your_audit_worker_name_here
|
|
103
93
|
AUDIT_BUCKET_NAME=your_audit_bucket_name_here
|
|
104
|
-
AUDIT_WORKER_DOMAIN=your_audit_worker_domain_here
|
|
105
94
|
|
|
106
95
|
# ================================
|
|
107
96
|
# IMAGES WORKER ENVIRONMENT VARIABLES
|
|
108
97
|
# ================================
|
|
109
98
|
IMAGES_WORKER_NAME=your_images_worker_name_here
|
|
110
|
-
IMAGES_WORKER_DOMAIN=your_images_worker_domain_here
|
|
111
99
|
IMAGE_SIGNED_URL_SECRET=your_image_signed_url_secret_here
|
|
112
100
|
# Optional: defaults to 3600 and max is 86400.
|
|
113
101
|
IMAGE_SIGNED_URL_TTL_SECONDS=3600
|
|
@@ -119,22 +107,8 @@ IMAGE_SIGNED_URL_BASE_URL=https://${PAGES_CUSTOM_DOMAIN}/api/image
|
|
|
119
107
|
# PDF WORKER ENVIRONMENT VARIABLES
|
|
120
108
|
# ================================
|
|
121
109
|
PDF_WORKER_NAME=your_pdf_worker_name_here
|
|
122
|
-
PDF_WORKER_DOMAIN=your_pdf_worker_domain_here
|
|
123
|
-
PDF_WORKER_AUTH=your_custom_pdf_worker_auth_token_here
|
|
124
110
|
BROWSER_API_TOKEN=your_cloudflare_browser_rendering_api_token_here
|
|
125
111
|
|
|
126
|
-
# ================================
|
|
127
|
-
# QUICK MANUAL SETUP CHECKLIST
|
|
128
|
-
# ================================
|
|
129
|
-
# □ Copy this file to .env
|
|
130
|
-
# □ Fill in all Cloudflare credentials
|
|
131
|
-
# □ Generate secure random tokens for custom auth
|
|
132
|
-
# □ Set environment variables in each worker's Cloudflare Dashboard
|
|
133
|
-
# □ Set environment variables in Pages Dashboard
|
|
134
|
-
# □ Configure KV binding in user-worker/wrangler.jsonc
|
|
135
|
-
# □ Configure R2 binding in data-worker/wrangler.jsonc
|
|
136
|
-
# □ Configure R2 binding in audit-worker/wrangler.jsonc
|
|
137
|
-
|
|
138
112
|
# ================================
|
|
139
113
|
# PRIMERSHEAR PDF FORMAT
|
|
140
114
|
# ================================
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Included:
|
|
|
36
36
|
- `app/` source (with `app/config-example/`)
|
|
37
37
|
- `functions/`, `public/`
|
|
38
38
|
- Worker package manifests
|
|
39
|
-
- Worker source files needed by the
|
|
39
|
+
- Worker source files needed by the workers, including nested helper modules
|
|
40
40
|
- PDF worker example support files limited to `workers/pdf-worker/src/assets/generated-assets.example.ts` and `workers/pdf-worker/src/formats/format-striae.ts` (no extra PDF image assets or custom formats)
|
|
41
41
|
- Worker example Wrangler configs (`workers/*/wrangler.jsonc.example`)
|
|
42
42
|
- Project-level example and build config (`.env.example`, `wrangler.toml.example`, `tsconfig.json`, etc.)
|
|
@@ -60,6 +60,5 @@ See `LICENSE`.
|
|
|
60
60
|
|
|
61
61
|
## Support
|
|
62
62
|
|
|
63
|
-
- Striae Community: [https://community.striae.org](https://community.striae.org)
|
|
64
63
|
- Support page: [https://www.striae.org/support](https://www.striae.org/support)
|
|
65
64
|
- Contact: [info@striae.org](mailto:info@striae.org)
|
|
@@ -265,61 +265,13 @@ export const getImageUrl = async (
|
|
|
265
265
|
const defaultAccessReason = accessReason || 'Image viewer access';
|
|
266
266
|
|
|
267
267
|
try {
|
|
268
|
-
|
|
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
|
-
|
|
293
|
-
const workerResponse = await fetchImageApi(user, `/${encodeURIComponent(fileData.id)}`, {
|
|
294
|
-
method: 'GET',
|
|
295
|
-
headers: {
|
|
296
|
-
'Accept': 'application/octet-stream,image/*'
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
if (!workerResponse.ok) {
|
|
301
|
-
await auditService.logFileAccess(
|
|
302
|
-
user,
|
|
303
|
-
fileData.originalFilename || fileData.id,
|
|
304
|
-
fileData.id,
|
|
305
|
-
'direct-url',
|
|
306
|
-
caseNumber,
|
|
307
|
-
'failure',
|
|
308
|
-
Date.now() - startTime,
|
|
309
|
-
'Image retrieval failed',
|
|
310
|
-
fileData.originalFilename
|
|
311
|
-
);
|
|
312
|
-
throw new Error('Failed to retrieve image');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const blob = await workerResponse.blob();
|
|
316
|
-
const objectUrl = URL.createObjectURL(blob);
|
|
268
|
+
const signedUrlResponse = await createSignedImageUrlApi(user, fileData.id);
|
|
317
269
|
|
|
318
270
|
await auditService.logFileAccess(
|
|
319
271
|
user,
|
|
320
272
|
fileData.originalFilename || fileData.id,
|
|
321
273
|
fileData.id,
|
|
322
|
-
'
|
|
274
|
+
'signed-url',
|
|
323
275
|
caseNumber,
|
|
324
276
|
'success',
|
|
325
277
|
Date.now() - startTime,
|
|
@@ -328,25 +280,23 @@ export const getImageUrl = async (
|
|
|
328
280
|
);
|
|
329
281
|
|
|
330
282
|
return {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
283
|
+
url: signedUrlResponse.result.url,
|
|
284
|
+
revoke: () => {},
|
|
285
|
+
urlType: 'signed',
|
|
286
|
+
expiresAt: signedUrlResponse.result.expiresAt
|
|
335
287
|
};
|
|
336
288
|
} catch (error) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
);
|
|
349
|
-
}
|
|
289
|
+
await auditService.logFileAccess(
|
|
290
|
+
user,
|
|
291
|
+
fileData.originalFilename || fileData.id,
|
|
292
|
+
fileData.id,
|
|
293
|
+
'signed-url',
|
|
294
|
+
caseNumber,
|
|
295
|
+
'failure',
|
|
296
|
+
Date.now() - startTime,
|
|
297
|
+
`Unexpected error during ${accessReason || 'image access'}`,
|
|
298
|
+
fileData.originalFilename
|
|
299
|
+
);
|
|
350
300
|
throw error;
|
|
351
301
|
}
|
|
352
302
|
};
|
|
@@ -18,19 +18,6 @@ function textResponse(message: string, status: number): Response {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function normalizeWorkerBaseUrl(workerDomain: string): string {
|
|
22
|
-
if (typeof workerDomain !== 'string' || workerDomain.trim().length === 0) {
|
|
23
|
-
throw new Error('Invalid worker domain');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const trimmedDomain = workerDomain.trim().replace(/\/+$/, '');
|
|
27
|
-
if (trimmedDomain.startsWith('http://') || trimmedDomain.startsWith('https://')) {
|
|
28
|
-
return trimmedDomain;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return `https://${trimmedDomain}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
21
|
function extractProxyPath(url: URL): string | null {
|
|
35
22
|
const routePrefix = '/api/audit';
|
|
36
23
|
if (!url.pathname.startsWith(routePrefix)) {
|
|
@@ -86,13 +73,10 @@ export const onRequest = async ({ request, env }: AuditProxyContext): Promise<Re
|
|
|
86
73
|
return textResponse('Forbidden', 403);
|
|
87
74
|
}
|
|
88
75
|
|
|
89
|
-
if (!env.
|
|
76
|
+
if (!env.AUDIT_WORKER) {
|
|
90
77
|
return textResponse('Audit service not configured', 502);
|
|
91
78
|
}
|
|
92
79
|
|
|
93
|
-
const auditWorkerBaseUrl = normalizeWorkerBaseUrl(env.AUDIT_WORKER_DOMAIN);
|
|
94
|
-
const upstreamUrl = `${auditWorkerBaseUrl}${proxyPath}${requestUrl.search}`;
|
|
95
|
-
|
|
96
80
|
let bodyToForward: BodyInit | undefined;
|
|
97
81
|
if (request.method === 'POST') {
|
|
98
82
|
const payload = await request.json().catch(() => null) as Record<string, unknown> | null;
|
|
@@ -124,15 +108,16 @@ export const onRequest = async ({ request, env }: AuditProxyContext): Promise<Re
|
|
|
124
108
|
upstreamHeaders.set('Accept', acceptHeader);
|
|
125
109
|
}
|
|
126
110
|
|
|
127
|
-
upstreamHeaders.set('X-Custom-Auth-Key', env.R2_KEY_SECRET);
|
|
128
|
-
|
|
129
111
|
let upstreamResponse: Response;
|
|
130
112
|
try {
|
|
131
|
-
upstreamResponse = await fetch(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
113
|
+
upstreamResponse = await env.AUDIT_WORKER.fetch(
|
|
114
|
+
`https://worker${proxyPath}${requestUrl.search}`,
|
|
115
|
+
{
|
|
116
|
+
method: request.method,
|
|
117
|
+
headers: upstreamHeaders,
|
|
118
|
+
body: bodyToForward
|
|
119
|
+
}
|
|
120
|
+
);
|
|
136
121
|
} catch {
|
|
137
122
|
return textResponse('Upstream audit service unavailable', 502);
|
|
138
123
|
}
|
|
@@ -18,19 +18,6 @@ function textResponse(message: string, status: number): Response {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function normalizeWorkerBaseUrl(workerDomain: string): string {
|
|
22
|
-
if (typeof workerDomain !== 'string' || workerDomain.trim().length === 0) {
|
|
23
|
-
throw new Error('Invalid worker domain');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const trimmedDomain = workerDomain.trim().replace(/\/+$/, '');
|
|
27
|
-
if (trimmedDomain.startsWith('http://') || trimmedDomain.startsWith('https://')) {
|
|
28
|
-
return trimmedDomain;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return `https://${trimmedDomain}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
21
|
function extractProxyPath(url: URL): string | null {
|
|
35
22
|
const routePrefix = '/api/data';
|
|
36
23
|
if (!url.pathname.startsWith(routePrefix)) {
|
|
@@ -95,13 +82,10 @@ export const onRequest = async ({ request, env }: DataProxyContext): Promise<Res
|
|
|
95
82
|
}
|
|
96
83
|
}
|
|
97
84
|
|
|
98
|
-
if (!env.
|
|
85
|
+
if (!env.DATA_WORKER) {
|
|
99
86
|
return textResponse('Data service not configured', 502);
|
|
100
87
|
}
|
|
101
88
|
|
|
102
|
-
const dataWorkerBaseUrl = normalizeWorkerBaseUrl(env.DATA_WORKER_DOMAIN);
|
|
103
|
-
const upstreamUrl = `${dataWorkerBaseUrl}${proxyPath}${requestUrl.search}`;
|
|
104
|
-
|
|
105
89
|
const upstreamHeaders = new Headers();
|
|
106
90
|
const contentTypeHeader = request.headers.get('Content-Type');
|
|
107
91
|
if (contentTypeHeader) {
|
|
@@ -113,17 +97,18 @@ export const onRequest = async ({ request, env }: DataProxyContext): Promise<Res
|
|
|
113
97
|
upstreamHeaders.set('Accept', acceptHeader);
|
|
114
98
|
}
|
|
115
99
|
|
|
116
|
-
upstreamHeaders.set('X-Custom-Auth-Key', env.R2_KEY_SECRET);
|
|
117
|
-
|
|
118
100
|
const shouldForwardBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
119
101
|
|
|
120
102
|
let upstreamResponse: Response;
|
|
121
103
|
try {
|
|
122
|
-
upstreamResponse = await fetch(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
104
|
+
upstreamResponse = await env.DATA_WORKER.fetch(
|
|
105
|
+
`https://worker${proxyPath}${requestUrl.search}`,
|
|
106
|
+
{
|
|
107
|
+
method: request.method,
|
|
108
|
+
headers: upstreamHeaders,
|
|
109
|
+
body: shouldForwardBody ? request.body : undefined
|
|
110
|
+
}
|
|
111
|
+
);
|
|
127
112
|
} catch {
|
|
128
113
|
return textResponse('Upstream data service unavailable', 502);
|
|
129
114
|
}
|
|
@@ -17,19 +17,6 @@ function textResponse(message: string, status: number): Response {
|
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function normalizeWorkerBaseUrl(workerDomain: string): string {
|
|
21
|
-
if (typeof workerDomain !== 'string' || workerDomain.trim().length === 0) {
|
|
22
|
-
throw new Error('Invalid worker domain');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const trimmedDomain = workerDomain.trim().replace(/\/+$/, '');
|
|
26
|
-
if (trimmedDomain.startsWith('http://') || trimmedDomain.startsWith('https://')) {
|
|
27
|
-
return trimmedDomain;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return `https://${trimmedDomain}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
20
|
type ProxyPathResult =
|
|
34
21
|
| { ok: true; path: string }
|
|
35
22
|
| { ok: false; reason: 'not-found' | 'bad-encoding' };
|
|
@@ -63,10 +50,6 @@ function extractProxyPath(url: URL): ProxyPathResult {
|
|
|
63
50
|
}
|
|
64
51
|
}
|
|
65
52
|
|
|
66
|
-
function resolveImageWorkerToken(env: Env): string {
|
|
67
|
-
return typeof env.IMAGES_API_TOKEN === 'string' ? env.IMAGES_API_TOKEN.trim() : '';
|
|
68
|
-
}
|
|
69
|
-
|
|
70
53
|
const BASE64URL_SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
71
54
|
|
|
72
55
|
function looksLikeSignedToken(value: string): boolean {
|
|
@@ -98,7 +81,11 @@ export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Re
|
|
|
98
81
|
signedToken !== null &&
|
|
99
82
|
looksLikeSignedToken(signedToken);
|
|
100
83
|
|
|
101
|
-
if (
|
|
84
|
+
if (request.method === 'GET') {
|
|
85
|
+
if (!isSignedTokenRequest) {
|
|
86
|
+
return textResponse('Unauthorized', 403);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
102
89
|
const identity = await verifyFirebaseIdentityFromRequest(request, env);
|
|
103
90
|
if (!identity) {
|
|
104
91
|
return textResponse('Unauthorized', 401);
|
|
@@ -114,14 +101,10 @@ export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Re
|
|
|
114
101
|
|
|
115
102
|
const proxyPath = proxyPathResult.path;
|
|
116
103
|
|
|
117
|
-
|
|
118
|
-
if (!env.IMAGES_WORKER_DOMAIN || !imageWorkerToken) {
|
|
104
|
+
if (!env.IMAGE_WORKER) {
|
|
119
105
|
return textResponse('Image service not configured', 502);
|
|
120
106
|
}
|
|
121
107
|
|
|
122
|
-
const imageWorkerBaseUrl = normalizeWorkerBaseUrl(env.IMAGES_WORKER_DOMAIN);
|
|
123
|
-
const upstreamUrl = `${imageWorkerBaseUrl}${proxyPath}${requestUrl.search}`;
|
|
124
|
-
|
|
125
108
|
const upstreamHeaders = new Headers();
|
|
126
109
|
const contentTypeHeader = request.headers.get('Content-Type');
|
|
127
110
|
if (contentTypeHeader) {
|
|
@@ -133,17 +116,18 @@ export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Re
|
|
|
133
116
|
upstreamHeaders.set('Accept', acceptHeader);
|
|
134
117
|
}
|
|
135
118
|
|
|
136
|
-
upstreamHeaders.set('Authorization', `Bearer ${imageWorkerToken}`);
|
|
137
|
-
|
|
138
119
|
const shouldForwardBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
139
120
|
|
|
140
121
|
let upstreamResponse: Response;
|
|
141
122
|
try {
|
|
142
|
-
upstreamResponse = await fetch(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
123
|
+
upstreamResponse = await env.IMAGE_WORKER.fetch(
|
|
124
|
+
`https://worker${proxyPath}${requestUrl.search}`,
|
|
125
|
+
{
|
|
126
|
+
method: request.method,
|
|
127
|
+
headers: upstreamHeaders,
|
|
128
|
+
body: shouldForwardBody ? request.body : undefined
|
|
129
|
+
}
|
|
130
|
+
);
|
|
147
131
|
} catch {
|
|
148
132
|
return textResponse('Upstream image service unavailable', 502);
|
|
149
133
|
}
|
|
@@ -23,19 +23,6 @@ function textResponse(message: string, status: number): Response {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function normalizeWorkerBaseUrl(workerDomain: string): string {
|
|
27
|
-
if (typeof workerDomain !== 'string' || workerDomain.trim().length === 0) {
|
|
28
|
-
throw new Error('Invalid worker domain');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const trimmedDomain = workerDomain.trim().replace(/\/+$/, '');
|
|
32
|
-
if (trimmedDomain.startsWith('http://') || trimmedDomain.startsWith('https://')) {
|
|
33
|
-
return trimmedDomain;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return `https://${trimmedDomain}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
26
|
function extractProxyPath(url: URL): string | null {
|
|
40
27
|
const routePrefix = '/api/pdf';
|
|
41
28
|
if (!url.pathname.startsWith(routePrefix)) {
|
|
@@ -93,13 +80,10 @@ export const onRequest = async ({ request, env }: PdfProxyContext): Promise<Resp
|
|
|
93
80
|
return textResponse('Not Found', 404);
|
|
94
81
|
}
|
|
95
82
|
|
|
96
|
-
if (!env.
|
|
83
|
+
if (!env.PDF_WORKER) {
|
|
97
84
|
return textResponse('PDF service not configured', 502);
|
|
98
85
|
}
|
|
99
86
|
|
|
100
|
-
const pdfWorkerBaseUrl = normalizeWorkerBaseUrl(env.PDF_WORKER_DOMAIN);
|
|
101
|
-
const upstreamUrl = `${pdfWorkerBaseUrl}${proxyPath}${requestUrl.search}`;
|
|
102
|
-
|
|
103
87
|
const upstreamHeaders = new Headers();
|
|
104
88
|
const contentTypeHeader = request.headers.get('Content-Type');
|
|
105
89
|
if (contentTypeHeader) {
|
|
@@ -111,8 +95,6 @@ export const onRequest = async ({ request, env }: PdfProxyContext): Promise<Resp
|
|
|
111
95
|
upstreamHeaders.set('Accept', acceptHeader);
|
|
112
96
|
}
|
|
113
97
|
|
|
114
|
-
upstreamHeaders.set('X-Custom-Auth-Key', env.PDF_WORKER_AUTH);
|
|
115
|
-
|
|
116
98
|
// Resolve the report format server-side based on the verified user email.
|
|
117
99
|
// This prevents email lists from ever being exposed in the client bundle.
|
|
118
100
|
const reportFormat = resolveReportFormat(
|
|
@@ -138,11 +120,14 @@ export const onRequest = async ({ request, env }: PdfProxyContext): Promise<Resp
|
|
|
138
120
|
|
|
139
121
|
let upstreamResponse: Response;
|
|
140
122
|
try {
|
|
141
|
-
upstreamResponse = await fetch(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
123
|
+
upstreamResponse = await env.PDF_WORKER.fetch(
|
|
124
|
+
`https://worker${proxyPath}${requestUrl.search}`,
|
|
125
|
+
{
|
|
126
|
+
method: request.method,
|
|
127
|
+
headers: upstreamHeaders,
|
|
128
|
+
body: upstreamBody
|
|
129
|
+
}
|
|
130
|
+
);
|
|
146
131
|
} catch {
|
|
147
132
|
return textResponse('Upstream PDF service unavailable', 502);
|
|
148
133
|
}
|
|
@@ -29,19 +29,6 @@ function jsonResponse(payload: Record<string, unknown>, status: number = 200): R
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function normalizeWorkerBaseUrl(workerDomain: string): string {
|
|
33
|
-
if (typeof workerDomain !== 'string' || workerDomain.trim().length === 0) {
|
|
34
|
-
throw new Error('Invalid worker domain');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const trimmedDomain = workerDomain.trim().replace(/\/+$/, '');
|
|
38
|
-
if (trimmedDomain.startsWith('http://') || trimmedDomain.startsWith('https://')) {
|
|
39
|
-
return trimmedDomain;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return `https://${trimmedDomain}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
32
|
function extractProxyPath(url: URL): string | null {
|
|
46
33
|
const routePrefix = '/api/user';
|
|
47
34
|
if (!url.pathname.startsWith(routePrefix)) {
|
|
@@ -109,12 +96,10 @@ export const onRequest = async ({ request, env }: UserProxyContext): Promise<Res
|
|
|
109
96
|
return textResponse('Not Found', 404);
|
|
110
97
|
}
|
|
111
98
|
|
|
112
|
-
if (!env.
|
|
99
|
+
if (!env.USER_WORKER) {
|
|
113
100
|
return textResponse('User service not configured', 502);
|
|
114
101
|
}
|
|
115
102
|
|
|
116
|
-
const userWorkerBaseUrl = normalizeWorkerBaseUrl(env.USER_WORKER_DOMAIN);
|
|
117
|
-
|
|
118
103
|
const existenceCheckUserId = extractExistenceCheckUserId(proxyPath);
|
|
119
104
|
if (existenceCheckUserId !== null) {
|
|
120
105
|
if (request.method !== 'GET') {
|
|
@@ -123,13 +108,12 @@ export const onRequest = async ({ request, env }: UserProxyContext): Promise<Res
|
|
|
123
108
|
|
|
124
109
|
let existenceResponse: Response;
|
|
125
110
|
try {
|
|
126
|
-
existenceResponse = await fetch(
|
|
127
|
-
|
|
111
|
+
existenceResponse = await env.USER_WORKER.fetch(
|
|
112
|
+
`https://worker/${encodeURIComponent(existenceCheckUserId)}`,
|
|
128
113
|
{
|
|
129
114
|
method: 'GET',
|
|
130
115
|
headers: {
|
|
131
|
-
'Accept': 'application/json'
|
|
132
|
-
'X-Custom-Auth-Key': env.USER_DB_AUTH
|
|
116
|
+
'Accept': 'application/json'
|
|
133
117
|
}
|
|
134
118
|
}
|
|
135
119
|
);
|
|
@@ -162,14 +146,15 @@ export const onRequest = async ({ request, env }: UserProxyContext): Promise<Res
|
|
|
162
146
|
// This is defense-in-depth — the primary check runs client-side in the login flow.
|
|
163
147
|
if (request.method === 'PUT' && env.REGISTRATION_EMAILS && env.REGISTRATION_EMAILS.trim().length > 0) {
|
|
164
148
|
try {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
149
|
+
const existenceResponse = await env.USER_WORKER.fetch(
|
|
150
|
+
`https://worker/${encodeURIComponent(requestedUserId)}`,
|
|
151
|
+
{
|
|
152
|
+
method: 'GET',
|
|
153
|
+
headers: {
|
|
154
|
+
'Accept': 'application/json'
|
|
155
|
+
}
|
|
171
156
|
}
|
|
172
|
-
|
|
157
|
+
);
|
|
173
158
|
|
|
174
159
|
if (existenceResponse.status === 404) {
|
|
175
160
|
// User does not exist yet — this is a registration PUT.
|
|
@@ -189,8 +174,6 @@ export const onRequest = async ({ request, env }: UserProxyContext): Promise<Res
|
|
|
189
174
|
}
|
|
190
175
|
}
|
|
191
176
|
|
|
192
|
-
const upstreamUrl = `${userWorkerBaseUrl}${proxyPath}${requestUrl.search}`;
|
|
193
|
-
|
|
194
177
|
const upstreamHeaders = new Headers();
|
|
195
178
|
const contentTypeHeader = request.headers.get('Content-Type');
|
|
196
179
|
if (contentTypeHeader) {
|
|
@@ -202,17 +185,18 @@ export const onRequest = async ({ request, env }: UserProxyContext): Promise<Res
|
|
|
202
185
|
upstreamHeaders.set('Accept', acceptHeader);
|
|
203
186
|
}
|
|
204
187
|
|
|
205
|
-
upstreamHeaders.set('X-Custom-Auth-Key', env.USER_DB_AUTH);
|
|
206
|
-
|
|
207
188
|
const shouldForwardBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
208
189
|
|
|
209
190
|
let upstreamResponse: Response;
|
|
210
191
|
try {
|
|
211
|
-
upstreamResponse = await fetch(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
192
|
+
upstreamResponse = await env.USER_WORKER.fetch(
|
|
193
|
+
`https://worker${proxyPath}${requestUrl.search}`,
|
|
194
|
+
{
|
|
195
|
+
method: request.method,
|
|
196
|
+
headers: upstreamHeaders,
|
|
197
|
+
body: shouldForwardBody ? request.body : undefined
|
|
198
|
+
}
|
|
199
|
+
);
|
|
216
200
|
} catch {
|
|
217
201
|
return textResponse('Upstream user service unavailable', 502);
|
|
218
202
|
}
|