@antelopejs/dms-frontend 0.9.2 → 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 (123) 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/config.js +3 -0
  8. package/dist/derived-outputs.js +3 -3
  9. package/dist/discovery.js +17 -6
  10. package/dist/entry-weight.js +15 -0
  11. package/dist/fs-sync.js +7 -4
  12. package/dist/index.js +2 -1
  13. package/dist/internal/built-workspace.d.ts +4 -0
  14. package/dist/internal/built-workspace.js +43 -0
  15. package/dist/internal/ensure.d.ts +1 -0
  16. package/dist/internal/ensure.js +8 -0
  17. package/dist/internal/locale-catalogs.d.ts +8 -3
  18. package/dist/internal/locale-catalogs.js +16 -6
  19. package/dist/internal/manifest-decoder.d.ts +7 -0
  20. package/dist/internal/manifest-decoder.js +81 -0
  21. package/dist/internal/own-release.d.ts +5 -0
  22. package/dist/internal/own-release.js +20 -0
  23. package/dist/internal/plain-object.d.ts +1 -0
  24. package/dist/internal/plain-object.js +6 -0
  25. package/dist/internal/text-list.d.ts +1 -0
  26. package/dist/internal/text-list.js +10 -0
  27. package/dist/internal/vite-manifest.d.ts +6 -0
  28. package/dist/internal/vite-manifest.js +24 -0
  29. package/dist/layer-watch.js +25 -24
  30. package/dist/layers.d.ts +1 -1
  31. package/dist/layers.js +3 -1
  32. package/dist/locale-files.d.ts +2 -1
  33. package/dist/locale-files.js +4 -0
  34. package/dist/manifest.d.ts +1 -1
  35. package/dist/manifest.js +44 -31
  36. package/dist/materialize.js +6 -2
  37. package/dist/module-set.js +2 -1
  38. package/dist/node-options.d.ts +1 -0
  39. package/dist/node-options.js +8 -0
  40. package/dist/output.d.ts +15 -9
  41. package/dist/output.js +30 -18
  42. package/dist/private-values.d.ts +1 -1
  43. package/dist/private-values.js +5 -3
  44. package/dist/project-modules.js +13 -8
  45. package/dist/renderer-range.js +5 -3
  46. package/dist/server-process.js +1 -1
  47. package/dist/verify-source-build.js +10 -2
  48. package/dist/verify-source-client-bundle.d.ts +1 -0
  49. package/dist/verify-source-client-bundle.js +30 -0
  50. package/dist/verify-source-runner.js +44 -60
  51. package/dist/verify-source-sources.js +49 -11
  52. package/dist/workspace-setup.js +1 -1
  53. package/package.json +6 -4
  54. package/skills/dms-frontend/SKILL.md +1 -1
  55. package/templates/vue/app-runtime.ts +90 -16
  56. package/templates/vue/client-server-guard.mjs +16 -0
  57. package/templates/vue/compress-assets.mjs +9 -1
  58. package/templates/vue/entry-weight.mjs +22 -0
  59. package/templates/vue/frontend-module.ts +1 -0
  60. package/templates/vue/frontend-runtime/module-registration.ts +2 -2
  61. package/templates/vue/frontend-runtime/state.ts +2 -2
  62. package/templates/vue/frontend-runtime/types.ts +8 -2
  63. package/templates/vue/globals.d.ts +5 -0
  64. package/templates/vue/head-order.mjs +7 -0
  65. package/templates/vue/icon-bundle.ts +31 -0
  66. package/templates/vue/icon-scan.mjs +4 -0
  67. package/templates/vue/message-subset.ts +65 -0
  68. package/templates/vue/network-error.ts +4 -0
  69. package/templates/vue/server/auth/backend.mjs +41 -4
  70. package/templates/vue/server/auth/client-ip.mjs +52 -4
  71. package/templates/vue/server/auth/establish-endpoints.mjs +10 -6
  72. package/templates/vue/server/auth/session.mjs +81 -2
  73. package/templates/vue/server/auth-hooks.mjs +15 -5
  74. package/templates/vue/server/backend-proxies.mjs +65 -9
  75. package/templates/vue/server/backend-request.mjs +61 -0
  76. package/templates/vue/server/backend-response.mjs +23 -6
  77. package/templates/vue/server/built-files.mjs +7 -0
  78. package/templates/vue/server/cli-report.mjs +53 -11
  79. package/templates/vue/server/client-manifest.mjs +23 -1
  80. package/templates/vue/server/content.mjs +18 -1
  81. package/templates/vue/server/dev-private-files.mjs +17 -1
  82. package/templates/vue/server/dev-styles.mjs +38 -0
  83. package/templates/vue/server/email.mjs +9 -3
  84. package/templates/vue/server/favicon.mjs +3 -0
  85. package/templates/vue/server/generated-files.mjs +211 -0
  86. package/templates/vue/server/homepage.mjs +21 -9
  87. package/templates/vue/server/html-tag.mjs +4 -0
  88. package/templates/vue/server/icons.mjs +80 -7
  89. package/templates/vue/server/in-place-render.mjs +35 -0
  90. package/templates/vue/server/inertia.mjs +61 -2
  91. package/templates/vue/server/module-event.mjs +98 -17
  92. package/templates/vue/server/module-route-types.mjs +78 -0
  93. package/templates/vue/server/module-routes.mjs +138 -31
  94. package/templates/vue/server/module-server.mjs +98 -27
  95. package/templates/vue/server/module-testing.mjs +14 -5
  96. package/templates/vue/server/page-props.mjs +92 -2
  97. package/templates/vue/server/page-redirects.mjs +37 -0
  98. package/templates/vue/server/path-patterns.mjs +13 -1
  99. package/templates/vue/server/proxy-compression.mjs +22 -0
  100. package/templates/vue/server/proxy-headers.mjs +11 -1
  101. package/templates/vue/server/proxy-redirects.mjs +20 -5
  102. package/templates/vue/server/proxy-request.mjs +57 -0
  103. package/templates/vue/server/proxy-transfers.mjs +33 -5
  104. package/templates/vue/server/public-endpoints.mjs +138 -23
  105. package/templates/vue/server/public-options.mjs +20 -6
  106. package/templates/vue/server/request-failure.mjs +23 -0
  107. package/templates/vue/server/request-routing.mjs +26 -9
  108. package/templates/vue/server/request-target.mjs +9 -3
  109. package/templates/vue/server/route-rules.mjs +39 -15
  110. package/templates/vue/server/ssr-module.mjs +32 -0
  111. package/templates/vue/server/static-files.mjs +24 -1
  112. package/templates/vue/server/type-guards.mjs +71 -0
  113. package/templates/vue/server/vite-errors.mjs +32 -5
  114. package/templates/vue/server/web-body.mjs +3 -0
  115. package/templates/vue/server-module.ts +10 -1
  116. package/templates/vue/server.mjs +132 -148
  117. package/templates/vue/sfc-global-types.mjs +112 -13
  118. package/templates/vue/ssr-renderer.ts +12 -3
  119. package/templates/vue/ui-inertia-page.mjs +16 -1
  120. package/templates/vue/ui-kbd.mjs +2 -1
  121. package/templates/vue/ui-lazy-icons.mjs +44 -0
  122. package/templates/vue/ui-request-state.mjs +2 -1
  123. package/templates/vue/vite.config.ts +2 -0
