@antelopejs/dms-frontend 0.1.7 → 0.1.8

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 CHANGED
@@ -190,18 +190,38 @@ never sends a token and never receives one: it gets back the same
190
190
  tenant-assignment outcomes are handled identically.
191
191
 
192
192
  Because the route turns a backend endpoint into a login, it only calls the
193
- endpoints the deployment names:
193
+ endpoints that were declared for it.
194
+
195
+ Backend modules declare their own. A module registering its frontend names the
196
+ routes that finish its flow, the DMS aggregates them into the frontend manifest
197
+ it serves with the bootstrap secret, and `dev`/`build` write them into the
198
+ workspace as it is materialized:
199
+
200
+ ```ts
201
+ // in the backend module
202
+ await AddFrontendModule({
203
+ name: "@antelopejs/dms-saas-frontend-vue",
204
+ sourcePath: path.join(__dirname, "../frontend-vue"),
205
+ renderer: { name: "vue", version: "3" },
206
+ authEstablishEndpoints: ["/api/saas/register/finalize"],
207
+ });
208
+ ```
209
+
210
+ A standard deployment therefore needs no configuration at all. The environment
211
+ variable stays as an override, for a backend route no module declares — or one
212
+ declared by a DMS too old to carry the field:
194
213
 
195
214
  ```bash
196
215
  # .env
197
- DMS_AUTH_ESTABLISH_ENDPOINTS=/api/saas/register/finalize,/api/invites/redeem
216
+ DMS_AUTH_ESTABLISH_ENDPOINTS=/api/invites/redeem
198
217
  ```
199
218
 
200
- The list is empty by default and matched verbatim against absolute `/api/…`
201
- paths no prefixes, no query strings, no traversal so no backend route that
202
- happens to mint a token pair can be turned into a login by a request from the
203
- browser. An undeclared endpoint is answered `403` and never called. The route
204
- is `POST`-only and same-origin, like every other auth action.
219
+ The effective allow-list is the union of the two. Every entry, from either
220
+ source, is matched verbatim against absolute `/api/…` paths — no prefixes, no
221
+ query strings, no traversal so no backend route that happens to mint a token
222
+ pair can be turned into a login by a request from the browser. An undeclared
223
+ endpoint is answered `403` and never called. The route is `POST`-only and
224
+ same-origin, like every other auth action.
205
225
 
206
226
  ### Workspaces
207
227
 
@@ -246,7 +266,7 @@ frontend-module registry drives server and client entries.
246
266
  | | `DMS_COOKIE_SECURE` | Secure cookies (`true` by default; `ajs dms dev` defaults to `false`) |
247
267
  | | `DMS_TRUSTED_PROXY_HOPS` | Number of trusted, rightmost reverse-proxy hops (default `0`) |
248
268
  | | `DMS_SESSION_SECRET` | Session cookie encryption key, 32 characters or more (required for login) |
249
- | | `DMS_AUTH_ESTABLISH_ENDPOINTS` | Backend endpoints `/auth/establish` may open a session from (comma-separated, empty by default) |
269
+ | | `DMS_AUTH_ESTABLISH_ENDPOINTS` | Extra backend endpoints `/auth/establish` may open a session from, on top of those the backend's modules declare (comma-separated, empty by default) |
250
270
  | | `DMS_CLIENT_BASE_URL` | Public frontend URL used in generated links and emails |
251
271
 
252
272
  All of these can be set in the project's `.env` instead of the environment; see [Configuration](#configuration).
