@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 +42 -15
- package/dist/commands/build.js +1 -1
- package/dist/commands/start.js +1 -1
- package/dist/config.js +2 -1
- package/dist/index.js +1 -1
- package/dist/layers.js +3 -2
- package/dist/materialize.js +22 -0
- package/package.json +1 -1
- package/templates/vue/server/auth/routes.mjs +49 -8
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
|
|
59
|
-
and
|
|
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
|
|
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/
|
|
223
|
+
DMS_AUTH_ESTABLISH_ENDPOINTS=/api/invites/redeem
|
|
198
224
|
```
|
|
199
225
|
|
|
200
|
-
The list is
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
is `
|
|
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` |
|
|
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).
|
package/dist/commands/build.js
CHANGED
|
@@ -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
|
|
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");
|
package/dist/commands/start.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
27
|
-
"`ajs project dev`); use `ajs
|
|
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) {
|
package/dist/materialize.js
CHANGED
|
@@ -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,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
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
136
|
+
const fromEnvironment = (declaration ?? "")
|
|
102
137
|
.split(",")
|
|
103
|
-
.map((entry) => entry.trim())
|
|
104
|
-
|
|
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
|
/**
|