@@ -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;
@@ -1,4 +1,36 @@
1
1
  import { compilePathPattern } from "./path-patterns.mjs";
2
+ import { isNonNullObject } from "./type-guards.mjs";
3
+
4
+ /** @typedef {import("../server-module.ts").DmsBackendProxyOptions} ProxyOptions */
5
+ /**
6
+ * A backend prefix the server proxies, compiled from a pattern `proxyRejection`
7
+ * accepted.
8
+ *
9
+ * @typedef {object} BackendProxy
10
+ * @property {import("./path-patterns.mjs").CompiledPathPattern} pattern
11
+ * @property {string} path The pattern as declared
12
+ * @property {string} owner Who declared it
13
+ * @property {NonNullable<ProxyOptions["auth"]>} auth
14
+ * @property {number} [maxBodyBytes]
15
+ */
16
+ /**
17
+ * What `registerBackendProxy` declared, per call.
18
+ *
19
+ * @typedef {object} ProxyDeclaration
20
+ * @property {string} id The declaring frontend module
21
+ * @property {string} pattern
22
+ * @property {ProxyOptions} [options]
23
+ */
24
+ /**
25
+ * What a backend module declared in the manifest, or an operator in
26
+ * `DMS_BACKEND_PROXIES`.
27
+ *
28
+ * @typedef {object} ManifestProxy
29
+ * @property {string} [module]
30
+ * @property {string} [path]
31
+ * @property {ProxyOptions["auth"]} [auth]
32
+ * @property {number} [maxBodyBytes]
33
+ */
2
34
 
