@antelopejs/dms-frontend 0.1.7 → 0.1.9

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
@@ -10,7 +10,9 @@ Frontend-agnostic loader for AntelopeJS DMS. The backend serves a frontend
10
10
  manifest and the matching frontend-module archives; the `ajs dms` CLI
11
11
  materializes them into a generated workspace for one renderer, builds it, and
12
12
  runs its Node frontend server. The `vue` renderer — Vue 3, Vite, Inertia, and
13
- SSR — is the one shipped today.
13
+ SSR — is the one shipped today. The package itself installs a single executable,
14
+ `ajs-dms`; `ajs dms` is the core CLI delegating to it, and is the name every
15
+ project, script and document uses.
14
16
 
15
17
  ## Renderers
16
18
 
@@ -55,15 +57,23 @@ not infer application ownership from other framework configuration.
55
57
 
56
58
  ## Install
57
59
 
58
- The loader is an AntelopeJS CLI plugin: install it next to `@antelopejs/core`
59
- and `ajs` delegates its `dms` command to the `ajs-dms` executable.
60
+ The loader is an official AntelopeJS CLI plugin: install it next to
61
+ `@antelopejs/core` and run it as `ajs dms <command>`. That is the only supported
62
+ way to invoke it — in a shell, in a package script, in CI and in a container
63
+ alike.
60
64
 
61
65
  ```bash
66
+ # in a project (the usual case: both are already project dependencies)
67
+ pnpm add @antelopejs/core @antelopejs/dms-frontend
68
+
69
+ # or globally
62
70
  pnpm add -g @antelopejs/core @antelopejs/dms-frontend
63
71
  ```
64
72
 
65
73
  `npm install -g` works too; this repository and every generated workspace use
66
- pnpm.
74
+ pnpm. Inside a package script, `ajs` resolves from `node_modules/.bin`, and the
75
+ `dms` command it delegates to resolves the project-local plugin, so a script
76
+ never depends on a global install.
67
77
 
68
78
  ## Commands
69
79
 
@@ -76,9 +86,6 @@ ajs dms clean -b https://dms.example.com
76
86
  ajs dms clean --all
77
87
  ```
78
88
 
79
- `ajs dms <command>` and `ajs-dms <command>` are the same program; the delegation
80
- only saves you from remembering a second executable name. Package scripts should
81
- call `ajs-dms` directly so they do not depend on the CLI being installed.
82
89
  `--help`, `--version` and `clean` run from any directory. `prepare` also runs
83
90
  anywhere: with no backend in reach it warns and exits 0, so a frontend module's
84
91
  `postinstall` hook never fails an install, and the generated types are refreshed
@@ -190,18 +197,38 @@ never sends a token and never receives one: it gets back the same
190
197
  tenant-assignment outcomes are handled identically.
191
198
 
192
199
  Because the route turns a backend endpoint into a login, it only calls the
193
- endpoints the deployment names:
200
+ endpoints that were declared for it.
201
+
202
+ Backend modules declare their own. A module registering its frontend names the
203
+ routes that finish its flow, the DMS aggregates them into the frontend manifest
204
+ it serves with the bootstrap secret, and `dev`/`build` write them into the
205
+ workspace as it is materialized:
206
+
207
+ ```ts
208
+ // in the backend module
209
+ await AddFrontendModule({
210
+ name: "@antelopejs/dms-saas-frontend-vue",
211
+ sourcePath: path.join(__dirname, "../frontend-vue"),
212
+ renderer: { name: "vue", version: "3" },
213
+ authEstablishEndpoints: ["/api/saas/register/finalize"],
214
+ });
215
+ ```
216
+
217
+ A standard deployment therefore needs no configuration at all. The environment
218
+ variable stays as an override, for a backend route no module declares — or one
219
+ declared by a DMS too old to carry the field:
194
220
 
195
221
  ```bash
196
222
  # .env
