@antelopejs/dms-frontend 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +377 -19
  2. package/dist/app-configs.d.ts +9 -0
  3. package/dist/app-configs.js +52 -0
  4. package/dist/asset-roots.d.ts +7 -0
  5. package/dist/asset-roots.js +34 -0
  6. package/dist/commands/build-action.js +9 -0
  7. package/dist/commands/dev-action.js +88 -19
  8. package/dist/commands/start-action.js +2 -0
  9. package/dist/common.d.ts +1 -0
  10. package/dist/common.js +1 -0
  11. package/dist/config.d.ts +2 -0
  12. package/dist/config.js +9 -3
  13. package/dist/derived-outputs.d.ts +2 -0
  14. package/dist/derived-outputs.js +38 -31
  15. package/dist/help.js +10 -3
  16. package/dist/layer-watch.js +123 -131
  17. package/dist/layers.js +2 -0
  18. package/dist/manifest-secrets.js +5 -5
  19. package/dist/manifest.js +7 -1
  20. package/dist/materialize.js +21 -1
  21. package/dist/module-set.d.ts +16 -0
  22. package/dist/module-set.js +80 -0
  23. package/dist/server-process.d.ts +1 -0
  24. package/dist/server-process.js +28 -2
  25. package/dist/verify-source-build.d.ts +11 -0
  26. package/dist/verify-source-build.js +113 -0
  27. package/dist/verify-source-runner.js +14 -57
  28. package/dist/workspace.d.ts +9 -0
  29. package/package.json +2 -1
  30. package/templates/vue/app-runtime.ts +58 -16
  31. package/templates/vue/client-server-guard.mjs +73 -0
  32. package/templates/vue/email-renderer.ts +17 -4
  33. package/templates/vue/email-runtime.ts +1 -1
  34. package/templates/vue/frontend-module.ts +1817 -259
  35. package/templates/vue/globals.d.ts +1 -1
  36. package/templates/vue/icon-hydration.ts +106 -1
  37. package/templates/vue/index.html +0 -1
  38. package/templates/vue/layer-app-config.ts +92 -0
  39. package/templates/vue/main.ts +10 -2
  40. package/templates/vue/package.json +5 -1
  41. package/templates/vue/prepare-types.mjs +34 -0
  42. package/templates/vue/server/auth/backend.mjs +13 -19
  43. package/templates/vue/server/auth/client-ip.mjs +27 -0
  44. package/templates/vue/server/auth/establish-endpoints.mjs +70 -0
  45. package/templates/vue/server/auth/session.mjs +6 -97
  46. package/templates/vue/server/auth-hooks.mjs +84 -0
  47. package/templates/vue/server/backend-proxies.mjs +110 -0
  48. package/templates/vue/server/dev-private-files.mjs +99 -0
  49. package/templates/vue/server/email.mjs +11 -33
  50. package/templates/vue/server/favicon.mjs +20 -0
  51. package/templates/vue/server/inertia.mjs +48 -3
  52. package/templates/vue/server/listen-host.mjs +19 -0
  53. package/templates/vue/server/module-event.mjs +265 -0
  54. package/templates/vue/server/module-routes.mjs +513 -0
  55. package/templates/vue/server/module-server.mjs +225 -0
  56. package/templates/vue/server/module-testing.mjs +65 -0
  57. package/templates/vue/server/page-props.mjs +151 -0
  58. package/templates/vue/server/path-patterns.mjs +38 -0
  59. package/templates/vue/server/proxy-headers.mjs +25 -0
  60. package/templates/vue/server/public-endpoints.mjs +69 -11
  61. package/templates/vue/server/request-routing.mjs +47 -0
  62. package/templates/vue/server/route-rules.mjs +129 -0
  63. package/templates/vue/server-module.ts +294 -0
  64. package/templates/vue/server.mjs +178 -169
  65. package/templates/vue/ssr-renderer.ts +60 -19
  66. package/templates/vue/tsconfig.json +2 -1
  67. package/templates/vue/ui-app-config.ts +18 -6
  68. package/templates/vue/ui-inertia-page.mjs +37 -0
  69. package/templates/vue/vite.config.ts +31 -10
  70. package/templates/vue/vite.email.config.ts +1 -1
  71. package/templates/vue/vite.server.config.ts +57 -0
  72. package/templates/vue/server/auth/oauth.mjs +0 -233
  73. package/templates/vue/server/auth/routes.mjs +0 -444
  74. package/templates/vue/server/render-token.mjs +0 -81
  75. package/templates/vue/server/tester.mjs +0 -227
