@antelopejs/dms-frontend 0.9.3 → 0.10.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 (116) hide show
  1. package/README.md +18 -24
  2. package/dist/commands/build-action.js +3 -2
  3. package/dist/commands/dev-action.js +2 -1
  4. package/dist/commands/prepare-action.js +1 -1
  5. package/dist/commands/start-action.js +3 -24
  6. package/dist/commands/workspace-task.js +2 -1
  7. package/dist/derived-outputs.js +3 -3
  8. package/dist/discovery.js +17 -6
  9. package/dist/entry-weight.js +15 -0
  10. package/dist/fs-sync.js +7 -4
  11. package/dist/index.js +2 -1
  12. package/dist/internal/built-workspace.d.ts +4 -0
  13. package/dist/internal/built-workspace.js +43 -0
  14. package/dist/internal/ensure.d.ts +1 -0
  15. package/dist/internal/ensure.js +8 -0
  16. package/dist/internal/locale-catalogs.d.ts +8 -3
  17. package/dist/internal/locale-catalogs.js +16 -6
  18. package/dist/internal/manifest-decoder.d.ts +7 -0
  19. package/dist/internal/manifest-decoder.js +81 -0
  20. package/dist/internal/own-release.d.ts +5 -0
  21. package/dist/internal/own-release.js +20 -0
  22. package/dist/internal/plain-object.d.ts +1 -0
  23. package/dist/internal/plain-object.js +6 -0
  24. package/dist/internal/text-list.d.ts +1 -0
  25. package/dist/internal/text-list.js +10 -0
  26. package/dist/internal/vite-manifest.d.ts +6 -0
  27. package/dist/internal/vite-manifest.js +24 -0
  28. package/dist/layer-watch.js +25 -24
  29. package/dist/layers.d.ts +1 -1
  30. package/dist/layers.js +3 -1
  31. package/dist/locale-files.d.ts +2 -1
  32. package/dist/locale-files.js +4 -0
  33. package/dist/manifest.d.ts +1 -1
  34. package/dist/manifest.js +44 -31
  35. package/dist/materialize.js +6 -2
  36. package/dist/module-set.js +2 -1
  37. package/dist/node-options.d.ts +1 -0
  38. package/dist/node-options.js +8 -0
  39. package/dist/output.d.ts +15 -9
  40. package/dist/output.js +30 -18
  41. package/dist/private-values.d.ts +1 -1
  42. package/dist/private-values.js +5 -3
  43. package/dist/project-modules.js +13 -8
  44. package/dist/renderer-range.js +5 -3
  45. package/dist/server-process.js +1 -1
  46. package/dist/verify-source-build.js +10 -2
  47. package/dist/verify-source-client-bundle.d.ts +1 -0
  48. package/dist/verify-source-client-bundle.js +30 -0
  49. package/dist/verify-source-runner.js +44 -60
  50. package/dist/verify-source-sources.js +49 -11
  51. package/dist/workspace-setup.js +1 -1
  52. package/package.json +6 -4
  53. package/skills/dms-frontend/SKILL.md +1 -1
  54. package/templates/vue/app-runtime.ts +2 -9
  55. package/templates/vue/client-server-guard.mjs +16 -0
  56. package/templates/vue/compress-assets.mjs +9 -1
  57. package/templates/vue/entry-weight.mjs +22 -0
  58. package/templates/vue/frontend-module.ts +1 -0
  59. package/templates/vue/frontend-runtime/module-registration.ts +2 -2
  60. package/templates/vue/frontend-runtime/state.ts +2 -2
  61. package/templates/vue/frontend-runtime/types.ts +8 -2
  62. package/templates/vue/head-order.mjs +7 -0
  63. package/templates/vue/icon-scan.mjs +4 -0
  64. package/templates/vue/server/auth/backend.mjs +41 -4
  65. package/templates/vue/server/auth/client-ip.mjs +52 -4
  66. package/templates/vue/server/auth/establish-endpoints.mjs +10 -6
  67. package/templates/vue/server/auth/session.mjs +81 -2
  68. package/templates/vue/server/auth-hooks.mjs +15 -5
  69. package/templates/vue/server/backend-proxies.mjs +65 -9
  70. package/templates/vue/server/backend-request.mjs +61 -0
  71. package/templates/vue/server/backend-response.mjs +23 -6
  72. package/templates/vue/server/built-files.mjs +7 -0
  73. package/templates/vue/server/cli-report.mjs +53 -11
  74. package/templates/vue/server/client-manifest.mjs +23 -1
  75. package/templates/vue/server/content.mjs +18 -1
  76. package/templates/vue/server/dev-private-files.mjs +17 -1
  77. package/templates/vue/server/dev-styles.mjs +38 -0
  78. package/templates/vue/server/email.mjs +9 -3
  79. package/templates/vue/server/favicon.mjs +3 -0
  80. package/templates/vue/server/generated-files.mjs +211 -0
  81. package/templates/vue/server/homepage.mjs +21 -9
  82. package/templates/vue/server/html-tag.mjs +4 -0
  83. package/templates/vue/server/icons.mjs +80 -7
  84. package/templates/vue/server/in-place-render.mjs +35 -0
  85. package/templates/vue/server/inertia.mjs +61 -2
  86. package/templates/vue/server/module-event.mjs +98 -17
  87. package/templates/vue/server/module-route-types.mjs +78 -0
  88. package/templates/vue/server/module-routes.mjs +138 -31
  89. package/templates/vue/server/module-server.mjs +98 -27
  90. package/templates/vue/server/module-testing.mjs +14 -5
  91. package/templates/vue/server/page-props.mjs +92 -2
  92. package/templates/vue/server/page-redirects.mjs +37 -0
  93. package/templates/vue/server/path-patterns.mjs +13 -1
  94. package/templates/vue/server/proxy-compression.mjs +22 -0
  95. package/templates/vue/server/proxy-headers.mjs +11 -1
  96. package/templates/vue/server/proxy-redirects.mjs +20 -5
  97. package/templates/vue/server/proxy-request.mjs +57 -0
  98. package/templates/vue/server/proxy-transfers.mjs +33 -5
  99. package/templates/vue/server/public-endpoints.mjs +138 -23
  100. package/templates/vue/server/public-options.mjs +20 -6
  101. package/templates/vue/server/request-failure.mjs +23 -0
  102. package/templates/vue/server/request-routing.mjs +26 -9
  103. package/templates/vue/server/request-target.mjs +9 -3
  104. package/templates/vue/server/route-rules.mjs +39 -15
  105. package/templates/vue/server/ssr-module.mjs +32 -0
  106. package/templates/vue/server/static-files.mjs +24 -1
  107. package/templates/vue/server/type-guards.mjs +71 -0
  108. package/templates/vue/server/vite-errors.mjs +32 -5
  109. package/templates/vue/server/web-body.mjs +3 -0
  110. package/templates/vue/server-module.ts +10 -1
  111. package/templates/vue/server.mjs +132 -148
  112. package/templates/vue/sfc-global-types.mjs +112 -13
  113. package/templates/vue/ssr-renderer.ts +2 -2
  114. package/templates/vue/ui-inertia-page.mjs +16 -1
  115. package/templates/vue/ui-kbd.mjs +2 -1
  116. package/templates/vue/ui-request-state.mjs +2 -1