197
- DMS_AUTH_ESTABLISH_ENDPOINTS=/api/saas/register/finalize,/api/invites/redeem
223
+ DMS_AUTH_ESTABLISH_ENDPOINTS=/api/invites/redeem
198
224
  ```
199
225
 
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.
226
+ The effective allow-list is the union of the two. Every entry, from either
227
+ source, is matched verbatim against absolute `/api/…` paths — no prefixes, no
228
+ query strings, no traversal so no backend route that happens to mint a token
229
+ pair can be turned into a login by a request from the browser. An undeclared
230
+ endpoint is answered `403` and never called. The route is `POST`-only and
231
+ same-origin, like every other auth action.
205
232
 
206
233
  ### Workspaces
207
234
 
@@ -246,7 +273,7 @@ frontend-module registry drives server and client entries.
246
273
  | | `DMS_COOKIE_SECURE` | Secure cookies (`true` by default; `ajs dms dev` defaults to `false`) |
247
274
  | | `DMS_TRUSTED_PROXY_HOPS` | Number of trusted, rightmost reverse-proxy hops (default `0`) |
248
275
  | | `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) |
276
+ | | `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
277
  | | `DMS_CLIENT_BASE_URL` | Public frontend URL used in generated links and emails |
251
278
 
252
279
  All of these can be set in the project's `.env` instead of the environment; see [Configuration](#configuration).
@@ -51,7 +51,7 @@ function cmdBuild() {
51
51
  if (code === 0) {
52
52
  console.log("");
53
53
  (0, cli_ui_1.success)("Build completed successfully!");
54
- console.log(chalk_1.default.dim(" Run 'ajs-dms start' to start the production server"));
54
+ console.log(chalk_1.default.dim(" Run 'ajs dms start' to start the production server"));
55
55
  }
56
56
  else {
57
57
  (0, cli_ui_1.error)("Build failed");
@@ -26,7 +26,7 @@ function cmdStart() {
26
26
  if (!(0, node_fs_1.existsSync)(serverPath) || !(0, node_fs_1.existsSync)(clientPath)) {
27
27
  console.log("");
28
28
  (0, cli_ui_1.error)("Production build not found!");
29
- console.log(chalk_1.default.dim(" Run 'ajs-dms build -b " +
29
+ console.log(chalk_1.default.dim(" Run 'ajs dms build -b " +
30
30
  options.backendUrl +
31
31
  "' first to create a production build"));
32
32
  process.exit(1);
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/index.js CHANGED
@@ -25,7 +25,7 @@ const runCLI = async () => {
25
25
  console.log(chalk_1.default.dim(` Frontend Loader for AntelopeJS DMS - v${version}\n`));
26
26
  }
27
27
  const program = new commander_1.Command()
28
- .name("ajs-dms")
28
+ .name("ajs dms")
29
29
  .description(`Antelope DMS - Frontend Loader v${version}\n\n` +
30
30
  `Materializes frontend modules from an AntelopeJS backend and starts a Vue or React Vite and Inertia application.`)
31
31
  .version(version, "-v, --version", "Display version number")
package/dist/layers.js CHANGED
@@ -23,8 +23,8 @@ function assertLayerPathsServed(modules) {
23
23
  return;
24
24
  throw new Error("The backend served a manifest without layer source paths:\n" +
25
25
  pathless.map((mod) => ` - ${mod.name}`).join("\n") +
26
- "\n`ajs-dms dev` needs a development backend running on this machine (started with " +
27
- "`ajs project dev`); use `ajs-dms build` against a remote or production one.\n" +
26
+ "\n`ajs dms dev` needs a development backend running on this machine (started with " +
27
+ "`ajs project dev`); use `ajs dms build` against a remote or production one.\n" +
28
28
  "If the backend is local and in development mode, it did not recognize the bootstrap " +
29
29
  "credential — see DMS_BOOTSTRAP_SECRET.");
30
30
  }
@@ -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.9",
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
  /**