@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.
Files changed (49) hide show
  1. package/.env.example +0 -26
  2. package/README.md +1 -2
  3. package/app/components/actions/image-manage.ts +17 -67
  4. package/functions/api/audit/[[path]].ts +9 -24
  5. package/functions/api/data/[[path]].ts +9 -24
  6. package/functions/api/image/[[path]].ts +14 -30
  7. package/functions/api/pdf/[[path]].ts +9 -24
  8. package/functions/api/user/[[path]].ts +20 -36
  9. package/package.json +143 -137
  10. package/scripts/deploy-all.sh +29 -10
  11. package/scripts/deploy-config/modules/env-utils.sh +0 -68
  12. package/scripts/deploy-config/modules/prompt.sh +4 -110
  13. package/scripts/deploy-config/modules/scaffolding.sh +5 -68
  14. package/scripts/deploy-config/modules/validation.sh +1 -30
  15. package/scripts/deploy-pages-secrets.sh +0 -9
  16. package/scripts/deploy-worker-secrets.sh +2 -8
  17. package/tsconfig.json +1 -1
  18. package/workers/audit-worker/package.json +2 -2
  19. package/workers/audit-worker/src/{audit-worker.example.ts → audit-worker.ts} +1 -17
  20. package/workers/audit-worker/src/config.ts +1 -6
  21. package/workers/audit-worker/src/types.ts +0 -1
  22. package/workers/audit-worker/wrangler.jsonc.example +2 -6
  23. package/workers/data-worker/package.json +3 -2
  24. package/workers/data-worker/src/config.ts +1 -6
  25. package/workers/data-worker/src/{data-worker.example.ts → data-worker.ts} +2 -18
  26. package/workers/data-worker/src/types.ts +0 -1
  27. package/workers/data-worker/wrangler.jsonc.example +2 -4
  28. package/workers/image-worker/package.json +2 -2
  29. package/workers/image-worker/src/handlers/delete-image.ts +0 -5
  30. package/workers/image-worker/src/handlers/mint-signed-url.ts +0 -5
  31. package/workers/image-worker/src/handlers/serve-image.ts +2 -5
  32. package/workers/image-worker/src/handlers/upload-image.ts +0 -5
  33. package/workers/image-worker/src/{image-worker.example.ts → image-worker.ts} +2 -15
  34. package/workers/image-worker/src/router.ts +2 -3
  35. package/workers/image-worker/src/security/signed-url.ts +2 -2
  36. package/workers/image-worker/src/types.ts +0 -1
  37. package/workers/image-worker/wrangler.jsonc.example +2 -1
  38. package/workers/pdf-worker/package.json +2 -2
  39. package/workers/pdf-worker/src/{pdf-worker.example.ts → pdf-worker.ts} +1 -23
  40. package/workers/pdf-worker/wrangler.jsonc.example +2 -1
  41. package/workers/user-worker/package.json +2 -2
  42. package/workers/user-worker/src/auth.ts +0 -7
  43. package/workers/user-worker/src/handlers/user-routes.ts +25 -39
  44. package/workers/user-worker/src/types.ts +0 -2
  45. package/workers/user-worker/src/{user-worker.example.ts → user-worker.ts} +15 -30
  46. package/workers/user-worker/wrangler.jsonc.example +2 -1
  47. package/wrangler.toml.example +22 -2
  48. package/worker-configuration.d.ts +0 -7509
  49. 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 example workers, including nested helper modules, while excluding production worker entry files (`workers/*/src/**/*.ts` excluding `workers/*/src/**/*worker.ts`)
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
- 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
-
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
- 'direct-url',
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
- blob,
332
- url: objectUrl,
333
- revoke: () => URL.revokeObjectURL(objectUrl),
334
- urlType: 'blob'
283
+ url: signedUrlResponse.result.url,
284
+ revoke: () => {},
285
+ urlType: 'signed',
286
+ expiresAt: signedUrlResponse.result.expiresAt
335
287
  };
336
288
  } catch (error) {
337
- if (!(error instanceof Error && error.message.includes('Failed to retrieve image'))) {
338
- await auditService.logFileAccess(
339
- user,
340
- fileData.originalFilename || fileData.id,
341
- fileData.id,
342
- 'direct-url',
343
- caseNumber,
344
- 'failure',
345
- Date.now() - startTime,
346
- `Unexpected error during ${accessReason || 'image access'}`,
347
- fileData.originalFilename
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.AUDIT_WORKER_DOMAIN || !env.R2_KEY_SECRET) {
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(upstreamUrl, {
132
- method: request.method,
133
- headers: upstreamHeaders,
134
- body: bodyToForward
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.DATA_WORKER_DOMAIN || !env.R2_KEY_SECRET) {
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(upstreamUrl, {
123
- method: request.method,
124
- headers: upstreamHeaders,
125
- body: shouldForwardBody ? request.body : undefined
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 (!isSignedTokenRequest) {
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
- const imageWorkerToken = resolveImageWorkerToken(env);
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(upstreamUrl, {
143
- method: request.method,
144
- headers: upstreamHeaders,
145
- body: shouldForwardBody ? request.body : undefined
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.PDF_WORKER_DOMAIN || !env.PDF_WORKER_AUTH) {
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(upstreamUrl, {
142
- method: request.method,
143
- headers: upstreamHeaders,
144
- body: upstreamBody
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.USER_WORKER_DOMAIN || !env.USER_DB_AUTH) {
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
- `${userWorkerBaseUrl}/${encodeURIComponent(existenceCheckUserId)}`,
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 existenceCheckUrl = `${userWorkerBaseUrl}/${encodeURIComponent(requestedUserId)}`;
166
- const existenceResponse = await fetch(existenceCheckUrl, {
167
- method: 'GET',
168
- headers: {
169
- 'Accept': 'application/json',
170
- 'X-Custom-Auth-Key': env.USER_DB_AUTH
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(upstreamUrl, {
212
- method: request.method,
213
- headers: upstreamHeaders,
214
- body: shouldForwardBody ? request.body : undefined
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
  }