@@ -1,4 +1,3 @@
1
- import { ofetch } from "ofetch";
2
1
  import {
3
2
  type App,
4
3
  effectScope,
@@ -19,6 +18,7 @@ import {
19
18
  import {
20
19
  type DmsAppContext,
21
20
  type DmsRoute,
21
+ type DmsServerFetch,
22
22
  type DmsRuntimeConfig,
23
23
  } from "./types";
24
24
 
@@ -65,7 +65,7 @@ export function forgetBrowserRuntime(runtime: DmsFrontendRuntime): void {
65
65
 
66
66
  /** Creates mutable runtime state owned by one Vue application. */
67
67
  export function createDmsFrontendRuntime(
68
- serverFetch?: typeof ofetch,
68
+ serverFetch?: DmsServerFetch,
69
69
  hydratedAsyncData?: Record<string, unknown>,
70
70
  isServer = false,
71
71
  requestCookies?: string,
@@ -1,5 +1,5 @@
1
1
  import { type InertiaLinkProps } from "@inertiajs/vue3";
2
- import { type FetchOptions, ofetch } from "ofetch";
2
+ import { type FetchOptions } from "ofetch";
3
3
  import {
4
4
  type App,
5
5
  type ComputedRef,
@@ -246,6 +246,12 @@ export interface UseAsyncDataOptions<T> {
246
246
  */
247
247
  refreshInterval?: MaybeRefOrGetter<number | null | undefined>;
248
248
  }
249
+ /** What the runtime calls to read the backend: `ofetch`, or a stand-in for it. */
250
+ export type DmsServerFetch = (
251
+ request: string,
252
+ options?: FetchOptions,
253
+ ) => Promise<unknown>;
254
+
249
255
  /**
250
256
  * `useDmsFetch`'s options: ofetch's request options, of which the method, the
251
257
  * query, the params and the body may be refs or getters, and the async data
@@ -271,7 +277,7 @@ export interface UseFetchOptions<T>
271
277
  context: DmsFetchTransformContext,
272
278
  ) => T | Promise<T>;
273
279
  /** Replaces the fetch function; it receives the run's signal. */
274
- $fetch?: typeof ofetch;
280
+ $fetch?: DmsServerFetch;
275
281
  }
276
282
  export interface DmsErrorData {
277
283
  statusCode?: number;
@@ -1,6 +1,10 @@
1
1
  const SCRIPT = /<script[^>]*\stype="module"[^>]*><\/script>/;
2
2
  const STYLESHEET = /[ \t]*<link rel="stylesheet"[^>]*>\n?/;
3
3
 
4
+ /**
5
+ * @param {string} html
6
+ * @returns {string}
7
+ */
4
8
  function deprioritize(html) {
5
9
  return html.replace(SCRIPT, (tag) =>
6
10
  tag.includes("fetchpriority")
@@ -24,6 +28,9 @@ function deprioritize(html) {
24
28
  * HTTP/1.1 the generated server speaks. Kept beside vite.config.ts rather than
25
29
  * inside it so the transform can be exercised on its own, the way
26
30
  * client-manifest.mjs is.
31
+ *
32
+ * @param {string} html
33
+ * @returns {string}
27
34
  */
28
35
  export function orderHeadForFirstPaint(html) {
29
36
  const prioritized = deprioritize(html);
@@ -19,6 +19,10 @@ const SOURCE_SCRIPT_GLOB = "app/**/*.ts";
19
19
  * fetched from the Iconify API at run time. Every frontend source root lives
20
20
  * under the workspace (`frontend-modules/`, enforced when the registry is
21
21
  * written), so a relative glob reaches it. Globs are POSIX on every platform.
22
+ *
23
+ * @param {string} viteRoot
24
+ * @param {readonly string[]} sourceRoots
25
+ * @returns {string[]}
22
26
  */
23
27
  export function iconScanGlobs(viteRoot, sourceRoots) {
24
28
  const sourceGlobs = sourceRoots.map((root) =>
@@ -1,4 +1,5 @@
1
1
  import { REDIRECT_STATUSES } from "../proxy-redirects.mjs";
2
+ import { isString } from "../type-guards.mjs";
2
3
  import { clientCountryHeader, clientIp, clientIpHeader } from "./client-ip.mjs";
3
4
 
4
5
  /**
@@ -14,6 +15,10 @@ export const CLIENT_HEADERS = [
14
15
  ];
15
16
 
16
17
  export class UpstreamError extends Error {
18
+ /**
19
+ * @param {number} status
20
+ * @param {string} message
21
+ */
17
22
  constructor(status, message) {
18
23
  super(message);
19
24
  this.status = status;
@@ -27,9 +32,15 @@ const PUBLIC_AUTH_MESSAGE = /^error\.[a-z0-9_.-]{1,100}$/;
27
32
  */
28
33
  export const GENERIC_ERROR_MESSAGE = "error.500.description";
29
34
 
35
+ /**
36
+ * @param {unknown} data A backend answer: any JSON value, or the text of one
37
+ * that is not JSON
38
+ * @returns {string | undefined}
39
+ */
30
40
  export function publicBackendMessage(data) {
31
- const message = typeof data === "string" ? data : data?.message;
32
- return typeof message === "string" && PUBLIC_AUTH_MESSAGE.test(message)
41
+ /** @type {unknown} */
42
+ const message = isString(data) ? data : data?.message;
43
+ return isString(message) && PUBLIC_AUTH_MESSAGE.test(message)
33
44
  ? message
34
45
  : undefined;
35
46
  }
@@ -37,8 +48,10 @@ export function publicBackendMessage(data) {
37
48
  /**
38
49
  * The headers of a client request the server relays to the backend.
39
50
  *
40
- * @param request The client request
41
- * @param names Lowercase names of the request headers to copy when present
51
+ * @param {import("./client-ip.mjs").ForwardedRequest} request The client
52
+ * request
53
+ * @param {readonly string[]} names Lowercase names of the request headers to
54
+ * copy when present
42
55
  * @returns The client country header, then the copied headers
43
56
  */
44
57
  export function relayedHeaders(request, names) {
@@ -58,8 +71,22 @@ export function relayedHeaders(request, names) {
58
71
  * `options.headers` adds request headers; the client's address and the
59
72
  * relayed client headers are set after them, so no caller replaces those.
60
73
  * A redirect is not followed: it is no JSON answer, so it fails with a 502.
74
+ *
75
+ * @param {string} path
76
+ * @param {import("./client-ip.mjs").ForwardedRequest} request The client
77
+ * request
78
+ * @param {{
79
+ * method?: string,
80
+ * body?: unknown,
81
+ * token?: string,
82
+ * relay?: boolean,
83
+ * headers?: Record<string, string>
84
+ * }} [options]
85
+ * @returns {Promise<unknown>} The answer's JSON, `{}` for a successful one that
86
+ * is not JSON
61
87
  */
62
88
  export async function backend(path, request, options = {}) {
89
+ /** @type {Record<string, string | string[]>} */
63
90
  const headers = Object.fromEntries(
64
91
  Object.entries(options.headers ?? {}).map(([name, value]) => [
65
92
  name.toLowerCase(),
@@ -87,6 +114,7 @@ export async function backend(path, request, options = {}) {
87
114
  throw new UpstreamError(502, GENERIC_ERROR_MESSAGE);
88
115
  }
89
116
  const text = await response.text();
117
+ /** @type {unknown} */
90
118
  let data;
91
119
  try {
92
120
  data = JSON.parse(text);
@@ -105,12 +133,21 @@ export async function backend(path, request, options = {}) {
105
133
  }
106
134
 
107
135
  export class RequestBodyError extends Error {
136
+ /**
137
+ * @param {number} status
138
+ * @param {string} message
139
+ */
108
140
  constructor(status, message) {
109
141
  super(message);
110
142
  this.status = status;
111
143
  }
112
144
  }
113
145
 
146
+ /**
147
+ * @param {import("node:http").ServerResponse} response
148
+ * @param {number} status
149
+ * @param {unknown} value Anything `JSON.stringify` writes
150
+ */
114
151
  export function json(response, status, value) {
115
152
  response.writeHead(status, { "content-type": "application/json" });
116
153
  response.end(JSON.stringify(value));
@@ -1,5 +1,21 @@
1
+ import { isString } from "../type-guards.mjs";
2
+
1
3
  const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9a-z-]+$/;
2
4
 
5
+ /**
6
+ * A request, whose socket is a TLS one (`encrypted`) when the server itself
7
+ * terminates TLS.
8
+ *
9
+ * @typedef {import("node:http").IncomingMessage & {
10
+ * socket: import("node:net").Socket & { encrypted?: boolean }
11
+ * }} ForwardedRequest
12
+ */
13
+
14
+ /**
15
+ * @param {string | undefined} value A header name from the environment
16
+ * @returns {string | undefined} The lowercase name, `undefined` when it is not
17
+ * a header name
18
+ */
3
19
  function headerName(value) {
4
20
  const name = (value ?? "").trim().toLowerCase();
5
21
  return HEADER_NAME.test(name) ? name : undefined;
@@ -17,18 +33,26 @@ function trustedHops() {
17
33
  * so its presence also vouches for the platform's `X-Forwarded-*`. On a path
18
34
  * the platform does not front (a private call from another service), it is
19
35
  * absent and the request falls back to `DMS_TRUSTED_PROXY_HOPS`.
36
+ *
37
+ * @param {ForwardedRequest} request
38
+ * @returns {string | undefined}
20
39
  */
21
40
  function platformClientIp(request) {
22
41
  const name = headerName(process.env.DMS_CLIENT_IP_HEADER);
23
42
  if (!name) return undefined;
24
43
  const value = request.headers?.[name];
25
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
44
+ return isString(value) && value.trim() ? value.trim() : undefined;
26
45
  }
27
46
 
47
+ /** @param {ForwardedRequest} request */
28
48
  function isProxyTrusted(request) {
29
49
  return Boolean(platformClientIp(request)) || trustedHops() > 0;
30
50
  }
31
51
 
52
+ /**
53
+ * @param {ForwardedRequest} request
54
+ * @returns {string}
55
+ */
32
56
  export function requestScheme(request) {
33
57
  if (isProxyTrusted(request)) {
34
58
  const forwarded = String(request.headers["x-forwarded-proto"] ?? "")
@@ -41,6 +65,10 @@ export function requestScheme(request) {
41
65
  return request.socket?.encrypted ? "https" : "http";
42
66
  }
43
67
 
68
+ /**
69
+ * @param {ForwardedRequest} request
70
+ * @returns {string | undefined}
71
+ */
44
72
  function requestHost(request) {
45
73
  if (!isProxyTrusted(request)) return request.headers.host;
46
74
  return (
@@ -62,8 +90,12 @@ function requestHost(request) {
62
90
  * proxy wrote it (`DMS_TRUSTED_PROXY_HOPS` above `0`): otherwise any caller
63
91
  * could have, and the socket's address replaces it. The scheme and the host
64
92
  * are read the same way as the CSRF origin check reads them.
93
+ *
94
+ * @param {ForwardedRequest} request
95
+ * @returns {Record<string, string>}
65
96
  */
66
97
  export function forwardedHeaders(request) {
98
+ /** @type {Record<string, string>} */
67
99
  const headers = {
68
100
  "x-forwarded-for": forwardedFor(request),
69
101
  "x-forwarded-proto": requestScheme(request),
@@ -74,6 +106,10 @@ export function forwardedHeaders(request) {
74
106
  return headers;
75
107
  }
76
108
 
109
+ /**
110
+ * @param {ForwardedRequest} request
111
+ * @returns {string}
112
+ */
77
113
  function forwardedFor(request) {
78
114
  const platformIp = platformClientIp(request);
79
115
  if (platformIp) return platformIp;
@@ -92,12 +128,19 @@ function forwardedFor(request) {
92
128
  * address this server resolved, so the backend reads the same address
93
129
  * without counting hops. Always written, never relayed: a value the caller
94
130
  * sent is replaced. None when the variable is unset.
131
+ *
132
+ * @param {ForwardedRequest} request
133
+ * @returns {Record<string, string>}
95
134
  */
96
135
  export function clientIpHeader(request) {
97
136
  const name = headerName(process.env.DMS_CLIENT_IP_HEADER);
98
137
  return name ? { [name]: clientIp(request) } : {};
99
138
  }
100
139
 
140
+ /**
141
+ * @param {ForwardedRequest} request
142
+ * @returns {boolean}
143
+ */
101
144
  export function isSameOrigin(request) {
102
145
  const origin = request.headers.origin;
103
146
  if (!origin) return false;
@@ -111,6 +154,10 @@ export function isSameOrigin(request) {
111
154
  }
112
155
  }
113
156
 
157
+ /**
158
+ * @param {ForwardedRequest} request
159
+ * @returns {string}
160
+ */
114
161
  export function clientIp(request) {
115
162
  const platformIp = platformClientIp(request);
116
163
  if (platformIp) return platformIp;
@@ -136,6 +183,9 @@ const DEFAULT_COUNTRY_HEADER = "CF-IPCountry";
136
183
  * from a trusted reverse proxy (the platform's client IP header, or
137
184
  * `DMS_TRUSTED_PROXY_HOPS` above `0`): otherwise any caller could have set
138
185
  * it, so nothing is relayed.
186
+ *
187
+ * @param {ForwardedRequest} request
188
+ * @returns {Record<string, string>}
139
189
  */
140
190
  export function clientCountryHeader(request) {
141
191
  const name = headerName(
@@ -143,9 +193,7 @@ export function clientCountryHeader(request) {
143
193
  );
144
194
  if (!name || !isProxyTrusted(request)) return {};
145
195
  const value = request.headers?.[name];
146
- return typeof value === "string" && value.trim()
147
- ? { [name]: value.trim() }
148
- : {};
196
+ return isString(value) && value.trim() ? { [name]: value.trim() } : {};
149
197
  }
150
198
 
151
199
  /**
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { isObjectLike, isString } from "../type-guards.mjs";
2
3
 
3
4
  /**
4
5
  * Absolute backend API paths only: no scheme, no authority, no query string,
@@ -17,6 +18,7 @@ const DECLARED_ENDPOINTS_FILE = new URL(
17
18
  import.meta.url,
18
19
  );
19
20
 
21
+ /** @type {unknown[] | undefined} */
20
22
  let declaredEndpoints;
21
23
 
22
24
  /**
@@ -28,13 +30,15 @@ let declaredEndpoints;
28
30
  * workspace built from a DMS that predates the declaration simply has no file,
29
31
  * and the list falls back to whatever the environment names.
30
32
  *
31
- * @param file Generated file to read
32
- * @returns The declared paths, unfiltered
33
+ * @param {URL | string} [file] Generated file to read
34
+ * @returns {unknown[]} The declared paths, unfiltered
33
35
  */
34
36
  export function readDeclaredEstablishEndpoints(file = DECLARED_ENDPOINTS_FILE) {
35
37
  try {
38
+ /** @type {unknown} */
36
39
  const parsed = JSON.parse(readFileSync(file, "utf8"));
37
- return Array.isArray(parsed?.endpoints) ? parsed.endpoints : [];
40
+ const endpoints = isObjectLike(parsed) ? parsed.endpoints : undefined;
41
+ return Array.isArray(endpoints) ? endpoints : [];
38
42
  } catch {
39
43
  return [];
40
44
  }
@@ -54,8 +58,8 @@ export function readDeclaredEstablishEndpoints(file = DECLARED_ENDPOINTS_FILE) {
54
58
  * (`DMS_AUTH_ESTABLISH_ENDPOINTS`). The module that owns sign-in reads them as
55
59
  * `event.app.authEstablishEndpoints`.
56
60
  *
57
- * @param declaration Comma-separated absolute backend paths from the environment
58
- * @param declared Paths declared by the backend's frontend modules
61
+ * @param {string} [declaration] Comma-separated absolute backend paths from the environment
62
+ * @param {unknown[]} [declared] Paths declared by the backend's frontend modules
59
63
  * @returns The well-formed backend API paths, without duplicates
60
64
  */
61
65
  export function allowedEstablishEndpoints(
@@ -68,7 +72,7 @@ export function allowedEstablishEndpoints(
68
72
  return [
69
73
  ...new Set(
70
74
  [...declared, ...fromEnvironment].filter(
71
- (entry) => typeof entry === "string" && BACKEND_API_PATH.test(entry),
75
+ (entry) => isString(entry) && BACKEND_API_PATH.test(entry),
72
76
  ),
73
77
  ),
74
78
  ];
@@ -9,6 +9,21 @@ export const SESSION_COOKIE = "dms_session";
9
9
  const REQUEST_SESSION = Symbol("dms-request-session");
10
10
  const MAX_AGE = 60 * 60 * 24 * 30;
11
11
 
12
+ /**
13
+ * The session a module writes: the `DmsSession` of `#dms/server`.
14
+ *
15
+ * @typedef {import("../../server-module.ts").DmsSession} Session
16
+ */
17
+
18
+ /**
19
+ * A request, and the session `setRequestSession` set on it for the rest of the
20
+ * request.
21
+ *
22
+ * @typedef {import("node:http").IncomingMessage & {
23
+ * [REQUEST_SESSION]?: Session | undefined
24
+ * }} SessionRequest
25
+ */
26
+
12
27
  /**
13
28
  * A suffix that keeps this instance's cookies apart from another's on the same
14
29
  * host. Browsers scope cookies by hostname, not port: without it, two DMS
@@ -31,7 +46,11 @@ function cookieScope() {
31
46
  return createHash("sha256").update(canonical).digest("hex").slice(0, 12);
32
47
  }
33
48
 
34
- /** The name `name` takes in this instance's cookie jar. */
49
+ /**
50
+ * The name `name` takes in this instance's cookie jar.
51
+ *
52
+ * @param {string} name
53
+ */
35
54
  export function scopedCookieName(name) {
36
55
  return `${name}_${cookieScope()}`;
37
56
  }
@@ -46,6 +65,10 @@ function key() {
46
65
  return createHash("sha256").update(secret).digest();
47
66
  }
48
67
 
68
+ /**
69
+ * @param {unknown} value A session, or the value of a module's sealed cookie
70
+ * @returns {string}
71
+ */
49
72
  function encode(value) {
50
73
  const iv = randomBytes(12);
51
74
  const cipher = createCipheriv("aes-256-gcm", key(), iv);
@@ -58,6 +81,11 @@ function encode(value) {
58
81
  );
59
82
  }
60
83
 
84
+ /**
85
+ * @param {string} value The cookie's value
86
+ * @returns {any} What was sealed, as the JSON it was written from, `undefined`
87
+ * when it does not open
88
+ */
61
89
  function decode(value) {
62
90
  try {
63
91
  const payload = Buffer.from(value, "base64url");
@@ -75,6 +103,11 @@ function decode(value) {
75
103
  }
76
104
  }
77
105
 
106
+ /**
107
+ * @param {import("node:http").IncomingMessage} request
108
+ * @param {string} name
109
+ * @returns {string | undefined}
110
+ */
78
111
  function cookieValue(request, name) {
79
112
  const scoped = scopedCookieName(name);
80
113
  const cookies = request.headers.cookie?.split(";") ?? [];
@@ -86,6 +119,9 @@ function cookieValue(request, name) {
86
119
  /**
87
120
  * Cookie attributes. Without a `maxAge` the cookie lasts as long as the
88
121
  * browser session: it is gone once the browser is closed.
122
+ *
123
+ * @param {number | undefined} maxAge
124
+ * @param {string} [path]
89
125
  */
90
126
  function attributes(maxAge, path = "/") {
91
127
  const secure = process.env.DMS_COOKIE_SECURE !== "false" ? "; Secure" : "";
@@ -93,11 +129,21 @@ function attributes(maxAge, path = "/") {
93
129
  return `Path=${path}; HttpOnly; SameSite=Lax${lifetime}${secure}`;
94
130
  }
95
131
 
96
- /** A session outlives the browser only when it is written `persistent: true`. */
132
+ /**
133
+ * A session outlives the browser only when it is written `persistent: true`.
134
+ *
135
+ * @param {Session} session
136
+ */
97
137
  function lifetime(session) {
98
138
  return session.persistent === true ? MAX_AGE : undefined;
99
139
  }
100
140
 
141
+ /**
142
+ * @param {import("node:http").ServerResponse} response
143
+ * @param {string} name
144
+ * @param {string} value
145
+ * @param {string} cookieAttributes
146
+ */
101
147
  function appendCookie(response, name, value, cookieAttributes) {
102
148
  const cookie = `${scopedCookieName(name)}=${value}; ${cookieAttributes}`;
103
149
  const current = response.getHeader("set-cookie");
@@ -109,16 +155,30 @@ function appendCookie(response, name, value, cookieAttributes) {
109
155
  );
110
156
  }
111
157
 
158
+ /**
159
+ * @param {SessionRequest} request
160
+ * @returns {Session | undefined} The session set for the request, else the one
161
+ * the cookie seals, `undefined` when there is none
162
+ */
112
163
  export function readSession(request) {
113
164
  if (REQUEST_SESSION in request) return request[REQUEST_SESSION];
114
165
  const value = cookieValue(request, SESSION_COOKIE);
166
+ // oxlint-disable-next-line typescript/no-unsafe-return -- `decode` returns the JSON this server sealed under `SESSION_COOKIE`, untyped because `JSON.parse` is; the AES-GCM tag rejects any cookie it did not write.
115
167
  return value ? decode(value) : undefined;
116
168
  }
117
169
 
170
+ /**
171
+ * @param {SessionRequest} request
172
+ * @param {Session | undefined} session
173
+ */
118
174
  export function setRequestSession(request, session) {
119
175
  request[REQUEST_SESSION] = session;
120
176
  }
121
177
 
178
+ /**
179
+ * @param {import("node:http").ServerResponse} response
180
+ * @param {Session} session
181
+ */
122
182
  export function writeSession(response, session) {
123
183
  appendCookie(
124
184
  response,
@@ -128,19 +188,38 @@ export function writeSession(response, session) {
128
188
  );
129
189
  }
130
190
 
191
+ /** @param {import("node:http").ServerResponse} response */
131
192
  export function clearSession(response) {
132
193
  appendCookie(response, SESSION_COOKIE, "", attributes(0));
133
194
  }
134
195
 
196
+ /**
197
+ * @param {import("node:http").IncomingMessage} request
198
+ * @param {string} name
199
+ * @returns {unknown} What was sealed, as the JSON it was written from,
200
+ * `undefined` when there is none or it does not open
201
+ */
135
202
  export function readSealedCookie(request, name) {
136
203
  const value = cookieValue(request, name);
137
204
  return value ? decode(value) : undefined;
138
205
  }
139
206
 
207
+ /**
208
+ * @param {import("node:http").ServerResponse} response
209
+ * @param {string} name
210
+ * @param {unknown} value
211
+ * @param {number | undefined} maxAge
212
+ * @param {string} [path]
213
+ */
140
214
  export function writeSealedCookie(response, name, value, maxAge, path = "/") {
141
215
  appendCookie(response, name, encode(value), attributes(maxAge, path));
142
216
  }
143
217
 
218
+ /**
219
+ * @param {import("node:http").ServerResponse} response
220
+ * @param {string} name
221
+ * @param {string} [path]
222
+ */
144
223
  export function clearSealedCookie(response, name, path = "/") {
145
224
  appendCookie(response, name, "", attributes(0, path));
146
225
  }
@@ -12,6 +12,8 @@ const HOOK_ROUTE = Object.freeze({ bodyLimit: 0 });
12
12
  * sign-in page and everything below it, whole segments only (`/auth` exempts
13
13
  * `/auth/callback`, never `/authors`), and the onboarding page. Derived from
14
14
  * the sign-in path in use, or a custom one would redirect to itself.
15
+ *
16
+ * @param {string} signInPath
15
17
  */
16
18
  function defaultExempt(signInPath) {
17
19
  const signIn = signInPath === "/" ? "/" : `${signInPath}/**`;
@@ -22,6 +24,9 @@ function defaultExempt(signInPath) {
22
24
  * The page props `user` and `session` of `session`, from the provider's
23
25
  * `publicSession`. The server adds the `updatedAt` stamp it wrote itself,
24
26
  * which the browser compares to drop a page restored from its cache.
27
+ *
28
+ * @param {NonNullable<import("./module-route-types.mjs").AuthProvider["publicSession"]>} publicSession
29
+ * @param {import("./module-event.mjs").Session | undefined} session
25
30
  */
26
31
  function hookView(publicSession, session) {
27
32
  if (!session) return {};
@@ -40,9 +45,9 @@ function hookView(publicSession, session) {
40
45
  * `dms.server.ts`. Without one, a page carries no session and a refused session
41
46
  * is not renewed.
42
47
  *
43
- * @param provider The resolved auth hooks, `undefined` when no module
48
+ * @param {import("./module-route-types.mjs").AuthProvider | undefined} provider The resolved auth hooks, `undefined` when no module
44
49
  * registers any
45
- * @param context `(id) => ({ module, app })`, a module's event context
50
+ * @param {(id: string) => import("./module-server.mjs").ModuleContext} context `(id) => ({ module, app })`, a module's event context
46
51
  * @returns `{ provider, signInPath, isSignInExempt, signInLocation, view,
47
52
  * refresh }`
48
53
  */
@@ -54,16 +59,21 @@ export function serverAuth(provider, context) {
54
59
  return {
55
60
  provider: provider?.id,
56
61
  signInPath,
57
- isSignInExempt: (pathname) =>
62
+ isSignInExempt: (/** @type {string} */ pathname) =>
58
63
  exempt.some((pattern) => pattern.test(pathname)),
59
- signInLocation: (requestUrl) =>
64
+ signInLocation: (/** @type {string} */ requestUrl) =>
60
65
  `${signInPath}?redirect=${encodeURIComponent(requestUrl)}`,
61
66
  view: publicSession
62
- ? (session) => hookView(publicSession, session)
67
+ ? (
68
+ /** @type {import("./module-event.mjs").Session | undefined} */ session,
69
+ ) => hookView(publicSession, session)
63
70
  : () => ({}),
64
71
  /**
65
72
  * Renews the request's session after the backend refused it. Returns the
66
73
  * new session, which the rest of the request runs with, or `undefined`.
74
+ *
75
+ * @param {import("node:http").IncomingMessage} request
76
+ * @param {import("node:http").ServerResponse} response
67
77
  */
68
78
  async refresh(request, response) {
69
79
  if (!refreshSession) return undefined;