@antelopejs/dms-frontend 0.9.3 → 0.10.1

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 (140) hide show
  1. package/README.md +46 -29
  2. package/dist/commands/build-action.d.ts +3 -0
  3. package/dist/commands/build-action.js +77 -8
  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 +4 -3
  8. package/dist/commands/dev.js +1 -1
  9. package/dist/commands/prepare-action.js +1 -1
  10. package/dist/commands/start-action.js +3 -24
  11. package/dist/commands/workspace-task.js +2 -1
  12. package/dist/config.js +1 -0
  13. package/dist/derived-outputs.d.ts +1 -0
  14. package/dist/derived-outputs.js +6 -4
  15. package/dist/discovery.js +17 -6
  16. package/dist/entry-weight.js +15 -0
  17. package/dist/fs-sync.js +7 -4
  18. package/dist/help.js +1 -1
  19. package/dist/index.js +2 -1
  20. package/dist/internal/built-workspace.d.ts +4 -0
  21. package/dist/internal/built-workspace.js +43 -0
  22. package/dist/internal/ensure.d.ts +1 -0
  23. package/dist/internal/ensure.js +8 -0
  24. package/dist/internal/locale-catalogs.d.ts +8 -3
  25. package/dist/internal/locale-catalogs.js +16 -6
  26. package/dist/internal/manifest-decoder.d.ts +7 -0
  27. package/dist/internal/manifest-decoder.js +81 -0
  28. package/dist/internal/own-release.d.ts +5 -0
  29. package/dist/internal/own-release.js +20 -0
  30. package/dist/internal/plain-object.d.ts +1 -0
  31. package/dist/internal/plain-object.js +6 -0
  32. package/dist/internal/prepared-workspace.d.ts +12 -0
  33. package/dist/internal/prepared-workspace.js +74 -0
  34. package/dist/internal/text-list.d.ts +1 -0
  35. package/dist/internal/text-list.js +10 -0
  36. package/dist/internal/vite-manifest.d.ts +6 -0
  37. package/dist/internal/vite-manifest.js +24 -0
  38. package/dist/layer-watch.js +25 -24
  39. package/dist/layers.d.ts +1 -1
  40. package/dist/layers.js +3 -1
  41. package/dist/locale-files.d.ts +6 -3
  42. package/dist/locale-files.js +15 -9
  43. package/dist/manifest.d.ts +1 -1
  44. package/dist/manifest.js +46 -32
  45. package/dist/materialize.js +6 -2
  46. package/dist/module-set.js +2 -1
  47. package/dist/node-options.d.ts +1 -0
  48. package/dist/node-options.js +8 -0
  49. package/dist/output.d.ts +15 -9
  50. package/dist/output.js +30 -18
  51. package/dist/ports.d.ts +3 -1
  52. package/dist/ports.js +1 -1
  53. package/dist/private-values.d.ts +1 -1
  54. package/dist/private-values.js +5 -3
  55. package/dist/project-modules.d.ts +5 -1
  56. package/dist/project-modules.js +23 -15
  57. package/dist/renderer-range.js +5 -3
  58. package/dist/server-process.js +1 -1
  59. package/dist/verify-source-build.js +10 -2
  60. package/dist/verify-source-client-bundle.d.ts +1 -0
  61. package/dist/verify-source-client-bundle.js +30 -0
  62. package/dist/verify-source-runner.js +36 -94
  63. package/dist/verify-source-sources.js +49 -11
  64. package/dist/verify-source-workspace.d.ts +2 -0
  65. package/dist/verify-source-workspace.js +111 -0
  66. package/dist/workspace-setup.d.ts +1 -0
  67. package/dist/workspace-setup.js +4 -3
  68. package/package.json +11 -7
  69. package/skills/dms-frontend/SKILL.md +1 -1
  70. package/templates/vue/app-runtime.ts +2 -9
  71. package/templates/vue/backend-boundary.mjs +132 -0
  72. package/templates/vue/client-chunking.mjs +2 -0
  73. package/templates/vue/client-server-guard.mjs +16 -0
  74. package/templates/vue/compress-assets.mjs +9 -1
  75. package/templates/vue/entry-weight.mjs +29 -3
  76. package/templates/vue/frontend-build.ts +4 -3
  77. package/templates/vue/frontend-module.ts +1 -0
  78. package/templates/vue/frontend-runtime/module-registration.ts +2 -2
  79. package/templates/vue/frontend-runtime/state.ts +2 -2
  80. package/templates/vue/frontend-runtime/types.ts +8 -2
  81. package/templates/vue/head-order.mjs +10 -2
  82. package/templates/vue/icon-api.mjs +2 -0
  83. package/templates/vue/icon-scan.mjs +4 -0
  84. package/templates/vue/server/auth/backend.mjs +42 -5
  85. package/templates/vue/server/auth/client-ip.mjs +52 -4
  86. package/templates/vue/server/auth/establish-endpoints.mjs +11 -7
  87. package/templates/vue/server/auth/session.mjs +86 -4
  88. package/templates/vue/server/auth-declarations.mjs +46 -0
  89. package/templates/vue/server/auth-hooks.mjs +55 -7
  90. package/templates/vue/server/backend-proxies.mjs +78 -18
  91. package/templates/vue/server/backend-request.mjs +61 -0
  92. package/templates/vue/server/backend-response.mjs +23 -6
  93. package/templates/vue/server/built-files.mjs +7 -0
  94. package/templates/vue/server/cli-report.mjs +75 -12
  95. package/templates/vue/server/client-error.mjs +5 -0
  96. package/templates/vue/server/client-manifest.mjs +23 -1
  97. package/templates/vue/server/content.mjs +40 -10
  98. package/templates/vue/server/dev-private-files.mjs +17 -1
  99. package/templates/vue/server/dev-styles.mjs +38 -0
  100. package/templates/vue/server/email-bundle-watch.mjs +9 -3
  101. package/templates/vue/server/email.mjs +14 -6
  102. package/templates/vue/server/favicon.mjs +3 -0
  103. package/templates/vue/server/generated-files.mjs +221 -0
  104. package/templates/vue/server/homepage.mjs +21 -9
  105. package/templates/vue/server/html-tag.mjs +4 -0
  106. package/templates/vue/server/icons.mjs +93 -16
  107. package/templates/vue/server/in-place-render.mjs +35 -0
  108. package/templates/vue/server/inertia.mjs +76 -3
  109. package/templates/vue/server/module-event.mjs +118 -25
  110. package/templates/vue/server/module-route-types.mjs +78 -0
  111. package/templates/vue/server/module-routes.mjs +151 -96
  112. package/templates/vue/server/module-server.mjs +121 -40
  113. package/templates/vue/server/module-testing.mjs +36 -12
  114. package/templates/vue/server/page-props.mjs +94 -3
  115. package/templates/vue/server/page-redirects.mjs +37 -0
  116. package/templates/vue/server/path-patterns.mjs +35 -12
  117. package/templates/vue/server/proxy-compression.mjs +22 -0
  118. package/templates/vue/server/proxy-headers.mjs +12 -2
  119. package/templates/vue/server/proxy-redirects.mjs +21 -5
  120. package/templates/vue/server/proxy-request.mjs +58 -0
  121. package/templates/vue/server/proxy-transfers.mjs +44 -10
  122. package/templates/vue/server/public-endpoints.mjs +139 -28
  123. package/templates/vue/server/public-options.mjs +20 -6
  124. package/templates/vue/server/request-failure.mjs +24 -1
  125. package/templates/vue/server/request-routing.mjs +32 -10
  126. package/templates/vue/server/request-target.mjs +30 -11
  127. package/templates/vue/server/route-rules.mjs +48 -18
  128. package/templates/vue/server/ssr-module.mjs +32 -0
  129. package/templates/vue/server/static-files.mjs +35 -7
  130. package/templates/vue/server/type-guards.mjs +71 -0
  131. package/templates/vue/server/vite-errors.mjs +45 -7
  132. package/templates/vue/server/web-body.mjs +5 -1
  133. package/templates/vue/server-module.ts +12 -2
  134. package/templates/vue/server.mjs +162 -157
  135. package/templates/vue/sfc-global-types.mjs +112 -13
  136. package/templates/vue/ssr-renderer.ts +2 -2
  137. package/templates/vue/ui-inertia-page.mjs +16 -1
  138. package/templates/vue/ui-kbd.mjs +2 -1
  139. package/templates/vue/ui-request-state.mjs +2 -1
  140. package/templates/vue/vite.config.ts +9 -0
