@antelopejs/dms-frontend 0.10.0 → 0.10.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.
Files changed (75) hide show
  1. package/README.md +34 -11
  2. package/dist/commands/build-action.d.ts +3 -0
  3. package/dist/commands/build-action.js +74 -6
  4. package/dist/commands/build.js +7 -0
  5. package/dist/commands/clean-action.js +1 -1
  6. package/dist/commands/clean.js +1 -1
  7. package/dist/commands/dev-action.js +2 -2
  8. package/dist/commands/dev.js +1 -1
  9. package/dist/config.js +1 -0
  10. package/dist/derived-outputs.d.ts +1 -0
  11. package/dist/derived-outputs.js +3 -1
  12. package/dist/help.js +1 -1
  13. package/dist/internal/prepared-workspace.d.ts +12 -0
  14. package/dist/internal/prepared-workspace.js +74 -0
  15. package/dist/layer-watch.js +1 -1
  16. package/dist/locale-files.d.ts +4 -2
  17. package/dist/locale-files.js +11 -9
  18. package/dist/manifest.js +2 -1
  19. package/dist/ports.d.ts +3 -1
  20. package/dist/ports.js +1 -1
  21. package/dist/project-modules.d.ts +6 -1
  22. package/dist/project-modules.js +26 -16
  23. package/dist/verify-source-runner.js +2 -44
  24. package/dist/verify-source-workspace.d.ts +2 -0
  25. package/dist/verify-source-workspace.js +111 -0
  26. package/dist/workspace-setup.d.ts +1 -0
  27. package/dist/workspace-setup.js +3 -2
  28. package/dist/workspace.js +1 -1
  29. package/package.json +20 -7
  30. package/templates/vue/backend-boundary.mjs +132 -0
  31. package/templates/vue/client-chunking.mjs +2 -0
  32. package/templates/vue/entry-weight.mjs +9 -5
  33. package/templates/vue/frontend-build.ts +4 -3
  34. package/templates/vue/head-order.mjs +3 -2
  35. package/templates/vue/icon-api.mjs +2 -0
  36. package/templates/vue/server/auth/backend.mjs +4 -4
  37. package/templates/vue/server/auth/establish-endpoints.mjs +2 -2
  38. package/templates/vue/server/auth/session.mjs +5 -2
  39. package/templates/vue/server/auth-declarations.mjs +46 -0
  40. package/templates/vue/server/auth-hooks.mjs +41 -3
  41. package/templates/vue/server/backend-proxies.mjs +16 -12
  42. package/templates/vue/server/backend-response.mjs +3 -3
  43. package/templates/vue/server/cli-report.mjs +22 -1
  44. package/templates/vue/server/client-error.mjs +5 -0
  45. package/templates/vue/server/content.mjs +24 -11
  46. package/templates/vue/server/email-bundle-watch.mjs +9 -3
  47. package/templates/vue/server/email.mjs +5 -3
  48. package/templates/vue/server/generated-files.mjs +16 -6
  49. package/templates/vue/server/icons.mjs +15 -11
  50. package/templates/vue/server/in-place-render.mjs +3 -3
  51. package/templates/vue/server/inertia.mjs +17 -3
  52. package/templates/vue/server/module-event.mjs +28 -16
  53. package/templates/vue/server/module-route-types.mjs +2 -2
  54. package/templates/vue/server/module-routes.mjs +24 -76
  55. package/templates/vue/server/module-server.mjs +27 -17
  56. package/templates/vue/server/module-testing.mjs +23 -8
  57. package/templates/vue/server/page-props.mjs +6 -5
  58. package/templates/vue/server/page-redirects.mjs +1 -1
  59. package/templates/vue/server/path-patterns.mjs +22 -11
  60. package/templates/vue/server/proxy-headers.mjs +1 -1
  61. package/templates/vue/server/proxy-redirects.mjs +1 -0
  62. package/templates/vue/server/proxy-request.mjs +2 -1
  63. package/templates/vue/server/proxy-transfers.mjs +17 -11
  64. package/templates/vue/server/public-endpoints.mjs +12 -16
  65. package/templates/vue/server/request-failure.mjs +1 -1
  66. package/templates/vue/server/request-routing.mjs +6 -1
  67. package/templates/vue/server/request-target.mjs +22 -9
  68. package/templates/vue/server/route-rules.mjs +12 -6
  69. package/templates/vue/server/static-files.mjs +12 -7
  70. package/templates/vue/server/vite-errors.mjs +13 -2
  71. package/templates/vue/server/web-body.mjs +2 -1
  72. package/templates/vue/server-module.ts +2 -1
  73. package/templates/vue/server.mjs +41 -20
  74. package/templates/vue/ui-inertia-page.mjs +1 -1
  75. package/templates/vue/vite.config.ts +9 -0
