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