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