@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.
- package/README.md +18 -24
- package/dist/commands/build-action.js +3 -2
- package/dist/commands/dev-action.js +2 -1
- package/dist/commands/prepare-action.js +1 -1
- package/dist/commands/start-action.js +3 -24
- package/dist/commands/workspace-task.js +2 -1
- package/dist/config.js +3 -0
- package/dist/derived-outputs.js +3 -3
- package/dist/discovery.js +17 -6
- package/dist/entry-weight.js +15 -0
- package/dist/fs-sync.js +7 -4
- package/dist/index.js +2 -1
- package/dist/internal/built-workspace.d.ts +4 -0
- package/dist/internal/built-workspace.js +43 -0
- package/dist/internal/ensure.d.ts +1 -0
- package/dist/internal/ensure.js +8 -0
- package/dist/internal/locale-catalogs.d.ts +8 -3
- package/dist/internal/locale-catalogs.js +16 -6
- package/dist/internal/manifest-decoder.d.ts +7 -0
- package/dist/internal/manifest-decoder.js +81 -0
- package/dist/internal/own-release.d.ts +5 -0
- package/dist/internal/own-release.js +20 -0
- package/dist/internal/plain-object.d.ts +1 -0
- package/dist/internal/plain-object.js +6 -0
- package/dist/internal/text-list.d.ts +1 -0
- package/dist/internal/text-list.js +10 -0
- package/dist/internal/vite-manifest.d.ts +6 -0
- package/dist/internal/vite-manifest.js +24 -0
- package/dist/layer-watch.js +25 -24
- package/dist/layers.d.ts +1 -1
- package/dist/layers.js +3 -1
- package/dist/locale-files.d.ts +2 -1
- package/dist/locale-files.js +4 -0
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +44 -31
- package/dist/materialize.js +6 -2
- package/dist/module-set.js +2 -1
- package/dist/node-options.d.ts +1 -0
- package/dist/node-options.js +8 -0
- package/dist/output.d.ts +15 -9
- package/dist/output.js +30 -18
- package/dist/private-values.d.ts +1 -1
- package/dist/private-values.js +5 -3
- package/dist/project-modules.js +13 -8
- package/dist/renderer-range.js +5 -3
- package/dist/server-process.js +1 -1
- package/dist/verify-source-build.js +10 -2
- package/dist/verify-source-client-bundle.d.ts +1 -0
- package/dist/verify-source-client-bundle.js +30 -0
- package/dist/verify-source-runner.js +44 -60
- package/dist/verify-source-sources.js +49 -11
- package/dist/workspace-setup.js +1 -1
- package/package.json +6 -4
- package/skills/dms-frontend/SKILL.md +1 -1
- package/templates/vue/app-runtime.ts +90 -16
- package/templates/vue/client-server-guard.mjs +16 -0
- package/templates/vue/compress-assets.mjs +9 -1
- package/templates/vue/entry-weight.mjs +22 -0
- package/templates/vue/frontend-module.ts +1 -0
- package/templates/vue/frontend-runtime/module-registration.ts +2 -2
- package/templates/vue/frontend-runtime/state.ts +2 -2
- package/templates/vue/frontend-runtime/types.ts +8 -2
- package/templates/vue/globals.d.ts +5 -0
- package/templates/vue/head-order.mjs +7 -0
- package/templates/vue/icon-bundle.ts +31 -0
- package/templates/vue/icon-scan.mjs +4 -0
- package/templates/vue/message-subset.ts +65 -0
- package/templates/vue/network-error.ts +4 -0
- package/templates/vue/server/auth/backend.mjs +41 -4
- package/templates/vue/server/auth/client-ip.mjs +52 -4
- package/templates/vue/server/auth/establish-endpoints.mjs +10 -6
- package/templates/vue/server/auth/session.mjs +81 -2
- package/templates/vue/server/auth-hooks.mjs +15 -5
- package/templates/vue/server/backend-proxies.mjs +65 -9
- package/templates/vue/server/backend-request.mjs +61 -0
- package/templates/vue/server/backend-response.mjs +23 -6
- package/templates/vue/server/built-files.mjs +7 -0
- package/templates/vue/server/cli-report.mjs +53 -11
- package/templates/vue/server/client-manifest.mjs +23 -1
- package/templates/vue/server/content.mjs +18 -1
- package/templates/vue/server/dev-private-files.mjs +17 -1
- package/templates/vue/server/dev-styles.mjs +38 -0
- package/templates/vue/server/email.mjs +9 -3
- package/templates/vue/server/favicon.mjs +3 -0
- package/templates/vue/server/generated-files.mjs +211 -0
- package/templates/vue/server/homepage.mjs +21 -9
- package/templates/vue/server/html-tag.mjs +4 -0
- package/templates/vue/server/icons.mjs +80 -7
- package/templates/vue/server/in-place-render.mjs +35 -0
- package/templates/vue/server/inertia.mjs +61 -2
- package/templates/vue/server/module-event.mjs +98 -17
- package/templates/vue/server/module-route-types.mjs +78 -0
- package/templates/vue/server/module-routes.mjs +138 -31
- package/templates/vue/server/module-server.mjs +98 -27
- package/templates/vue/server/module-testing.mjs +14 -5
- package/templates/vue/server/page-props.mjs +92 -2
- package/templates/vue/server/page-redirects.mjs +37 -0
- package/templates/vue/server/path-patterns.mjs +13 -1
- package/templates/vue/server/proxy-compression.mjs +22 -0
- package/templates/vue/server/proxy-headers.mjs +11 -1
- package/templates/vue/server/proxy-redirects.mjs +20 -5
- package/templates/vue/server/proxy-request.mjs +57 -0
- package/templates/vue/server/proxy-transfers.mjs +33 -5
- package/templates/vue/server/public-endpoints.mjs +138 -23
- package/templates/vue/server/public-options.mjs +20 -6
- package/templates/vue/server/request-failure.mjs +23 -0
- package/templates/vue/server/request-routing.mjs +26 -9
- package/templates/vue/server/request-target.mjs +9 -3
- package/templates/vue/server/route-rules.mjs +39 -15
- package/templates/vue/server/ssr-module.mjs +32 -0
- package/templates/vue/server/static-files.mjs +24 -1
- package/templates/vue/server/type-guards.mjs +71 -0
- package/templates/vue/server/vite-errors.mjs +32 -5
- package/templates/vue/server/web-body.mjs +3 -0
- package/templates/vue/server-module.ts +10 -1
- package/templates/vue/server.mjs +132 -148
- package/templates/vue/sfc-global-types.mjs +112 -13
- package/templates/vue/ssr-renderer.ts +12 -3
- package/templates/vue/ui-inertia-page.mjs +16 -1
- package/templates/vue/ui-kbd.mjs +2 -1
- package/templates/vue/ui-lazy-icons.mjs +44 -0
- package/templates/vue/ui-request-state.mjs +2 -1
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
? (
|
|
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
|
-
/**
|
|
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 && (
|
|
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
|
-
/**
|
|
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 =
|
|
48
|
+
value = isString(data) ? data : data?.message;
|
|
39
49
|
} catch {}
|
|
40
|
-
return
|
|
41
|
-
? value
|
|
42
|
-
: undefined;
|
|
50
|
+
return isString(value) && REFUSAL_CODE.test(value) ? value : undefined;
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
/**
|
|
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
|
-
* @
|
|
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
|
-
/**
|
|
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
|
|
58
|
-
|
|
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 =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
{
|
|
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
|
);
|