package/dist/config.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Options = exports.TAILWIND_SOURCE_GLOB = exports.PNPM_LIFECYCLE_SCRIPTS = exports.layerCopyIgnore = exports.LAYER_COPY_BLOCKLIST = exports.FRONTEND_MODULE_ENTRY = exports.LAYERS_SUBDIR = exports.TEMPLATE_FILES = exports.WORKSPACE_DIR_MODE = exports.DEPS_HASH_FILE = exports.DMS_FRONTEND_HOME = void 0;
6
+ exports.Options = exports.TAILWIND_SOURCE_GLOB = exports.PNPM_LIFECYCLE_SCRIPTS = exports.layerCopyIgnore = exports.LAYER_COPY_BLOCKLIST = exports.AUTH_ESTABLISH_FILE = exports.FRONTEND_MODULE_ENTRY = exports.LAYERS_SUBDIR = exports.TEMPLATE_FILES = exports.WORKSPACE_DIR_MODE = exports.DEPS_HASH_FILE = exports.DMS_FRONTEND_HOME = void 0;
7
7
  exports.writeSecretBearingFile = writeSecretBearingFile;
8
8
  exports.normalizeBootstrapSecret = normalizeBootstrapSecret;
9
9
  exports.resolveBootstrapSecret = resolveBootstrapSecret;
@@ -52,6 +52,7 @@ exports.TEMPLATE_FILES = [
52
52
  ];
53
53
  exports.LAYERS_SUBDIR = "frontend-modules";
54
54
  exports.FRONTEND_MODULE_ENTRY = "dms.frontend.ts";
55
+ exports.AUTH_ESTABLISH_FILE = "generated-auth-establish.json";
55
56
  exports.LAYER_COPY_BLOCKLIST = [
56
57
  "node_modules",
57
58
  "dist",
package/dist/layers.js CHANGED
@@ -59,6 +59,7 @@ function resolveLayer(layerPath, mod) {
59
59
  priority: mod.priority,
60
60
  configKey: mod.configKey,
61
61
  options: mod.options,
62
+ authEstablishEndpoints: mod.authEstablishEndpoints,
62
63
  };
63
64
  }
64
65
  function buildLayersFromPaths(modules) {
@@ -5,6 +5,7 @@ exports.copyStaticTemplates = copyStaticTemplates;
5
5
  exports.writeWorkspacePackageJson = writeWorkspacePackageJson;
6
6
  exports.materializeLayers = materializeLayers;
7
7
  exports.createFrontendModuleRegistry = createFrontendModuleRegistry;
8
+ exports.collectAuthEstablishEndpoints = collectAuthEstablishEndpoints;
8
9
  exports.writeFrontendModuleRegistry = writeFrontendModuleRegistry;
9
10
  exports.writeDmsMainCss = writeDmsMainCss;
10
11
  const node_fs_1 = require("node:fs");
@@ -107,9 +108,30 @@ function createFrontendModuleRegistry(workspaceDir, layers) {
107
108
  validateFrontendModuleRegistry(registry, workspaceDir);
108
109
  return registry;
109
110
  }
111
+ const BACKEND_API_PATH = /^\/api\/[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9][A-Za-z0-9._~-]*)*$/;
112
+ function collectAuthEstablishEndpoints(layers) {
113
+ const endpoints = new Set();
114
+ for (const layer of layers) {
115
+ for (const endpoint of layer.authEstablishEndpoints ?? []) {
116
+ if (typeof endpoint === "string" && BACKEND_API_PATH.test(endpoint)) {
117
+ endpoints.add(endpoint);
118
+ continue;
119
+ }
120
+ console.warn(`⚠ Ignoring malformed authEstablishEndpoints entry ${JSON.stringify(endpoint)} ` +
121
+ `declared by ${layer.packageName ?? layer.path}: expected an absolute ` +
122
+ "backend API path under /api/ with no query string.");
123
+ }
124
+ }
125
+ return [...endpoints];
126
+ }
127
+ function writeAuthEstablishEndpoints(workspaceDir, layers) {
128
+ const endpoints = collectAuthEstablishEndpoints(layers);
129
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspaceDir, config_1.AUTH_ESTABLISH_FILE), `${JSON.stringify({ endpoints }, null, 2)}\n`);
130
+ }
110
131
  function writeFrontendModuleRegistry(workspaceDir, layers) {
111
132
  const registry = createFrontendModuleRegistry(workspaceDir, layers);
112
133
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspaceDir, "generated-frontend-modules.json"), `${JSON.stringify(registry, null, 2)}\n`);
134
+ writeAuthEstablishEndpoints(workspaceDir, layers);
113
135
  writeFrontendTypePaths(workspaceDir, registry);