@@ -0,0 +1,265 @@
1
+ // What a module route handler receives and how its answer is written: the
2
+ // Web-standard request, the session, the module's sealed cookies, the backend,
3
+ // and the defaults every route gets once (same-origin on unsafe methods, a body
4
+ // limit, `no-store`). See `server-module.ts` for the types a module reads.
5
+
6
+ import { Readable } from "node:stream";
7
+ import { pipeline } from "node:stream/promises";
8
+ import { backend, json, RequestBodyError } from "./auth/backend.mjs";
9
+ import {
10
+ clientIp,
11
+ CROSS_ORIGIN_ERROR,
12
+ isSameOrigin,
13
+ requestScheme,
14
+ } from "./auth/client-ip.mjs";
15
+ import {
16
+ clearSealedCookie,
17
+ clearSession,
18
+ readSealedCookie,
19
+ readSession,
20
+ SESSION_COOKIE,
21
+ setRequestSession,
22
+ writeSealedCookie,
23
+ writeSession,
24
+ } from "./auth/session.mjs";
25
+ import { renderEmailTemplate } from "./email.mjs";
26
+ import { backendUrl } from "./request-target.mjs";
27
+
28
+ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
29
+ const COOKIE_NAME = /^[A-Za-z0-9_-]{1,64}$/;
30
+ /** The sealed cookies of the module that owns sign-in: session, accounts, flows. */
31
+ const RESERVED_COOKIE_PREFIX = "dms_";
32
+ const BODY_TOO_LARGE = "Request body too large";
33
+
34
+ function requestOrigin(request) {
35
+ const host = request.headers.host;
36
+ try {
37
+ return new URL(`${requestScheme(request)}://${host}`).origin;
38
+ } catch {
39
+ return "http://frontend.local";
40
+ }
41
+ }
42
+
43
+ function requestHeaders(request) {
44
+ const headers = new Headers();
45
+ for (const [name, value] of Object.entries(request.headers)) {
46
+ if (name.startsWith(":") || value === undefined) continue;
47
+ for (const entry of [value].flat()) headers.append(name, entry);
48
+ }
49
+ return headers;
50
+ }
51
+
52
+ /** The request body as a Web stream that errors past `limit` bytes. */
53
+ function limitedBody(request, limit) {
54
+ let size = 0;
55
+ return Readable.toWeb(request).pipeThrough(
56
+ new TransformStream({
57
+ transform(chunk, controller) {
58
+ size += chunk.byteLength;
59
+ if (size > limit)
60
+ controller.error(new RequestBodyError(413, BODY_TOO_LARGE));
61
+ else controller.enqueue(chunk);
62
+ },
63
+ }),
64
+ );
65
+ }
66
+
67
+ function webRequest(request, route) {
68
+ const url = new URL(request.url, requestOrigin(request));
69
+ const hasBody = !["GET", "HEAD"].includes(request.method);
70
+ const declaredLength = Number(request.headers["content-length"] ?? 0);
71
+ if (hasBody && declaredLength > route.bodyLimit)
72
+ throw new RequestBodyError(413, BODY_TOO_LARGE);
73
+ return new Request(url, {
74
+ method: request.method,
75
+ headers: requestHeaders(request),
76
+ body: hasBody ? limitedBody(request, route.bodyLimit) : undefined,
77
+ duplex: "half",
78
+ });
79
+ }
80
+
81
+ async function readJson(webRequestValue) {
82
+ if (!webRequestValue.body) return {};
83
+ const chunks = [];
84
+ const reader = webRequestValue.body.getReader();
85
+ for (;;) {
86
+ const { done, value } = await reader.read();
87
+ if (done) break;
88
+ chunks.push(value);
89
+ }
90
+ if (!chunks.length) return {};
91
+ try {
92
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
93
+ } catch {
94
+ throw new RequestBodyError(400, "Invalid request body");
95
+ }
96
+ }
97
+
98
+ function checkedCookieName(name) {
99
+ if (typeof name !== "string" || !COOKIE_NAME.test(name))
100
+ throw new Error(`Invalid cookie name: ${String(name)}`);
101
+ if (name === SESSION_COOKIE)
102
+ throw new Error(
103
+ `The ${SESSION_COOKIE} cookie is the session's: use event.session`,
104
+ );
105
+ return name;
106
+ }
107
+
108
+ function sessionAccess(request, response, view) {
109
+ return {
110
+ read: () => readSession(request),
111
+ view: () => view?.(readSession(request)) ?? {},
112
+ write(session) {
113
+ session.updatedAt = Date.now();
114
+ writeSession(response, session);
115
+ },
116
+ clear: () => clearSession(response),
117
+ useForRequest: (session) => setRequestSession(request, session),
118
+ };
119
+ }
120
+
121
+ /**
122
+ * The name a module's sealed cookie takes in the jar. The module that owns
123
+ * sign-in (`registerAuth`) names its own, the `dms_*` ones included; any other
124
+ * module's are prefixed with its id and a dot, which no name of the owner's
125
+ * holds, so no module reads or overwrites another's, nor the owner's session
126
+ * and account cookies.
127
+ */
128
+ function moduleCookieName(name, moduleId, authOwner) {
129
+ const checked = checkedCookieName(name);
130
+ if (moduleId === authOwner) return checked;
131
+ if (checked.startsWith(RESERVED_COOKIE_PREFIX))
132
+ throw new Error(
133
+ `The ${RESERVED_COOKIE_PREFIX}* cookies belong to the module that owns sign-in` +
134
+ `${authOwner ? ` (${authOwner})` : ""}: choose another name than ${name}`,
135
+ );
136
+ return `${String(moduleId).replaceAll(/[^A-Za-z0-9_-]/g, "-")}.${checked}`;
137
+ }
138
+
139
+ function cookieAccess(request, response, moduleId, authOwner) {
140
+ const named = (name) => moduleCookieName(name, moduleId, authOwner);
141
+ return {
142
+ readSealed: (name) => readSealedCookie(request, named(name)),
143
+ writeSealed: (name, value, options = {}) =>
144
+ writeSealedCookie(
145
+ response,
146
+ named(name),
147
+ value,
148
+ options.maxAge,
149
+ options.path ?? "/",
150
+ ),
151
+ clear: (name, options = {}) =>
152
+ clearSealedCookie(response, named(name), options.path ?? "/"),
153
+ };
154
+ }
155
+
156
+ function frozenCopy(value) {
157
+ return Object.freeze(structuredClone(value ?? {}));
158
+ }
159
+
160
+ /**
161
+ * The event a handler receives.
162
+ *
163
+ * @param context `{ request, response, route, params, module, app, view,
164
+ * authOwner }`, where `module` is `{ id, options, privateOptions }`, `app`
165
+ * the deployment's read-only data, `view` what a session shows a page and
166
+ * `authOwner` the id of the module that owns sign-in
167
+ */
168
+ export function createServerEvent(context) {
169
+ const { request, response, route, params } = context;
170
+ const event = {
171
+ request: webRequest(request, route),
172
+ params: Object.freeze({ ...params }),
173
+ session: sessionAccess(request, response, context.view),
174
+ cookies: cookieAccess(
175
+ request,
176
+ response,
177
+ context.module.id,
178
+ context.authOwner,
179
+ ),
180
+ backend: (path, init = {}) => backend(path, request, init),
181
+ backendUrl: (path) => backendUrl(path),
182
+ clientIp: () => clientIp(request),
183
+ module: Object.freeze({
184
+ id: context.module.id,
185
+ options: frozenCopy(context.module.options),
186
+ privateOptions: frozenCopy(context.module.privateOptions),
187
+ }),
188
+ renderEmail: (template, props, options = {}) =>
189
+ renderEmailTemplate(template, props, options),
190
+ app: Object.freeze({
191
+ authEstablishEndpoints: Object.freeze([
192
+ ...(context.app?.authEstablishEndpoints ?? []),
193
+ ]),
194
+ }),
195
+ };
196
+ event.url = new URL(event.request.url);
197
+ event.readJson = () => readJson(event.request);
198
+ return event;
199
+ }
200
+
201
+ function isResponse(value) {
202
+ return (
203
+ value instanceof Response ||
204
+ (typeof value === "object" &&
205
+ value !== null &&
206
+ typeof value.status === "number" &&
207
+ typeof value.headers?.get === "function" &&
208
+ "body" in value)
209
+ );
210
+ }
211
+
212
+ function asResponse(result) {
213
+ if (isResponse(result)) return result;
214
+ if (result === undefined || result === null)
215
+ return new Response(null, { status: 204 });
216
+ return Response.json(result);
217
+ }
218
+
219
+ /**
220
+ * Writes a handler's answer. The cookies the event wrote are already on the
221
+ * Node response; those of a returned `Response` are added to them.
222
+ */
223
+ async function writeServerResponse(request, response, result) {
224
+ const answer = asResponse(result);
225
+ const cookies = [response.getHeader("set-cookie") ?? []]
226
+ .flat()
227
+ .concat(answer.headers.getSetCookie());
228
+ answer.headers.forEach((value, name) => {
229
+ if (name !== "set-cookie") response.setHeader(name, value);
230
+ });
231
+ if (cookies.length) response.setHeader("set-cookie", cookies);
232
+ if (!answer.headers.has("cache-control"))
233
+ response.setHeader("cache-control", "no-store");
234
+ response.writeHead(answer.status, answer.statusText || undefined);
235
+ if (!answer.body || request.method === "HEAD") {
236
+ await answer.body?.cancel();
237
+ response.end();
238
+ return;
239
+ }
240
+ await pipeline(Readable.fromWeb(answer.body), response);
241
+ }
242
+
243
+ /**
244
+ * Answers a request with the module route it reached. The route's defaults
245
+ * apply first: a request with a state-changing method needs the frontend's
246
+ * own `Origin` unless the route says otherwise. An error the handler lets
247
+ * through reaches the server's error handling with the cookies written so
248
+ * far.
249
+ *
250
+ * @param context See `createServerEvent`
251
+ */
252
+ export async function answerModuleRoute(context) {
253
+ const { request, response, route } = context;
254
+ if (
255
+ route.sameOrigin &&
256
+ !SAFE_METHODS.has(request.method) &&
257
+ !isSameOrigin(request)
258
+ ) {
259
+ json(response, 403, CROSS_ORIGIN_ERROR);
260
+ return;
261
+ }
262
+ const event = createServerEvent(context);
263
+ const result = await route.handler(event);
264
+ await writeServerResponse(request, response, result);
265
+ }