@@ -1,10 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { isObjectLike, isString } from "./type-guards.mjs";
4
5
 
5
6
  const CLIENT_MANIFEST_PATH = fileURLToPath(
6
7
  new URL("../dist/client/.vite/manifest.json", import.meta.url),
7
8
  );
9
+ /** @type {string | undefined} */
8
10
  let productionAssetVersion;
9
11
 
10
12
  /**
@@ -27,6 +29,10 @@ export function assetVersion() {
27
29
  return productionAssetVersion;
28
30
  }
29
31
 
32
+ /**
33
+ * @param {InertiaPage} page
34
+ * @returns {string}
35
+ */
30
36
  export function inertiaAppHtml(page) {
31
37
  const serialized = JSON.stringify(page).replaceAll("/", "\\/");
32
38
  return `<script data-page="app" type="application/json">${serialized}</script><div id="app"></div>`;
@@ -35,12 +41,51 @@ export function inertiaAppHtml(page) {
35
41
  /** The one Inertia component every DMS page renders through. */
36
42
  export const DMS_PAGE_COMPONENT = "DmsDynamicPage";
37
43
 
44
+ /**
45
+ * The once props a page declares, by key: the prop each stands for, and when
46
+ * the browser's copy expires (`null`: never).
47
+ *
48
+ * @typedef {Record<string, { prop: string, expiresAt: number | null }>} OnceProps
49
+ */
50
+
51
+ /**
52
+ * The Inertia page object of a visit.
53
+ *
54
+ * @typedef {{
55
+ * component: string,
56
+ * props: Record<string, unknown>,
57
+ * url: string,
58
+ * version: string,
59
+ * onceProps?: OnceProps
60
+ * }} InertiaPage
61
+ */
62
+
63
+ /**
64
+ * Whether the page has the props the server render needs: the path and the
65
+ * page the backend described. A full visit carries both; the page payload is
66
+ * the backend's JSON, read as the renderer reads it.
67
+ *
68
+ * @param {InertiaPage} page
69
+ * @returns {page is InertiaPage & import("../ssr-renderer.ts").DmsSsrPage}
70
+ */
71
+ export function isSsrPage(page) {
72
+ return isString(page.props.path) && isObjectLike(page.props.page);
73
+ }
74
+
75
+ /**
76
+ * @param {string} url
77
+ * @param {Record<string, unknown> | undefined} props
78
+ * @param {string} [version]
79
+ * @param {OnceProps} [onceProps]
80
+ * @returns {InertiaPage}
81
+ */
38
82
  export function createInertiaPage(
39
83
  url,
40
84
  props,
41
85
  version = assetVersion(),
42
86
  onceProps,
43
87
  ) {
88
+ /** @type {InertiaPage} */
44
89
  const page = {
45
90
  component: DMS_PAGE_COMPONENT,
46
91
  props: { ...props, errors: props?.errors ?? {} },
@@ -51,6 +96,11 @@ export function createInertiaPage(
51
96
  return page;
52
97
  }
53
98
 
99
+ /**
100
+ * @param {import("node:http").IncomingMessage} request
101
+ * @param {string} name
102
+ * @returns {string[]}
103
+ */
54
104
  function headerList(request, name) {
55
105
  return String(request.headers[name] ?? "")
56
106
  .split(",")
@@ -63,6 +113,9 @@ function headerList(request, name) {
63
113
  * A partial reload (`router.reload({ only, except })`) of the page on screen
64
114
  * names them: a nested path selects its top-level prop, which is sent whole,
65
115
  * and only a top-level name excludes one.
116
+ *
117
+ * @param {import("node:http").IncomingMessage} request
118
+ * @returns {{ partial: boolean, wants: (prop: string) => boolean }}
66
119
  */
67
120
  export function requestedProps(request) {
68
121
  const partial =
@@ -81,11 +134,16 @@ export function requestedProps(request) {
81
134
  };
82
135
  }
83
136
 
84
- /** The once props the browser already holds, unexpired, by key. */
137
+ /**
138
+ * The once props the browser already holds, unexpired, by key.
139
+ *
140
+ * @param {import("node:http").IncomingMessage} request
141
+ */
85
142
  export function heldOnceProps(request) {
86
143
  return new Set(headerList(request, "x-inertia-except-once-props"));
87
144
  }
88
145
 
146
+ /** @param {string} [version] */
89
147
  export function inertiaHeaders(version = assetVersion()) {
90
148
  return {
91
149
  "cache-control": PRIVATE_CACHE_CONTROL,
@@ -105,6 +163,11 @@ export function htmlHeaders() {
105
163
  };
106
164
  }
107
165
 
166
+ /**
167
+ * @param {import("node:http").IncomingMessage} request
168
+ * @param {import("node:http").ServerResponse} response
169
+ * @returns {boolean}
170
+ */
108
171
  export function handleAssetVersionMismatch(request, response) {
109
172
  if (
110
173
  !request.headers["x-inertia"] ||
@@ -121,15 +184,25 @@ export function handleAssetVersionMismatch(request, response) {
121
184
  return true;
122
185
  }
123
186
 
124
- /** A page visit: a document or Inertia request, as opposed to an API call. */
187
+ /**
188
+ * A page visit: a document or Inertia request, as opposed to an API call.
189
+ *
190
+ * @param {import("node:http").IncomingMessage} request
191
+ * @returns {boolean}
192
+ */
125
193
  export function isFrontendVisit(request) {
126
194
  if (request.method !== "GET") return false;
127
195
  return (
128
196
  Boolean(request.headers["x-inertia"]) ||
129
- request.headers.accept?.includes("text/html")
197
+ request.headers.accept?.includes("text/html") === true
130
198
  );
131
199
  }
132
200
 
201
+ /**
202
+ * @param {import("node:http").IncomingMessage} request
203
+ * @param {import("node:http").ServerResponse} response
204
+ * @param {string} location
205
+ */
133
206
  export function redirectFrontendVisit(request, response, location) {
134
207
  const vary = { vary: "X-Inertia" };
135
208
  if (!request.headers["x-inertia"]) {
@@ -23,14 +23,42 @@ import {
23
23
  relayedLocation,
24
24
  } from "./proxy-redirects.mjs";
25
25
  import { backendUrl } from "./request-target.mjs";
26
+ import {
27
+ isFunction,
28
+ isNumber,
29
+ isObjectLike,
30
+ isRecord,
31
+ isString,
32
+ } from "./type-guards.mjs";
26
33
  import { pipeWebBody } from "./web-body.mjs";
27
34
 
35
+ /** @typedef {import("node:http").IncomingMessage} IncomingMessage */
36
+ /** @typedef {import("./request-target.mjs").ServerRequest} ServerRequest */
37
+ /** @typedef {import("node:http").ServerResponse} ServerResponse */
38
+ /** @typedef {import("../server-module.ts").DmsServerEvent} ServerEvent */
39
+ /** @typedef {import("../server-module.ts").DmsSession} Session */
40
+ /** @typedef {(session: Session | undefined) => import("../server-module.ts").DmsPublicSession} SessionView */
41
+ /**
42
+ * What an event is made from.
43
+ *
44
+ * @typedef {object} EventContext
45
+ * @property {ServerRequest} request
46
+ * @property {ServerResponse} response
47
+ * @property {Pick<import("./module-route-types.mjs").ServerRoute, "bodyLimit">} route
48
+ * @property {Readonly<Record<string, string>>} params
49
+ * @property {{ id: string, options?: unknown, privateOptions?: unknown }} module
50
+ * @property {{ authEstablishEndpoints?: readonly string[] }} [app]
51
+ * @property {SessionView} [view] What a session shows a page
52
+ * @property {string} [authOwner] The module that owns sign-in
53
+ */
54
+
28
55
  const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
29
56
  const COOKIE_NAME = /^[A-Za-z0-9_-]{1,64}$/;
30
57
  /** The sealed cookies of the module that owns sign-in: session, accounts, flows. */
31
58
  const RESERVED_COOKIE_PREFIX = "dms_";
32
59
  const BODY_TOO_LARGE = "Request body too large";
33
60
 
61
+ /** @param {IncomingMessage} request */
34
62
  function requestOrigin(request) {
35
63
  const host = request.headers.host;
36
64
  try {
@@ -40,6 +68,7 @@ function requestOrigin(request) {
40
68
  }
41
69
  }
42
70
 
71
+ /** @param {IncomingMessage} request */
43
72
  function requestHeaders(request) {
44
73
  const headers = new Headers();
45
74
  for (const [name, value] of Object.entries(request.headers)) {
@@ -49,12 +78,17 @@ function requestHeaders(request) {
49
78
  return headers;
50
79
  }
51
80
 
52
- /** The request body as a Web stream that errors past `limit` bytes. */
81
+ /**
82
+ * The request body as a Web stream that errors past `limit` bytes.
83
+ *
84
+ * @param {IncomingMessage} request
85
+ * @param {number} limit
86
+ */
53
87
  function limitedBody(request, limit) {
54
88
  let size = 0;
55
89
  return Readable.toWeb(request).pipeThrough(
56
90
  new TransformStream({
57
- transform(chunk, controller) {
91
+ transform(/** @type {Uint8Array} */ chunk, controller) {
58
92
  size += chunk.byteLength;
59
93
  if (size > limit)
60
94
  controller.error(new RequestBodyError(413, BODY_TOO_LARGE));
@@ -64,6 +98,10 @@ function limitedBody(request, limit) {
64
98
  );
65
99
  }
66
100
 
101
+ /**
102
+ * @param {ServerRequest} request
103
+ * @param {EventContext["route"]} route
104
+ */
67
105
  function webRequest(request, route) {
68
106
  const url = new URL(request.url, requestOrigin(request));
69
107
  const hasBody = !["GET", "HEAD"].includes(request.method);
@@ -78,14 +116,23 @@ function webRequest(request, route) {
78
116
  });
79
117
  }
80
118
 
119
+ /**
120
+ * @param {Request} webRequestValue
121
+ * @returns {Promise<unknown>}
122
+ */
81
123
  async function readJson(webRequestValue) {
82
124
  if (!webRequestValue.body) return {};
125
+ /** @type {Uint8Array[]} */
83
126
  const chunks = [];
84
127
  const reader = webRequestValue.body.getReader();
85
128
  for (;;) {
86
- const { done, value } = await reader.read();
87
- if (done) break;
88
- chunks.push(value);
129
+ const read = await reader.read();
130
+ if (read.done) break;
131
+ /** @type {unknown} */
132
+ const chunk = read.value;
133
+ if (!(chunk instanceof Uint8Array))
134
+ throw new RequestBodyError(400, "Invalid request body");
135
+ chunks.push(chunk);
89
136
  }
90
137
  if (!chunks.length) return {};
91
138
  try {
@@ -95,8 +142,9 @@ async function readJson(webRequestValue) {
95
142
  }
96
143
  }
97
144
 
145
+ /** @param {unknown} name */
98
146
  function checkedCookieName(name) {
99
- if (typeof name !== "string" || !COOKIE_NAME.test(name))
147
+ if (!isString(name) || !COOKIE_NAME.test(name))
100
148
  throw new Error(`Invalid cookie name: ${String(name)}`);
101
149
  if (name === SESSION_COOKIE)
102
150
  throw new Error(
@@ -105,6 +153,12 @@ function checkedCookieName(name) {
105
153
  return name;
106
154
  }
107
155
 
156
+ /**
157
+ * @param {IncomingMessage} request
158
+ * @param {ServerResponse} response
159
+ * @param {SessionView | undefined} view
160
+ * @returns {ServerEvent["session"]}
161
+ */
108
162
  function sessionAccess(request, response, view) {
109
163
  return {
110
164
  read: () => readSession(request),
@@ -124,6 +178,10 @@ function sessionAccess(request, response, view) {
124
178
  * module's are prefixed with its id and a dot, which no name of the owner's
125
179
  * holds, so no module reads or overwrites another's, nor the owner's session
126
180
  * and account cookies.
181
+ *
182
+ * @param {unknown} name
183
+ * @param {string} moduleId
184
+ * @param {string | undefined} authOwner
127
185
  */
128
186
  function moduleCookieName(name, moduleId, authOwner) {
129
187
  const checked = checkedCookieName(name);
@@ -131,15 +189,26 @@ function moduleCookieName(name, moduleId, authOwner) {
131
189
  if (checked.startsWith(RESERVED_COOKIE_PREFIX))
132
190
  throw new Error(
133
191
  `The ${RESERVED_COOKIE_PREFIX}* cookies belong to the module that owns sign-in` +
134
- `${authOwner ? ` (${authOwner})` : ""}: choose another name than ${name}`,
192
+ `${authOwner ? ` (${authOwner})` : ""}: choose another name than ${checked}`,
135
193
  );
136
194
  return `${String(moduleId).replaceAll(/[^A-Za-z0-9_-]/g, "-")}.${checked}`;
137
195
  }
138
196
 
197
+ /**
198
+ * @param {IncomingMessage} request
199
+ * @param {ServerResponse} response
200
+ * @param {string} moduleId
201
+ * @param {string | undefined} authOwner
202
+ * @returns {ServerEvent["cookies"]}
203
+ */
139
204
  function cookieAccess(request, response, moduleId, authOwner) {
140
- const named = (name) => moduleCookieName(name, moduleId, authOwner);
205
+ const named = (/** @type {unknown} */ name) =>
206
+ moduleCookieName(name, moduleId, authOwner);
141
207
  return {
142
- readSealed: (name) => readSealedCookie(request, named(name)),
208
+ readSealed: (name, accepts) => {
209
+ const value = readSealedCookie(request, named(name));
210
+ return value !== undefined && accepts(value) ? value : undefined;
211
+ },
143
212
  writeSealed: (name, value, options = {}) =>
144
213
  writeSealedCookie(
145
214
  response,
@@ -153,6 +222,7 @@ function cookieAccess(request, response, moduleId, authOwner) {
153
222
  };
154
223
  }
155
224
 
225
+ /** @param {unknown} value */
156
226
  function frozenCopy(value) {
157
227
  return Object.freeze(structuredClone(value ?? {}));
158
228
  }
@@ -165,15 +235,24 @@ function frozenCopy(value) {
165
235
  * the defaults every route gets once (same-origin on unsafe methods, a body
166
236
  * limit, `no-store`). See `server-module.ts` for the types a module reads.
167
237
  *
168
- * @param context `{ request, response, route, params, module, app, view,
238
+ * @param {EventContext} context `{ request, response, route, params, module, app, view,
169
239
  * authOwner }`, where `module` is `{ id, options, privateOptions }`, `app`
170
240
  * the deployment's read-only data, `view` what a session shows a page and
171
241
  * `authOwner` the id of the module that owns sign-in
242
+ * @returns {ServerEvent}
172
243
  */
173
244
  export function createServerEvent(context) {
174
245
  const { request, response, route, params } = context;
175
- const event = {
176
- request: webRequest(request, route),
246
+ const webRequestValue = webRequest(request, route);
247
+ // SAFETY: `server-module.ts` lets a handler name the JSON shape it expects from the backend and from the client, as `Response.json()` does; the server parses that JSON and hands it on unchecked, which these two members are the only place to state.
248
+ const parsedJson = /** @type {Pick<ServerEvent, "backend" | "readJson">} */ ({
249
+ backend: (/** @type {string} */ path, init = {}) =>
250
+ backend(path, request, init),
251
+ readJson: () => readJson(webRequestValue),
252
+ });
253
+ return {
254
+ ...parsedJson,
255
+ request: webRequestValue,
177
256
  params: Object.freeze({ ...params }),
178
257
  session: sessionAccess(request, response, context.view),
179
258
  cookies: cookieAccess(
@@ -182,40 +261,48 @@ export function createServerEvent(context) {
182
261
  context.module.id,
183
262
  context.authOwner,
184
263
  ),
185
- backend: (path, init = {}) => backend(path, request, init),
186
- backendUrl: (path) => backendUrl(path),
264
+ backendUrl: (/** @type {string} */ path) => backendUrl(path),
187
265
  clientIp: () => clientIp(request),
188
266
  module: Object.freeze({
189
267
  id: context.module.id,
190
268
  options: frozenCopy(context.module.options),
191
269
  privateOptions: frozenCopy(context.module.privateOptions),
192
270
  }),
193
- renderEmail: (template, props, options = {}) =>
194
- renderEmailTemplate(template, props, options),
271
+ renderEmail: (
272
+ /** @type {string} */ template,
273
+ /** @type {Record<string, unknown>} */ props,
274
+ options = {},
275
+ ) => renderEmailTemplate(template, props, options),
195
276
  app: Object.freeze({
196
277
  authEstablishEndpoints: Object.freeze([
197
278
  ...(context.app?.authEstablishEndpoints ?? []),
198
279
  ]),
199
280
  }),
281
+ url: new URL(webRequestValue.url),
200
282
  };
201
- event.url = new URL(event.request.url);
202
- event.readJson = () => readJson(event.request);
203
- return event;
204
283
  }
205
284
 
285
+ /** @param {unknown} value */
206
286
  function isResponse(value) {
207
287
  return (
208
288
  value instanceof Response ||
209
- (typeof value === "object" &&
210
- value !== null &&
211
- typeof value.status === "number" &&
212
- typeof value.headers?.get === "function" &&
289
+ (isRecord(value) &&
290
+ isNumber(value.status) &&
291
+ isObjectLike(value.headers) &&
292
+ isFunction(value.headers.get) &&
213
293
  "body" in value)
214
294
  );
215
295
  }
216
296
 
297
+ /**
298
+ * @param {any} result What a handler answered: any value, of which only
299
+ * `isResponse` looks
300
+ * @returns {Response}
301
+ */
217
302
  function asResponse(result) {
218
- if (isResponse(result)) return result;
303
+ if (isResponse(result))
304
+ // oxlint-disable-next-line typescript/no-unsafe-return -- `isResponse` also accepts a `Response` of another realm by its members (`status`, `headers.get`, `body`), so it cannot be a `value is Response` predicate without claiming members it does not check.
305
+ return result;
219
306
  if (result === undefined || result === null)
220
307
  return new Response(null, { status: 204 });
221
308
  return Response.json(result);
@@ -227,11 +314,17 @@ function asResponse(result) {
227
314
  * answer the handler relays goes out as the proxy relays one: its `Location`
228
315
  * on the frontend's origin when the frontend proxies its path, no cookie of
229
316
  * another site its fetch reached.
317
+ *
318
+ * @param {ServerRequest} request
319
+ * @param {ServerResponse} response
320
+ * @param {unknown} result
321
+ * @param {(path: string) => boolean} isProxiedPath
230
322
  */
231
323
  async function writeServerResponse(request, response, result, isProxiedPath) {
232
324
  const answer = asResponse(result);
233
325
  const cookies = [response.getHeader("set-cookie") ?? []]
234
326
  .flat()
327
+ .map(String)
235
328
  .concat(relayableCookies(answer));
236
329
  answer.headers.forEach((value, name) => {
237
330
  if (name === "set-cookie") return;
@@ -261,7 +354,7 @@ async function writeServerResponse(request, response, result, isProxiedPath) {
261
354
  * through reaches the server's error handling with the cookies written so
262
355
  * far.
263
356
  *
264
- * @param context See `createServerEvent`, plus `isProxiedPath(pathname)`:
357
+ * @param {Omit<EventContext, "route"> & { route: import("./module-route-types.mjs").ServerRoute, isProxiedPath: (path: string) => boolean }} context See `createServerEvent`, plus `isProxiedPath(pathname)`:
265
358
  * whether the frontend sends that path to the backend
266
359
  */
267
360
  export async function answerModuleRoute(context) {
@@ -0,0 +1,78 @@
1
+ export {};
2
+
3
+ /** @typedef {import("../server-module.ts").DmsServerMethod} ServerMethod */
4
+ /**
5
+ * What `registerAuth` declared. The server binds each hook to the declared
6
+ * object, so a hook may use `this`.
7
+ *
8
+ * @typedef {object} AuthDeclaration
9
+ * @property {string} [signInPath]
10
+ * @property {readonly string[]} [signInExempt]
11
+ * @property {(event: import("../server-module.ts").DmsServerEvent, session: import("../server-module.ts").DmsSession) => Promise<import("../server-module.ts").DmsSession | undefined>} [refreshSession]
12
+ * @property {(session: import("../server-module.ts").DmsSession) => import("../server-module.ts").DmsPublicSession} [publicSession]
13
+ */
14
+ /** @typedef {import("../server-module.ts").DmsRouteRule} RouteRule */
15
+ /**
16
+ * A module that has a `dms.server.ts`.
17
+ *
18
+ * @typedef {object} ServerModuleDeclaration
19
+ * @property {string} id
20
+ * @property {number} [priority]
21
+ * @property {import("../server-module.ts").DmsServerSetup} setup
22
+ */
23
+ /**
24
+ * One `registerRoute` call.
25
+ *
26
+ * @typedef {object} RouteRegistration
27
+ * @property {ServerMethod | readonly ServerMethod[]} method
28
+ * @property {string} path
29
+ * @property {import("../server-module.ts").DmsServerHandler} handler
30
+ * @property {import("../server-module.ts").DmsServerRouteOptions} [options]
31
+ */
32
+ /**
33
+ * What a module's setup declared, by kind, in declaration order.
34
+ *
35
+ * @typedef {object} ModuleDeclared
36
+ * @property {RouteRegistration[]} routes
37
+ * @property {AuthDeclaration[]} auth
38
+ * @property {Readonly<Record<string, RouteRule>>[]} rules
39
+ * @property {{ pattern: string, options?: import("./backend-proxies.mjs").ProxyOptions }[]} proxies
40
+ */
41
+ /** @typedef {ServerModuleDeclaration & { declared: ModuleDeclared }} DeclaredModule */
42
+ /**
43
+ * A route a module serves, one per method.
44
+ *
45
+ * @typedef {object} ServerRoute
46
+ * @property {string} path The normalized path
47
+ * @property {string} pathIdentity The path with its parameters made anonymous
48
+ * @property {string[]} params The parameter names, in path order
49
+ * @property {RegExp} matcher
50
+ * @property {string} id The serving module
51
+ * @property {number} priority
52
+ * @property {number} order The declaration's rank in its module
53
+ * @property {string} method
54
+ * @property {import("../server-module.ts").DmsServerHandler} handler
55
+ * @property {boolean} sameOrigin
56
+ * @property {number} bodyLimit
57
+ * @property {boolean} overridesBackend
58
+ */
59
+ /**
60
+ * The auth hooks of the module that provides them.
61
+ *
62
+ * @typedef {object} AuthProvider
63
+ * @property {string} id
64
+ * @property {number} priority
65
+ * @property {string} [signInPath]
66
+ * @property {import("./path-patterns.mjs").CompiledPathPattern[]} [signInExempt]
67
+ * @property {AuthDeclaration["refreshSession"]} refreshSession
68
+ * @property {AuthDeclaration["publicSession"]} publicSession
69
+ */
70
+ /**
71
+ * What the modules declared, ready to serve.
72
+ *
73
+ * @typedef {object} ServerDeclarations
74
+ * @property {ServerRoute[]} routes
75
+ * @property {AuthProvider | undefined} auth
76
+ * @property {import("./route-rules.mjs").ResolvedRouteRule[]} rules
77
+ * @property {import("./backend-proxies.mjs").BackendProxy[]} proxies
78
+ */