114
136
  writeFrontendModuleLoader(workspaceDir, registry);
115
137
  writeLocaleMessages(workspaceDir, registry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
2
3
  import { backend, body, json, UpstreamError } from "./backend.mjs";
3
4
  import { CROSS_ORIGIN_ERROR, isSameOrigin } from "./client-ip.mjs";
4
5
  import {
@@ -28,6 +29,36 @@ const PASSTHROUGH = new Map([
28
29
  const BACKEND_API_PATH =
29
30
  /^\/api\/[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9][A-Za-z0-9._~-]*)*$/;
30
31
 
32
+ // Written at the workspace root by the loader, from the endpoints each backend
33
+ // module declared in the frontend manifest. This file sits two directories up.
34
+ const DECLARED_ENDPOINTS_FILE = new URL(
35
+ "../../generated-auth-establish.json",
36
+ import.meta.url,
37
+ );
38
+
39
+ let declaredEndpoints;
40
+
41
+ /**
42
+ * Backend endpoints the modules of this deployment declared as session-opening.
43
+ *
44
+ * Read from the workspace rather than from the backend: the file is generated
45
+ * when the workspace is materialized, so the server needs no backend round trip
46
+ * — and no operator — to know which module flows may end on a login. A
47
+ * workspace built from a DMS that predates the declaration simply has no file,
48
+ * and the list falls back to whatever the environment names.
49
+ *
50
+ * @param file Generated file to read
51
+ * @returns The declared paths, unfiltered
52
+ */
53
+ export function readDeclaredEstablishEndpoints(file = DECLARED_ENDPOINTS_FILE) {
54
+ try {
55
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
56
+ return Array.isArray(parsed?.endpoints) ? parsed.endpoints : [];
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+
31
62
  export function publicSession(session) {
32
63
  if (!session) return {};
33
64
  return {
@@ -88,20 +119,30 @@ async function establish(request, response, endpoint) {
88
119
  /**
89
120
  * Backend endpoints this deployment lets a module open a session from.
90
121
  *
91
- * Empty by default: a module route only becomes a session-opening route once
92
- * the operator names it, so no backend endpoint that happens to mint a token
93
- * pair can be turned into a login by a request from the browser.
122
+ * The union of what the backend's modules declared the standard case, which
123
+ * needs no configuration and what the environment adds on top, which is how
124
+ * an operator extends the list for a route no module owns. Everything else
125
+ * stays refused, so no backend endpoint that happens to mint a token pair can
126
+ * be turned into a login by a request from the browser.
94
127
  *
95
- * @param declaration Comma-separated absolute backend paths
96
- * @returns The declared paths that are well-formed backend API paths
128
+ * @param declaration Comma-separated absolute backend paths from the environment
129
+ * @param declared Paths declared by the backend's frontend modules
130
+ * @returns The well-formed backend API paths, without duplicates
97
131
  */
98
132
  export function allowedEstablishEndpoints(
99
133
  declaration = process.env.DMS_AUTH_ESTABLISH_ENDPOINTS,
134
+ declared = (declaredEndpoints ??= readDeclaredEstablishEndpoints()),
100
135
  ) {
101
- return (declaration ?? "")
136
+ const fromEnvironment = (declaration ?? "")
102
137
  .split(",")
103
- .map((entry) => entry.trim())
104
- .filter((entry) => BACKEND_API_PATH.test(entry));
138
+ .map((entry) => entry.trim());
139
+ return [
140
+ ...new Set(
141
+ [...declared, ...fromEnvironment].filter(
142
+ (entry) => typeof entry === "string" && BACKEND_API_PATH.test(entry),
143
+ ),
144
+ ),
145
+ ];
105
146
  }
106
147
 
107
148
  /**