@@ -1,3 +1,4 @@
1
+ import { resolveAuth } from "./auth-declarations.mjs";
1
2
  import { resolveBackendProxies } from "./backend-proxies.mjs";
2
3
  import { compilePathPattern } from "./path-patterns.mjs";
3
4
  import { matchPublicEndpoint } from "./public-endpoints.mjs";
@@ -12,8 +13,8 @@ import {
12
13
  /** @typedef {(message: string) => void} Warn */
13
14
  /**
14
15
  * @import {
15
- * AuthProvider, DeclaredModule, ModuleDeclared, RouteRegistration,
16
- * ServerDeclarations, ServerModuleDeclaration, ServerRoute
16
+ * DeclaredModule, ModuleDeclared, RouteRegistration,
17
+ * ServerDeclarations, ServerMethod, ServerModuleDeclaration, ServerRoute
17
18
  * } from "./module-route-types.mjs"
18
19
  * @import { PublicEndpoint } from "./public-endpoints.mjs"
19
20
  */
@@ -156,13 +157,19 @@ function collectDeclarations(setup) {
156
157
  return declared;
157
158
  }
158
159
 
160
+ /**
161
+ * @param {unknown} name
162
+ * @returns {name is ServerMethod}
163
+ */
164
+ function isMethod(name) {
165
+ return isString(name) && METHODS.has(name);
166
+ }
167
+
159
168
  /** @param {unknown} method */
160
169
  function methodsOf(method) {
161
170
  /** @type {readonly unknown[]} */
162
171
  const methods = Array.isArray(method) ? method : [method];
163
- return methods.length && methods.every((name) => METHODS.has(name))
164
- ? methods
165
- : undefined;
172
+ return methods.length && methods.every(isMethod) ? methods : undefined;
166
173
  }
167
174
 
168
175
  /** @param {RouteRegistration} registration */
@@ -176,7 +183,7 @@ function registrationRejection(registration) {
176
183
  }
177
184
 
178
185
  /**
179
- * @param {RouteRegistration["method"]} method
186
+ * @param {string | readonly string[]} method
180
187
  * @param {string} path
181
188
  */
182
189
  function describeRoute(method, path) {
@@ -241,15 +248,16 @@ function declarationsOfModule(declaration, warn) {
241
248
  function routesOfModule(module, warn) {
242
249
  const { id, priority = 0 } = module;
243
250
  return module.declared.routes.flatMap((registration, order) => {
251
+ const methods = methodsOf(registration.method);
244
252
  const reason = registrationRejection(registration);
245
- if (reason) {
253
+ if (reason || !methods) {
246
254
  warn(
247
255
  `Frontend module ${id} declares route ${describeRoute(registration.method, registration.path)}, which is ignored: ${reason}.`,
248
256
  );
249
257
  return [];
250
258
  }
251
259
  const compiled = compilePath(registration.path);
252
- return methodsOf(registration.method).map((method) => ({
260
+ return methods.map((method) => ({
253
261
  ...compiled,
254
262
  id,
255
263
  priority,
@@ -399,70 +407,6 @@ function resolveRoutes(modules, warn, publicEndpoints) {
399
407
  return routes;
400
408
  }
401
409
 
402
- const AUTH_HOOKS = ["refreshSession", "publicSession"];
403
-
404
- /** @param {unknown} auth */
405
- function authRejection(auth) {
406
- if (!isNonNullObject(auth)) return "it is not an object";
407
- for (const hook of AUTH_HOOKS)
408
- if (auth[hook] !== undefined && !isFunction(auth[hook]))
409
- return `${hook} is not a function`;
410
- if (
411
- auth.signInPath !== undefined &&
412
- (!isString(auth.signInPath) ||
413
- !compilePathPattern(auth.signInPath) ||
414
- auth.signInPath.includes("*"))
415
- )
416
- return "signInPath is not a page path";
417
- if (
418
- auth.signInExempt !== undefined &&
419
- !(
420
- Array.isArray(auth.signInExempt) &&
421
- auth.signInExempt.every((pattern) => compilePathPattern(pattern))
422
- )
423
- )
424
- return "signInExempt is not a list of /x, /x/* or /x/** patterns";
425
- return undefined;
426
- }
427
-
428
- /**
429
- * The auth hooks: those of the highest-priority module that registers them.
430
- * A module registering them twice, or after another one, is reported.
431
- *
432
- * @param {DeclaredModule[]} modules
433
- * @param {Warn} warn
434
- */
435
- function resolveAuth(modules, warn) {
436
- /** @type {AuthProvider | undefined} */
437
- let provider;
438
- for (const module of modules) {
439
- for (const auth of module.declared.auth) {
440
- const reason = authRejection(auth);
441
- if (reason) {
442
- warn(
443
- `Frontend module ${module.id} registers auth hooks, which are ignored: ${reason}.`,
444
- );
445
- continue;
446
- }
447
- if (provider) {
448
- warn(
449
- `Frontend module ${module.id} registers auth hooks, which are ignored: ${provider.id} (priority ${provider.priority ?? 0}) provides them.`,
450
- );
451
- continue;
452
- }
453
- provider = {
454
- id: module.id,
455
- priority: module.priority ?? 0,
456
- signInPath: auth.signInPath,
457
- signInExempt: auth.signInExempt?.map(compilePathPattern),
458
- refreshSession: auth.refreshSession,
459
- publicSession: auth.publicSession,
460
- };
461
- }
462
- }
463
- return provider;
464
- }
465
-
466
410
  /**
467
411
  * The route rules: of two declarations of the same pattern, the module of
468
412
  * higher priority keeps it, with a warning.
@@ -482,18 +426,22 @@ function resolveRules(modules, warn) {
482
426
  continue;
483
427
  }
484
428
  for (const [pattern, rule] of Object.entries(rules)) {
485
- const reason = ruleRejection(pattern, rule);
429
+ const compiled = compilePathPattern(pattern);
486
430
  const current = kept.get(pattern);
487
- if (reason || current) {
431
+ const reason =
432
+ ruleRejection(pattern, rule) ??
433
+ (current &&
434
+ `${current.id} (priority ${current.priority}) declares it`);
435
+ if (!compiled || reason) {
488
436
  warn(
489
- `Frontend module ${module.id} declares the route rule "${pattern}", which is ignored: ${reason ?? `${current.id} (priority ${current.priority}) declares it`}.`,
437
+ `Frontend module ${module.id} declares the route rule "${pattern}", which is ignored: ${reason}.`,
490
438
  );
491
439
  continue;
492
440
  }
493
441
  kept.set(pattern, {
494
442
  id: module.id,
495
443
  priority: module.priority ?? 0,
496
- pattern: compilePathPattern(pattern),
444
+ pattern: compiled,
497
445
  rule,
498
446
  });
499
447
  }
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
3
3
  import { allowedEstablishEndpoints } from "./auth/establish-endpoints.mjs";
4
4
  import { serverAuth } from "./auth-hooks.mjs";
5
5
  import { answerModuleRoute } from "./module-event.mjs";
6
- import { isNonNullObject, isString } from "./type-guards.mjs";
6
+ import { isNonNullObject, isRecord, isString } from "./type-guards.mjs";
7
7
  import {
8
8
  matchModuleRoute,
9
9
  mayHaveModuleRoute,
@@ -81,19 +81,24 @@ function readJsonFile(url, fallback) {
81
81
  const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
82
82
 
83
83
  /**
84
- * The object holding `path`'s last key, when every key up to it is own.
84
+ * The object holding `path`'s last key, and that key, when every key up to it
85
+ * is own.
85
86
  *
86
87
  * @param {Record<string, unknown>} target
87
88
  * @param {string[]} path
89
+ * @returns {{ holder: Record<string, unknown>, key: string } | undefined}
88
90
  */
89
- function holderOf(target, path) {
91
+ function slotOf(target, path) {
92
+ const key = path.at(-1);
93
+ if (key === undefined || UNSAFE_KEYS.has(key)) return undefined;
90
94
  let node = target;
91
- for (const key of path.slice(0, -1)) {
92
- if (UNSAFE_KEYS.has(key) || !Object.hasOwn(node, key)) return undefined;
93
- node = node[key];
94
- if (!isNonNullObject(node)) return undefined;
95
+ for (const step of path.slice(0, -1)) {
96
+ if (UNSAFE_KEYS.has(step) || !Object.hasOwn(node, step)) return undefined;
97
+ const next = node[step];
98
+ if (!isRecord(next)) return undefined;
99
+ node = next;
95
100
  }
96
- return UNSAFE_KEYS.has(path.at(-1)) ? undefined : node;
101
+ return { holder: node, key };
97
102
  }
98
103
 
99
104
  /**
@@ -104,10 +109,12 @@ function holderOf(target, path) {
104
109
  * @param {string} value
105
110
  */
106
111
  function fillPath(target, path, value) {
107
- const holder = holderOf(target, path);
108
- const key = path.at(-1);
109
- if (holder && (holder[key] === undefined || holder[key] === ""))
110
- holder[key] = value;
112
+ const slot = slotOf(target, path);
113
+ if (
114
+ slot &&
115
+ (slot.holder[slot.key] === undefined || slot.holder[slot.key] === "")
116
+ )
117
+ slot.holder[slot.key] = value;
111
118
  }
112
119
 
113
120
  /**
@@ -118,10 +125,13 @@ function fillPath(target, path, value) {
118
125
  * @param {string} value
119
126
  */
120
127
  function fillEmptied(target, path, value) {
121
- const holder = holderOf(target, path);
122
- const key = path.at(-1);
123
- if (holder && Object.hasOwn(holder, key) && holder[key] === "")
124
- holder[key] = value;
128
+ const slot = slotOf(target, path);
129
+ if (
130
+ slot &&
131
+ Object.hasOwn(slot.holder, slot.key) &&
132
+ slot.holder[slot.key] === ""
133
+ )
134
+ slot.holder[slot.key] = value;
125
135
  }
126
136
 
127
137
  /**
@@ -327,7 +337,7 @@ export async function loadServerModules(devServer) {
327
337
  * module route serves is the module's for every method: another method is
328
338
  * answered `405`.
329
339
  *
330
- * @param {import("node:http").IncomingMessage} request The Node request
340
+ * @param {import("./request-target.mjs").ServerRequest} request The Node request
331
341
  * @param {import("node:http").ServerResponse} response The Node response
332
342
  * @param {string} pathname The request's normalized path
333
343
  * @param {ServerModules} modules What `loadServerModules` or `createServerModules` returned
@@ -2,7 +2,9 @@ import { createServer } from "node:http";
2
2
  import { RequestBodyError, UpstreamError } from "./auth/backend.mjs";
3
3
  import { UNEXPECTED_ERROR_BODY } from "./backend-response.mjs";
4
4
  import { createServerModules, handleModuleRoute } from "./module-server.mjs";
5
- import { failureStatus } from "./request-failure.mjs";
5
+ import { failureStatus, writeBadRequest } from "./request-failure.mjs";
6
+ import { isServerRequest } from "./request-target.mjs";
7
+ import { isString } from "./type-guards.mjs";
6
8
 
7
9
  /**
8
10
  * What the frontend server answers an API request whose handler failed.
@@ -52,6 +54,10 @@ export async function serveModuleRoutes(declarations, deployment = {}) {
52
54
  warn: (message) => warnings.push(message),
53
55
  });
54
56
  const server = createServer((request, response) => {
57
+ if (!isServerRequest(request)) {
58
+ writeBadRequest(response);
59
+ return;
60
+ }
55
61
  const { pathname } = new URL(request.url, "http://frontend.local");
56
62
  handleModuleRoute(request, response, pathname, modules)
57
63
  .then((answered) => {
@@ -61,16 +67,25 @@ export async function serveModuleRoutes(declarations, deployment = {}) {
61
67
  /** @param {unknown} error */ (error) => writeFailure(error, response),
62
68
  );
63
69
  });
64
- await new Promise((settle) => server.listen(0, "127.0.0.1", settle));
65
- const { port } = server.address();
70
+ /** @type {Promise<void>} */
71
+ const listening = new Promise((settle) =>
72
+ server.listen(0, "127.0.0.1", settle),
73
+ );
74
+ await listening;
75
+ const address = server.address();
76
+ if (address === null || isString(address))
77
+ throw new TypeError("The module test server is not listening on a port");
78
+ const { port } = address;
79
+ /** @returns {Promise<void>} */
80
+ const close = () =>
81
+ new Promise((settle) => {
82
+ server.closeAllConnections();
83
+ server.close(() => settle());
84
+ });
66
85
  return {
67
86
  url: `http://127.0.0.1:${port}`,
68
87
  modules,
69
88
  warnings,
70
- close: () =>
71
- new Promise((settle) => {
72
- server.closeAllConnections();
73
- server.close(() => settle());
74
- }),
89
+ close,
75
90
  };
76
91
  }
@@ -46,7 +46,7 @@ const NOT_FOUND = 404;
46
46
  *
47
47
  * @typedef {{
48
48
  * signInPath: string,
49
- * view: (session: unknown) => { user?: unknown, session?: unknown }
49
+ * view: (session: import("./auth/session.mjs").Session | undefined) => { user?: unknown, session?: unknown }
50
50
  * }} PageAuth
51
51
  */
52
52
 
@@ -66,8 +66,9 @@ export function pageRoute(props) {
66
66
  * browser holding the layout of another account or workspace, after a sign-in,
67
67
  * a sign-out or a switch, holds another key, so it is sent the right one.
68
68
  *
69
- * @param {unknown} session The sealed session: any JSON value, since only its
70
- * `accountId` and `activeTenantId` are read, as empty when it holds none
69
+ * @param {{ accountId?: string, activeTenantId?: string } | undefined} session
70
+ * The sealed session, of which only the `accountId` and `activeTenantId` are
71
+ * read, as empty when it holds none
71
72
  * @returns {string}
72
73
  */
73
74
  export function sharedOnceKey(session) {
@@ -118,7 +119,7 @@ function pagePath(url, withShared) {
118
119
  }
119
120
  for (const [name, value] of pageQuery) query.set(name, value);
120
121
  if (!withShared) query.set(SHARED_PROP, "false");
121
- return `/dms/page?${query}`;
122
+ return `/dms/page?${query.toString()}`;
122
123
  }
123
124
 
124
125
  /**
@@ -206,7 +207,7 @@ export async function propsForQuery(props, url, backend, auth) {
206
207
  * session's layout, sent now or already held: declaring it otherwise would let
207
208
  * a layout of another session pass for this one.
208
209
  *
209
- * @param {import("node:http").IncomingMessage} request
210
+ * @param {import("./request-target.mjs").ServerRequest} request
210
211
  * @param {PageBackend} backend
211
212
  * @param {PageAuth} auth
212
213
  * @returns {Promise<{
@@ -17,7 +17,7 @@ export function redirectToHomepage(request, response, pathname) {
17
17
  }
18
18
 
19
19
  /**
20
- * @param {import("node:http").IncomingMessage} request
20
+ * @param {import("./request-target.mjs").ServerRequest} request
21
21
  * @param {import("node:http").ServerResponse} response
22
22
  * @param {import("./page-props.mjs").PageProps} props
23
23
  * @param {import("./module-server.mjs").ServerModules["serverAuth"]} auth
@@ -2,6 +2,19 @@ import { isString } from "./type-guards.mjs";
2
2
 
3
3
  const SEGMENT = /^[A-Za-z0-9._~-]+$/;
4
4
 
5
+ /** The forms a path pattern takes, as the warnings name them. */
6
+ export const PATH_PATTERN_FORMS = "/x, /x/* or /x/**";
7
+
8
+ /**
9
+ * The wildcard a pattern may end with: what it matches below the literal
10
+ * segments, and how it ranks against a pattern with the same literals.
11
+ */
12
+ const TAILS = new Map([
13
+ ["*", { matcher: "/[^/]+", rank: 3 }],
14
+ ["**", { matcher: "(?:/.*)?", rank: 0 }],
15
+ ]);
16
+ const NO_TAIL = { matcher: "", rank: 1 };
17
+
5
18
  /**
6
19
  * @typedef {object} CompiledPathPattern
7
20
  * @property {string} pattern The pattern as declared
@@ -17,7 +30,8 @@ const SEGMENT = /^[A-Za-z0-9._~-]+$/;
17
30
  *
18
31
  * Page path patterns, as the modules' `dms.server.ts` name them: `/x` is the
19
32
  * page itself, `/x/*` one segment below it, `/x/**` the page and everything
20
- * below it. Nothing else: no prefix match inside a segment, no query string.
33
+ * below it. Nothing else: a wildcard only as the last segment, no prefix
34
+ * match inside a segment, no query string.
21
35
  *
22
36
  * @param {unknown} pattern
23
37
  * @returns {CompiledPathPattern | undefined}
@@ -25,26 +39,23 @@ const SEGMENT = /^[A-Za-z0-9._~-]+$/;
25
39
  export function compilePathPattern(pattern) {
26
40
  if (!isString(pattern) || !pattern.startsWith("/")) return undefined;
27
41
  const segments = pattern === "/" ? [] : pattern.slice(1).split("/");
28
- const deep = segments.at(-1) === "**";
29
- const literal = deep ? segments.slice(0, -1) : segments;
42
+ const last = TAILS.get(segments.at(-1) ?? "");
43
+ const literal = last ? segments.slice(0, -1) : segments;
30
44
  if (
31
45
  literal.some(
32
46
  (segment) =>
33
- segment !== "*" &&
34
- (!SEGMENT.test(segment) || segment === "." || segment === ".."),
47
+ !SEGMENT.test(segment) || segment === "." || segment === "..",
35
48
  )
36
49
  )
37
50
  return undefined;
51
+ const tail = last ?? NO_TAIL;
38
52
  const body = literal
39
- .map((segment) =>
40
- segment === "*" ? "/[^/]+" : `/${segment.replaceAll(".", "\\.")}`,
41
- )
53
+ .map((segment) => `/${segment.replaceAll(".", "\\.")}`)
42
54
  .join("");
43
- const matcher = new RegExp(`^${body}${deep ? "(?:/.*)?" : ""}/?$`);
44
- const literals = literal.filter((segment) => segment !== "*").length;
55
+ const matcher = new RegExp(`^${body}${tail.matcher}/?$`);
45
56
  return {
46
57
  pattern,
47
- specificity: literals * 4 + literal.length * 2 + (deep ? 0 : 1),
58
+ specificity: literal.length * 6 + tail.rank,
48
59
  test: (pathname) => matcher.test(pathname === "" ? "/" : pathname),
49
60
  };
50
61
  }
@@ -57,7 +57,7 @@ export function writeProxyHeaders(
57
57
  for (const name of [...HOP_BY_HOP_HEADERS, ...named]) delete headers[name];
58
58
  delete headers["set-cookie"];
59
59
  const cookies = upstream.headers.getSetCookie?.() ?? [];
60
- if (cookies.length) headers["set-cookie"] = cookies;
60
+ if (cookies.length) Object.assign(headers, { "set-cookie": cookies });
61
61
  if (headers.location && upstream.url)
62
62
  headers.location = relayedLocation(
63
63
  headers.location,
@@ -6,6 +6,7 @@ export const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
6
6
  * @returns {string | undefined}
7
7
  */
8
8
  function originOf(url) {
9
+ if (url === undefined) return undefined;
9
10
  try {
10
11
  return new URL(url).origin;
11
12
  } catch {
@@ -21,7 +21,7 @@ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
21
21
  * the storage's signed URL, on the public origin the server itself may not
22
22
  * reach, which turned every download into a 502.
23
23
  *
24
- * @param {import("node:http").IncomingMessage} request
24
+ * @param {import("./request-target.mjs").ServerRequest} request
25
25
  * @param {import("node:http").ServerResponse} response
26
26
  * @param {import("./backend-proxies.mjs").BackendProxy} backendProxy
27
27
  * @param {(path: string) => boolean} isProxiedPath
@@ -36,6 +36,7 @@ export async function proxy(request, response, backendProxy, isProxiedPath) {
36
36
  if (response.closed) return;
37
37
  const abandoned = new AbortController();
38
38
  response.once("close", () => abandoned.abort());
39
+ /** @type {RequestInit} */
39
40
  const init = {
40
41
  signal: abandoned.signal,
41
42
  method: request.method,
@@ -1,3 +1,5 @@
1
+ import { isObjectLike } from "./type-guards.mjs";
2
+
1
3
  const PAYLOAD_TOO_LARGE = 413;
2
4
  const TOO_LARGE_BODY = JSON.stringify({ message: "Payload Too Large" });
3
5
  /**
@@ -41,7 +43,7 @@ async function* limitedChunks(request, maxBodyBytes) {
41
43
  }
42
44
 
43
45
  /**
44
- * @param {import("node:http").IncomingMessage} request
46
+ * @param {import("./request-target.mjs").ServerRequest} request
45
47
  * @param {number | undefined} maxBodyBytes
46
48
  */
47
49
  function requestBody(request, maxBodyBytes) {
@@ -70,7 +72,7 @@ function declaresTooLargeBody(request, maxBodyBytes) {
70
72
  function isBodyTooLarge(error) {
71
73
  return (
72
74
  error instanceof BodyTooLargeError ||
73
- error?.cause instanceof BodyTooLargeError
75
+ (isObjectLike(error) && error.cause instanceof BodyTooLargeError)
74
76
  );
75
77
  }
76
78
 
@@ -93,7 +95,10 @@ function refuseTooLargeBody(request, response) {
93
95
  connection: "close",
94
96
  });
95
97
  response.write(TOO_LARGE_BODY);
96
- if (request.complete) return response.end();
98
+ if (request.complete) {
99
+ response.end();
100
+ return;
101
+ }
97
102
  const close = () => {
98
103
  clearTimeout(timer);
99
104
  response.end();
@@ -117,7 +122,7 @@ function refuseTooLargeBody(request, response) {
117
122
  * before a byte is passed on, a chunked body is cut off as soon as it goes
118
123
  * over.
119
124
  *
120
- * @param {import("node:http").IncomingMessage} request
125
+ * @param {import("./request-target.mjs").ServerRequest} request
121
126
  * @param {import("node:http").ServerResponse} response
122
127
  * @param {number | undefined} maxBodyBytes
123
128
  * @param {(
@@ -125,18 +130,19 @@ function refuseTooLargeBody(request, response) {
125
130
  * ) => Promise<Response>} send The backend request with the body to stream:
126
131
  * none for a GET or a HEAD, the request itself, or its bytes cut off past
127
132
  * the limit
128
- * @returns {Promise<Response | import("node:http").ServerResponse | undefined>}
129
- * The backend's answer, or once the body went over and `response` got the
130
- * 413: `undefined`, or `response` itself when the request had been read
131
- * already (`response.end()`'s own return)
133
+ * @returns {Promise<Response | undefined>} The backend's answer, or
134
+ * `undefined` once the body went over and `response` got the 413
132
135
  */
133
136
  export async function sendWithinLimit(request, response, maxBodyBytes, send) {
134
- if (declaresTooLargeBody(request, maxBodyBytes))
135
- return refuseTooLargeBody(request, response);
137
+ if (declaresTooLargeBody(request, maxBodyBytes)) {
138
+ refuseTooLargeBody(request, response);
139
+ return undefined;
140
+ }
136
141
  try {
137
142
  return await send(requestBody(request, maxBodyBytes));
138
143
  } catch (error) {
139
144
  if (!isBodyTooLarge(error)) throw error;
140
- return refuseTooLargeBody(request, response);
145
+ refuseTooLargeBody(request, response);
146
+ return undefined;
141
147
  }
142
148
  }
@@ -2,13 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { json } from "./auth/backend.mjs";
3
3
  import { CROSS_ORIGIN_ERROR, isSameOrigin } from "./auth/client-ip.mjs";
4
4
  import { PROXY_AUTH_MODES } from "./backend-proxies.mjs";
5
- import {
6
- isNonNullObject,
7
- isNumber,
8
- isObjectLike,
9
- isRecord,
10
- isString,
11
- } from "./type-guards.mjs";
5
+ import { isNumber, isObjectLike, isRecord, isString } from "./type-guards.mjs";
12
6
 
13
7
  /**
14
8
  * A public endpoint, compiled from a declaration that passed every check.
@@ -96,7 +90,7 @@ export function readDeclaredPublicEndpoints(file = DECLARED_ENDPOINTS_FILE) {
96
90
  * when the proxies are resolved.
97
91
  *
98
92
  * @param {unknown} entry
99
- * @returns {entry is { path: string, auth?: "session" | "none", maxBodyBytes?: number }}
93
+ * @returns {entry is { path: string, auth?: "session" | "none", maxBodyBytes?: number, module?: unknown }}
100
94
  */
101
95
  function isProxyDeclaration(entry) {
102
96
  return (
@@ -252,11 +246,11 @@ function pathMatcher(path) {
252
246
  * A declaration made usable, or `undefined` when any part of it is malformed:
253
247
  * a bad entry is dropped whole, never widened to what it might have meant.
254
248
  *
255
- * @param {PublicEndpointDeclaration | null | undefined} entry
249
+ * @param {unknown} entry
256
250
  * @returns {PublicEndpoint | undefined}
257
251
  */
258
252
  export function compilePublicEndpoint(entry) {
259
- if (!isNonNullObject(entry)) return undefined;
253
+ if (!isObjectLike(entry)) return undefined;
260
254
  if (!isString(entry.path) || !PUBLIC_PATH.test(entry.path)) return undefined;
261
255
  const methods = normalizedMethods(entry.methods);
262
256
  const headers = normalizedHeaders(entry.headers);
@@ -284,7 +278,9 @@ export function allowedPublicEndpoints(
284
278
  ) {
285
279
  return [...declared, ...environmentEndpoints(declaration)].flatMap(
286
280
  (entry) => {
287
- const endpoint = compilePublicEndpoint(entry);
281
+ const endpoint = isRecord(entry)
282
+ ? compilePublicEndpoint(entry)
283
+ : undefined;
288
284
  if (!endpoint)
289
285
  console.warn(
290
286
  `Ignoring malformed public endpoint ${JSON.stringify(entry)}: expected ` +
@@ -337,23 +333,23 @@ export function matchPublicEndpoint(
337
333
  * Origin. Under `auth: "session"`, a state-changing request needs one unless
338
334
  * it reaches a public endpoint, which is why the proxy never sends a public
339
335
  * endpoint the session: a cross-site form posting to it acts as nobody.
336
+ * Under either mode, a request that reaches a public endpoint carries the
337
+ * endpoint's declared headers.
340
338
  *
341
- * @param {import("node:http").IncomingMessage} request The incoming request
339
+ * @param {import("./request-target.mjs").ServerRequest} request The incoming request
342
340
  * @param {ReadonlySet<string>} safeMethods Methods that change nothing and need no Origin
343
341
  * @param {import("./backend-proxies.mjs").BackendProxy} proxy The backend proxy the request reached
344
342
  * @returns `{ anonymous, headers, isRefused }`
345
343
  */
346
344
  export function proxyAccess(request, safeMethods, proxy) {
347
- if (proxy.auth === "none")
348
- return { anonymous: true, headers: [], isRefused: false };
349
345
  const publicEndpoint = matchPublicEndpoint(
350
346
  request.method,
351
347
  new URL(request.url, "http://frontend.local").pathname,
352
348
  );
353
- if (publicEndpoint)
349
+ if (publicEndpoint || proxy.auth === "none")
354
350
  return {
355
351
  anonymous: true,
356
- headers: publicEndpoint.headers,
352
+ headers: publicEndpoint?.headers ?? [],
357
353
  isRefused: false,
358
354
  };
359
355
  return {
@@ -38,7 +38,7 @@ export function failureStatus(error) {
38
38
  */
39
39
  export function isResponseGone(error, response) {
40
40
  return (
41
- error?.code === "ERR_STREAM_PREMATURE_CLOSE" ||
41
+ (isObjectLike(error) && error.code === "ERR_STREAM_PREMATURE_CLOSE") ||
42
42
  response.destroyed ||
43
43
  response.writableEnded ||
44
44
  response.headersSent
@@ -1,4 +1,5 @@
1
1
  import { matchBackendProxy } from "./backend-proxies.mjs";
2
+ import { isIconRequest } from "./icons.mjs";
2
3
  import { isRendererFilePath, moduleRouteMethods } from "./module-routes.mjs";
3
4
 
4
5
  /**
@@ -37,7 +38,11 @@ const PAGE_METHODS = new Set(["GET", "HEAD"]);
37
38
  * `{ kind: "not-found" }`
38
39
  */
39
40
  export function requestOwner(method, pathname, modules, hasRendererFile) {
40
- if (isRendererFilePath(pathname) || hasRendererFile(pathname))
41
+ if (
42
+ isIconRequest(pathname) ||
43
+ isRendererFilePath(pathname) ||
44
+ hasRendererFile(pathname)
45
+ )
41
46
  return { kind: "renderer" };
42
47
  const route = modules.routes.find((candidate) =>
43
48
  candidate.matcher.test(pathname),