3
35
  /** How the proxy authenticates a request to the backend. */
4
36
  export const PROXY_AUTH_MODES = Object.freeze(["session", "none"]);
@@ -6,8 +38,10 @@ const DEFAULT_AUTH = "session";
6
38
  /** The renderer's own paths, never a backend's. */
7
39
  const RENDERER_PREFIXES = ["/api/_dms", "/@", "/assets", "/node_modules"];
8
40
 
41
+ /** @param {string} pattern */
9
42
  function literalPrefix(pattern) {
10
43
  const segments = pattern === "/" ? [] : pattern.slice(1).split("/");
44
+ /** @type {string[]} */
11
45
  const literal = [];
12
46
  for (const segment of segments) {
13
47
  if (segment === "*" || segment === "**") break;
@@ -16,7 +50,12 @@ function literalPrefix(pattern) {
16
50
  return `/${literal.join("/")}`;
17
51
  }
18
52
 
19
- /** Why a proxy declaration is ignored, or `undefined`. */
53
+ /**
54
+ * Why a proxy declaration is ignored, or `undefined`.
55
+ *
56
+ * @param {string} pattern
57
+ * @param {ProxyOptions | undefined} options
58
+ */
20
59
  export function proxyRejection(pattern, options) {
21
60
  const compiled = compilePathPattern(pattern);
22
61
  if (!compiled) return "the pattern is not /x, /x/* or /x/**";
@@ -29,7 +68,7 @@ export function proxyRejection(pattern, options) {
29
68
  )
30
69
  )
31
70
  return `${RENDERER_PREFIXES.join(", ")} are the renderer's own paths`;
32
- if (options !== undefined && (typeof options !== "object" || !options))
71
+ if (options !== undefined && !isNonNullObject(options))
33
72
  return "its options are not an object";
34
73
  if (options?.auth !== undefined && !PROXY_AUTH_MODES.includes(options.auth))
35
74
  return `auth is not one of ${PROXY_AUTH_MODES.map((mode) => `"${mode}"`).join(", ")}`;
@@ -41,6 +80,12 @@ export function proxyRejection(pattern, options) {
41
80
  return undefined;
42
81
  }
43
82
 
83
+ /**
84
+ * @param {string} pattern A pattern `proxyRejection` accepted
85
+ * @param {ProxyOptions | undefined} options
86
+ * @param {string} owner Who declared it
87
+ * @returns {BackendProxy}
88
+ */
44
89
  function compileProxy(pattern, options, owner) {
45
90
  return {
46
91
  pattern: compilePathPattern(pattern),
@@ -60,16 +105,22 @@ function compileProxy(pattern, options, owner) {
60
105
  * (`backendProxies`, next to its `publicEndpoints`). A path no prefix covers is
61
106
  * never sent to the backend.
62
107
  *
63
- * @param declared `{ id, pattern, options }` per `registerBackendProxy` call,
108
+ * @param {readonly ProxyDeclaration[]} declared `{ id, pattern, options }` per `registerBackendProxy` call,
64
109
  * from the highest-priority module down
65
- * @param manifest `{ module, path, auth?, maxBodyBytes? }` per backend
110
+ * @param {readonly (ManifestProxy | null | undefined)[]} manifest `{ module, path, auth?, maxBodyBytes? }` per backend
66
111
  * module declaration
67
- * @param warn Reports a declaration problem
68
- * @returns The proxies, the most specific pattern first: a request reaches
112
+ * @param {(message: string) => void} warn Reports a declaration problem
113
+ * @returns {BackendProxy[]} The proxies, the most specific pattern first: a request reaches
69
114
  * the first that matches
70
115
  */
71
116
  export function resolveBackendProxies(declared, manifest, warn) {
117
+ /** @type {Map<string | undefined, BackendProxy>} */
72
118
  const kept = new Map();
119
+ /**
120
+ * @param {string | undefined} pattern
121
+ * @param {ProxyOptions | undefined} options
122
+ * @param {string} owner
123
+ */
73
124
  const add = (pattern, options, owner) => {
74
125
  const reason = proxyRejection(pattern, options);
75
126
  const current = kept.get(pattern);
@@ -94,7 +145,12 @@ export function resolveBackendProxies(declared, manifest, warn) {
94
145
  );
95
146
  }
96
147
 
97
- /** The backend proxy a path reaches, or `undefined`. */
148
+ /**
149
+ * The backend proxy a path reaches, or `undefined`.
150
+ *
151
+ * @param {readonly BackendProxy[]} proxies
152
+ * @param {string} pathname
153
+ */
98
154
  export function matchBackendProxy(proxies, pathname) {
99
155
  return proxies.find((proxy) => proxy.pattern.test(pathname));
100
156
  }
@@ -103,8 +159,8 @@ export function matchBackendProxy(proxies, pathname) {
103
159
  * The lines the server prints at startup: each backend prefix, who declared
104
160
  * it and how it authenticates, then who owns the rest.
105
161
  *
106
- * @param proxies What `resolveBackendProxies` returned
107
- * @param routeCount The number of module routes
162
+ * @param {readonly BackendProxy[]} proxies What `resolveBackendProxies` returned
163
+ * @param {number} routeCount The number of module routes
108
164
  */
109
165
  export function routingReport(proxies, routeCount) {
110
166
  const width = Math.max(0, ...proxies.map((proxy) => proxy.path.length));
@@ -0,0 +1,61 @@
1
+ import { CLIENT_HEADERS, relayedHeaders } from "./auth/backend.mjs";
2
+ import { forwardedHeaders } from "./auth/client-ip.mjs";
3
+ import { readSession } from "./auth/session.mjs";
4
+ import { backendResponseError } from "./backend-response.mjs";
5
+ import { serverReads } from "./in-place-render.mjs";
6
+
7
+ /**
8
+ * The headers of a backend request: the relayed client headers, the client's
9
+ * Origin and Referer as received, the `X-Forwarded-*` headers (set last, so no
10
+ * caller writes them), and the session's access token unless the request goes
11
+ * anonymously.
12
+ *
13
+ * @param {import("node:http").IncomingMessage} request
14
+ * @param {{ anonymous: boolean, headers: readonly string[] }} [access]
15
+ * @returns {Record<string, string>}
16
+ */
17
+ export function backendHeaders(
18
+ request,
19
+ access = { anonymous: false, headers: [] },
20
+ ) {
21
+ /** @type {Record<string, string>} */
22
+ const headers = {
23
+ ...relayedHeaders(request, [
24
+ "accept",
25
+ "content-type",
26
+ "if-match",
27
+ "origin",
28
+ "referer",
29
+ ...CLIENT_HEADERS,
30
+ ...access.headers,
31
+ ]),
32
+ ...forwardedHeaders(request),
33
+ };
34
+ const session = access.anonymous ? undefined : readSession(request);
35
+ if (session?.accessToken)
36
+ headers.authorization = `Bearer ${session.accessToken}`;
37
+ return headers;
38
+ }
39
+
40
+ /**
41
+ * @param {import("node:http").IncomingMessage} request
42
+ * @returns {import("./in-place-render.mjs").ServerReads}
43
+ */
44
+ export function serverComponentFetch(request) {
45
+ return serverReads(() => backendHeaders(request));
46
+ }
47
+
48
+ /**
49
+ * @param {string} path
50
+ * @param {import("node:http").IncomingMessage} request
51
+ * @returns {Promise<unknown>}
52
+ */
53
+ export async function backendJson(path, request) {
54
+ const response = await fetch(new URL(path, process.env.DMS_API_BASE_URL), {
55
+ headers: backendHeaders(request),
56
+ });
57
+ if (!response.ok) {
58
+ throw await backendResponseError(response);
59
+ }
60
+ return response.json();
61
+ }
@@ -1,5 +1,6 @@
1
1
  import { GENERIC_ERROR_MESSAGE } from "./auth/backend.mjs";
2
2
  import { isFrontendVisit, redirectFrontendVisit } from "./inertia.mjs";
3
+ import { isString } from "./type-guards.mjs";
3
4
 
4
5
  /**
5
6
  * What a JSON caller gets when the backend fails: the generic i18n key of
@@ -18,6 +19,10 @@ const FORBIDDEN = 403;
18
19
  const FRONTEND_ORIGIN = "http://frontend.local";
19
20
 
20
21
  export class BackendResponseError extends Error {
22
+ /**
23
+ * @param {number} status
24
+ * @param {string | undefined} code The typed refusal code of the answer
25
+ */
21
26
  constructor(status, code) {
22
27
  super(`DMS backend returned ${status}`);
23
28
  this.status = status;
@@ -28,21 +33,28 @@ export class BackendResponseError extends Error {
28
33
  /**
29
34
  * The refusal code in a backend answer: the string or the `message` of its
30
35
  * JSON body, or the body itself when it is not JSON (a bare code).
36
+ *
37
+ * @param {Response} response
38
+ * @returns {Promise<string | undefined>}
31
39
  */
32
40
  async function refusalCode(response) {
33
41
  const text = (await response.text().catch(() => "")).trim();
34
42
  if (text.length > REFUSAL_CODE_MAX_LENGTH) return undefined;
43
+ /** @type {unknown} */
35
44
  let value = text;
36
45
  try {
46
+ /** @type {unknown} */
37
47
  const data = JSON.parse(text);
38
- value = typeof data === "string" ? data : data?.message;
48
+ value = isString(data) ? data : data?.message;
39
49
  } catch {}
40
- return typeof value === "string" && REFUSAL_CODE.test(value)
41
- ? value
42
- : undefined;
50
+ return isString(value) && REFUSAL_CODE.test(value) ? value : undefined;
43
51
  }
44
52
 
45
- /** The error for a failed backend response, with its typed code if any. */
53
+ /**
54
+ * The error for a failed backend response, with its typed code if any.
55
+ *
56
+ * @param {Response} response
57
+ */
46
58
  export async function backendResponseError(response) {
47
59
  return new BackendResponseError(response.status, await refusalCode(response));
48
60
  }
@@ -54,7 +66,11 @@ export async function backendResponseError(response) {
54
66
  * `loadRenderer` resolves. Only a same-origin path other than the refused one
55
67
  * qualifies: a refused destination sent to itself again would loop.
56
68
  *
57
- * @returns Whether the visit was redirected
69
+ * @param {unknown} error What the request failed with
70
+ * @param {import("node:http").IncomingMessage} request
71
+ * @param {import("node:http").ServerResponse} response
72
+ * @param {() => Promise<typeof import("../ssr-renderer.ts")>} loadRenderer
73
+ * @returns {Promise<boolean>} Whether the visit was redirected
58
74
  */
59
75
  export async function redirectAccessRefusal(
60
76
  error,
@@ -69,6 +85,7 @@ export async function redirectAccessRefusal(
69
85
  !error.code
70
86
  )
71
87
  return false;
88
+ /** @type {string | undefined} */
72
89
  let location;
73
90
  try {
74
91
  location = (await loadRenderer()).accessRedirect?.(error.code);
@@ -14,6 +14,9 @@ const PUBLIC_DOT_DIRECTORY = ".well-known";
14
14
  * source path of every module file), and a module's `public/` may hold dot
15
15
  * files too. `/.well-known/`, which RFC 8615 reserves for files meant to be
16
16
  * public, stays served.
17
+ *
18
+ * @param {string} relativePath
19
+ * @returns {boolean}
17
20
  */
18
21
  export function isPublicBuildPath(relativePath) {
19
22
  return relativePath
@@ -33,6 +36,10 @@ export function isPublicBuildPath(relativePath) {
33
36
  *
34
37
  * The prefix uses the native separator: `resolve` answers with backslashes on
35
38
  * Windows.
39
+ *
40
+ * @param {string} root
41
+ * @param {string} pathname
42
+ * @returns {string | undefined}
36
43
  */
37
44
  export function publicBuildFile(root, pathname) {
38
45
  let decoded;
@@ -1,9 +1,16 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { stripVTControlCharacters } from "node:util";
4
+ import { isNumber, isObjectLike, isRecord, isString } from "./type-guards.mjs";
4
5
 
5
6
  const WILDCARD_ADDRESSES = ["0.0.0.0", "::"];
6
7
 
8
+ /**
9
+ * @param {import("node:child_process").Serializable} message
10
+ * @param {string} line
11
+ * @param {(line: string) => void} write
12
+ * @returns {Promise<void>}
13
+ */
7
14
  async function report(message, line, write) {
8
15
  if (!process.send) return write(line);
9
16
  await new Promise((sent) => process.send(message, () => sent()));
@@ -15,6 +22,10 @@ async function report(message, line, write) {
15
22
  * What the server tells whoever started it. Started by `ajs dms dev` or `ajs
16
23
  * dms start`, it reports over IPC and the CLI words what the user reads.
17
24
  * Started on its own, it prints the line itself.
25
+ *
26
+ * @param {string} address
27
+ * @param {number} port
28
+ * @returns {Promise<void>}
18
29
  */
19
30
  export async function reportReady(address, port) {
20
31
  const host = WILDCARD_ADDRESSES.includes(address)
@@ -33,6 +44,11 @@ export async function reportReady(address, port) {
33
44
  * The server cannot listen: a port taken by another process, an address this
34
45
  * host does not have. Reported instead of Node's unhandled-error dump, then
35
46
  * the server exits.
47
+ *
48
+ * @param {NodeJS.ErrnoException} error
49
+ * @param {string} host
50
+ * @param {number} port
51
+ * @returns {Promise<void>}
36
52
  */
37
53
  export async function reportListenError(error, host, port) {
38
54
  await report(
@@ -52,31 +68,46 @@ export async function reportListenError(error, host, port) {
52
68
  /** The `[PARSE_ERROR] ` Rolldown puts in front of what it says. */
53
69
  const RULE_PREFIX = /^\[[A-Z_]+\]\s*/;
54
70
 
55
- /** The first problem Rolldown reports when it cannot bundle a config. */
71
+ /**
72
+ * The first problem Rolldown reports when it cannot bundle a config.
73
+ *
74
+ * @param {unknown} error What the startup threw
75
+ */
56
76
  function bundlerError(error) {
57
- const first = error?.errors?.[0];
58
- return first && typeof first === "object" ? first : undefined;
77
+ const errors = isObjectLike(error) ? error.errors : undefined;
78
+ const first = isObjectLike(errors) ? errors[0] : undefined;
79
+ return isRecord(first) ? first : undefined;
59
80
  }
60
81
 
61
82
  /**
62
83
  * Where a startup error points, when it says: Rolldown's location for a config
63
84
  * it could not bundle (the line is read back from the file, which Rolldown
64
85
  * only draws in a colored frame), Rollup's for a plugin that failed on a file.
86
+ *
87
+ * @param {unknown} error What the startup threw
65
88
  */
66
89
  function errorLocation(error) {
67
- const source = bundlerError(error) ?? error;
68
- const file = source?.loc?.file ?? source?.id;
69
- if (typeof file !== "string") return undefined;
70
- const { line, column } = source.loc ?? {};
90
+ const source =
91
+ bundlerError(error) ?? (isObjectLike(error) ? error : undefined);
92
+ const loc = isObjectLike(source?.loc) ? source.loc : undefined;
93
+ const file = loc?.file ?? source?.id;
94
+ if (!isString(file)) return undefined;
95
+ const line = isNumber(loc?.line) ? loc.line : undefined;
71
96
  return {
72
97
  file: resolve(file),
73
98
  line,
74
- column,
99
+ column: isNumber(loc?.column) ? loc.column : undefined,
75
100
  lineText: line === undefined ? undefined : readLine(file, line),
76
101
  };
77
102
  }
78
103
 
79
- /** Line `line` (1-based) of `file`, `undefined` when it cannot be read. */
104
+ /**
105
+ * Line `line` (1-based) of `file`, `undefined` when it cannot be read.
106
+ *
107
+ * @param {string} file
108
+ * @param {number} line
109
+ * @returns {string | undefined}
110
+ */
80
111
  function readLine(file, line) {
81
112
  try {
82
113
  return readFileSync(file, "utf8").split(/\r?\n/)[line - 1];
@@ -85,7 +116,11 @@ function readLine(file, line) {
85
116
  }
86
117
  }
87
118
 
88
- /** The first line of what went wrong, without Rolldown's rule and frame. */
119
+ /**
120
+ * The first line of what went wrong, without Rolldown's rule and frame.
121
+ *
122
+ * @param {unknown} error What the startup threw
123
+ */
89
124
  function errorText(error) {
90
125
  const reported = bundlerError(error);
91
126
  const message = reported
@@ -101,6 +136,8 @@ function errorText(error) {
101
136
  /**
102
137
  * Vite could not start, so no page can be served: the error and where it
103
138
  * points instead of a stack trace, then the server exits.
139
+ *
140
+ * @param {unknown} error What the startup threw
104
141
  */
105
142
  export async function reportStartError(error) {
106
143
  const message = errorText(error);
@@ -109,7 +146,12 @@ export async function reportStartError(error) {
109
146
  ? `${location.file}${location.line ? `:${location.line}:${location.column ?? 0}` : ""}: `
110
147
  : "";
111
148
  await report(
112
- { type: "dms:start-error", message, code: error?.code, location },
149
+ {
150
+ type: "dms:start-error",
151
+ message,
152
+ code: isObjectLike(error) ? error.code : undefined,
153
+ location,
154
+ },
113
155
  `DMS development server failed to start: ${where}${message}`,
114
156
  console.error,
115
157
  );