@micacms/editor 0.2.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.
- package/README.md +89 -0
- package/dist/admin-dir.d.ts +3 -0
- package/dist/admin-dir.d.ts.map +1 -0
- package/dist/admin-dir.js +15 -0
- package/dist/admin-dir.js.map +1 -0
- package/dist/banner.d.ts +46 -0
- package/dist/banner.d.ts.map +1 -0
- package/dist/banner.js +88 -0
- package/dist/banner.js.map +1 -0
- package/dist/cli.d.ts +43 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +184 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +96 -0
- package/dist/config.js.map +1 -0
- package/dist/guards.d.ts +18 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +36 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/open.d.ts +9 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +28 -0
- package/dist/open.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Mica Editor
|
|
2
|
+
|
|
3
|
+
> **Experimental.** Pre-1.0: the CLI, the API and the content format can still
|
|
4
|
+
> change without notice. Use it at your own risk.
|
|
5
|
+
|
|
6
|
+
The local Mica editor: `@micacms/runtime` and `@micacms/editor-ui` composed for
|
|
7
|
+
a developer's own machine. No authentication, no user, no database — the
|
|
8
|
+
content directory is the only thing this process writes, and loopback plus a
|
|
9
|
+
Host allowlist are what make that safe.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
localhost:6422
|
|
13
|
+
│
|
|
14
|
+
▼
|
|
15
|
+
Hono ── /api/* /admin/* /health
|
|
16
|
+
│ │
|
|
17
|
+
connector Vue SPA (from @micacms/editor-ui)
|
|
18
|
+
│
|
|
19
|
+
your working tree ←— the draft workspace
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Running it against a project
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @micacms/editor --content ./content
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The published package carries the API and the built admin, so this is one
|
|
29
|
+
command and no clone. It binds to **loopback** and refuses anything else,
|
|
30
|
+
answers only to `localhost` / `127.0.0.1` / `[::1]` — any other Host gets a 403
|
|
31
|
+
pointing at the self-hosted distribution — and prints a banner instead of
|
|
32
|
+
startup log lines before opening a browser on the editor.
|
|
33
|
+
|
|
34
|
+
There is no sign-in, because there is nothing to sign in to: no identity
|
|
35
|
+
provider, no account, no session, and no `@micacms/auth-oidc` anywhere in the
|
|
36
|
+
dependency graph — a boundary test walks the graph to keep it that way. There
|
|
37
|
+
is no database either: a save is a file in your working tree, which your
|
|
38
|
+
site's dev server hot-reloads, and Publish turns the reviewed changes into one
|
|
39
|
+
commit pushed with your own Git configuration (`MICA_LOCAL_PUBLISH=false`
|
|
40
|
+
keeps Publish out of the editor entirely).
|
|
41
|
+
|
|
42
|
+
## Working on Mica itself
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
rush update
|
|
46
|
+
rush build --to tag:packages
|
|
47
|
+
|
|
48
|
+
cd apps/local-editor
|
|
49
|
+
cp .env.example .env
|
|
50
|
+
|
|
51
|
+
rush dev # API on :6422, admin on :5173
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`CONTENT_ROOT=../../examples/muster-consulting/content` is the default, so the
|
|
55
|
+
server edits the example project's content. There is nothing to seed: the
|
|
56
|
+
content is already there, in Git.
|
|
57
|
+
|
|
58
|
+
The server has no content model of its own. It reads one from the content
|
|
59
|
+
directory at `.mica/schema.json`, which the Astro project publishes and the
|
|
60
|
+
developer commits. If it is not there, the server says so and exits.
|
|
61
|
+
|
|
62
|
+
Then open <http://localhost:5173/admin/>. The editor opens directly — no name,
|
|
63
|
+
no avatar and no account menu, because there is nothing behind them.
|
|
64
|
+
|
|
65
|
+
`rush dev` runs this app and the Vite dev server together. Vite
|
|
66
|
+
proxies `/api` to the API; the API still needs
|
|
67
|
+
`ADMIN_ORIGINS=http://localhost:5173` for its Origin check, which
|
|
68
|
+
`.env.example` already sets.
|
|
69
|
+
|
|
70
|
+
## What lives where
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
src/
|
|
74
|
+
├── cli.ts the `mica` bin: flags, checks, environment, hand-off
|
|
75
|
+
├── index.ts the composition: local connector + runtime + editor UI
|
|
76
|
+
├── config.ts the local slice of the environment, and the refusals
|
|
77
|
+
├── guards.ts the Host allowlist
|
|
78
|
+
└── banner.ts what a person sees instead of log lines
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Everything of substance is elsewhere: the API and services in
|
|
82
|
+
`@micacms/runtime`, the SPA in `@micacms/editor-ui`, the content rules in
|
|
83
|
+
`@micacms/core` and `@micacms/connectors`. This package is the composition the
|
|
84
|
+
architecture calls `apps/local-editor`, published under the name the command
|
|
85
|
+
deserves.
|
|
86
|
+
|
|
87
|
+
A deployment other people can reach is the other composition:
|
|
88
|
+
`@micacms/self-hosted`, with an OIDC provider in front — see
|
|
89
|
+
[docs/self-hosting.md](../../docs/self-hosting.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-dir.d.ts","sourceRoot":"","sources":["../src/admin-dir.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAO9E,wBAAgB,YAAY,IAAI,MAAM,GAAG,SAAS,CAQjD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Where the built editor UI lives: inside the @micacms/editor-ui package. */
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
export function editorUiDist() {
|
|
6
|
+
try {
|
|
7
|
+
return path.join(path.dirname(require.resolve("@micacms/editor-ui/package.json")), "dist");
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Running from source before an install has linked the package. The app
|
|
11
|
+
// falls back to its 503 explainer, which names the fix.
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=admin-dir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-dir.js","sourceRoot":"","sources":["../src/admin-dir.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAE9E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,UAAU,YAAY;IAC1B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC7F,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,wDAAwD;QACxD,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `npx @micacms/editor` says when it works.
|
|
3
|
+
*
|
|
4
|
+
* The startup log lines are written for whoever is reading a container's
|
|
5
|
+
* output later: every field that could matter, flat, greppable, and in no
|
|
6
|
+
* particular order of importance. A person who has just typed a command needs
|
|
7
|
+
* the opposite — the address to open, the directory being edited, and what
|
|
8
|
+
* pressing Save will do to their repository. Three facts, in that order,
|
|
9
|
+
* because the first minute decides whether there is a second one.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is decoration over information the server already has. It is
|
|
12
|
+
* printed instead of the startup log lines, never as well: two accounts of the
|
|
13
|
+
* same event, disagreeing in tone, is worse than either alone.
|
|
14
|
+
*/
|
|
15
|
+
export interface Banner {
|
|
16
|
+
version: string;
|
|
17
|
+
/** The origin the person should open, without the `/admin/` path. */
|
|
18
|
+
url: string;
|
|
19
|
+
contentRoot: string;
|
|
20
|
+
collections: number;
|
|
21
|
+
singletons: number;
|
|
22
|
+
}
|
|
23
|
+
/** ANSI, or nothing at all when the output is not a terminal. */
|
|
24
|
+
export declare function colors(enabled: boolean): {
|
|
25
|
+
bold: (text: string) => string;
|
|
26
|
+
dim: (text: string) => string;
|
|
27
|
+
brand: (text: string) => string;
|
|
28
|
+
label: (text: string) => string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* True unless something says otherwise.
|
|
32
|
+
*
|
|
33
|
+
* `NO_COLOR` is the convention (no-color.org); a pipe or a file is the other
|
|
34
|
+
* case, and escape codes in a redirected log help nobody.
|
|
35
|
+
*/
|
|
36
|
+
export declare function supportsColor(stream: {
|
|
37
|
+
isTTY?: boolean;
|
|
38
|
+
}, env: NodeJS.ProcessEnv): boolean;
|
|
39
|
+
export declare function startupBanner(banner: Banner, useColor: boolean): string;
|
|
40
|
+
/**
|
|
41
|
+
* The one failure this command is most likely to hit, and the one the operating
|
|
42
|
+
* system explains worst: `EADDRINUSE` alone tells nobody which port or what to
|
|
43
|
+
* do instead.
|
|
44
|
+
*/
|
|
45
|
+
export declare function portInUse(port: number, host: string, useColor: boolean): string;
|
|
46
|
+
//# sourceMappingURL=banner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../src/banner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iEAAiE;AACjE,wBAAgB,MAAM,CAAC,OAAO,EAAE,OAAO;iBACC,MAAM;gBAAN,MAAM;kBAAN,MAAM;kBAAN,MAAM;EAO7C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAI1F;AAkBD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAuBvE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAQ/E"}
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `npx @micacms/editor` says when it works.
|
|
3
|
+
*
|
|
4
|
+
* The startup log lines are written for whoever is reading a container's
|
|
5
|
+
* output later: every field that could matter, flat, greppable, and in no
|
|
6
|
+
* particular order of importance. A person who has just typed a command needs
|
|
7
|
+
* the opposite — the address to open, the directory being edited, and what
|
|
8
|
+
* pressing Save will do to their repository. Three facts, in that order,
|
|
9
|
+
* because the first minute decides whether there is a second one.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is decoration over information the server already has. It is
|
|
12
|
+
* printed instead of the startup log lines, never as well: two accounts of the
|
|
13
|
+
* same event, disagreeing in tone, is worse than either alone.
|
|
14
|
+
*/
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
/** ANSI, or nothing at all when the output is not a terminal. */
|
|
17
|
+
export function colors(enabled) {
|
|
18
|
+
const wrap = (code) => (text) => (enabled ? `[${code}m${text}[0m` : text);
|
|
19
|
+
return {
|
|
20
|
+
bold: wrap("1"),
|
|
21
|
+
dim: wrap("2"),
|
|
22
|
+
brand: wrap("36"),
|
|
23
|
+
label: wrap("2"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* True unless something says otherwise.
|
|
28
|
+
*
|
|
29
|
+
* `NO_COLOR` is the convention (no-color.org); a pipe or a file is the other
|
|
30
|
+
* case, and escape codes in a redirected log help nobody.
|
|
31
|
+
*/
|
|
32
|
+
export function supportsColor(stream, env) {
|
|
33
|
+
if (env["NO_COLOR"])
|
|
34
|
+
return false;
|
|
35
|
+
if (env["FORCE_COLOR"])
|
|
36
|
+
return true;
|
|
37
|
+
return Boolean(stream.isTTY);
|
|
38
|
+
}
|
|
39
|
+
/** Paths inside the working directory read better as the relative ones. */
|
|
40
|
+
function readable(target, cwd = process.cwd()) {
|
|
41
|
+
const relative = path.relative(cwd, target);
|
|
42
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative))
|
|
43
|
+
return target;
|
|
44
|
+
return `./${relative}`;
|
|
45
|
+
}
|
|
46
|
+
/** "3 collections and 2 singletons", or whichever half is not zero. */
|
|
47
|
+
function modelSummary(collections, singletons) {
|
|
48
|
+
const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
49
|
+
const parts = [];
|
|
50
|
+
if (collections > 0)
|
|
51
|
+
parts.push(plural(collections, "collection"));
|
|
52
|
+
if (singletons > 0)
|
|
53
|
+
parts.push(plural(singletons, "singleton"));
|
|
54
|
+
return parts.join(" and ") || "an empty model";
|
|
55
|
+
}
|
|
56
|
+
export function startupBanner(banner, useColor) {
|
|
57
|
+
const c = colors(useColor);
|
|
58
|
+
const editor = `${banner.url}/admin/`;
|
|
59
|
+
const row = (label, value) => ` ${c.label(label.padEnd(9))} ${value}`;
|
|
60
|
+
const lines = [
|
|
61
|
+
"",
|
|
62
|
+
` ${c.brand("◆")} ${c.bold("Mica")} ${c.dim(banner.version)}`,
|
|
63
|
+
"",
|
|
64
|
+
row("Editor", c.bold(editor)),
|
|
65
|
+
row("Content", `${readable(banner.contentRoot)} ${c.dim(`— ${modelSummary(banner.collections, banner.singletons)}`)}`),
|
|
66
|
+
row("Saving", c.dim("writes files into that directory; Publish commits and pushes them.")),
|
|
67
|
+
"",
|
|
68
|
+
` ${c.dim("No sign-in on your own machine — the link above opens straight into the editor.")}`,
|
|
69
|
+
` ${c.dim("Mica keeps no state of its own here: everything you save is a file in that directory.")}`,
|
|
70
|
+
"",
|
|
71
|
+
` ${c.dim("Run your site's dev server beside this to see the changes. Ctrl+C stops Mica.")}`,
|
|
72
|
+
"",
|
|
73
|
+
];
|
|
74
|
+
return `${lines.join("\n")}\n`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The one failure this command is most likely to hit, and the one the operating
|
|
78
|
+
* system explains worst: `EADDRINUSE` alone tells nobody which port or what to
|
|
79
|
+
* do instead.
|
|
80
|
+
*/
|
|
81
|
+
export function portInUse(port, host, useColor) {
|
|
82
|
+
const c = colors(useColor);
|
|
83
|
+
return (`\n ${c.bold(`Port ${port} is already in use on ${host}.`)}\n\n` +
|
|
84
|
+
` Something else is listening there — another Mica, most likely.\n` +
|
|
85
|
+
` Stop it, or give this one a different port:\n\n` +
|
|
86
|
+
` npx @micacms/editor --port ${port + 1}\n\n`);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=banner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banner.js","sourceRoot":"","sources":["../src/banner.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npx @micacms/editor` — Mica against a project on this machine.
|
|
4
|
+
*
|
|
5
|
+
* The point of this file is that adopting Mica should not mean adopting this
|
|
6
|
+
* repository. Everything it does is set environment variables and hand over to
|
|
7
|
+
* the same entry point a container runs, so there is one server with one
|
|
8
|
+
* configuration path and this is a front door rather than a second one.
|
|
9
|
+
*
|
|
10
|
+
* Two defaults differ from a container's, and both are deliberate:
|
|
11
|
+
*
|
|
12
|
+
* - it binds to loopback. A container binds 0.0.0.0 because it has to; a
|
|
13
|
+
* laptop does not, and this editor has no authentication. Anything
|
|
14
|
+
* reachable by anyone else runs @micacms/self-hosted with an identity
|
|
15
|
+
* provider — see docs/self-hosting.md.
|
|
16
|
+
* - it talks to a person instead of to a log collector: one short banner
|
|
17
|
+
* saying where the editor is and what happens when you save, and the
|
|
18
|
+
* browser opened on it. `MICA_CLI` is how the server is told which of the
|
|
19
|
+
* two audiences it has.
|
|
20
|
+
*
|
|
21
|
+
* There is no state to keep anywhere: the local editor has no database, no
|
|
22
|
+
* account and no session, so nothing is written outside the content
|
|
23
|
+
* directory.
|
|
24
|
+
*
|
|
25
|
+
* Explicit environment wins over every flag default here, and a flag wins over
|
|
26
|
+
* a `.env` file, because `loadConfig` leaves already-set variables alone.
|
|
27
|
+
*/
|
|
28
|
+
export interface Options {
|
|
29
|
+
content: string;
|
|
30
|
+
port?: string;
|
|
31
|
+
host?: string;
|
|
32
|
+
/** Undefined means "decide from the terminal" — see `shouldOpenBrowser`. */
|
|
33
|
+
open?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function parse(argv: string[]): Options;
|
|
36
|
+
/**
|
|
37
|
+
* Opening a browser is a courtesy, so it is offered only where it can be
|
|
38
|
+
* welcome: an interactive terminal that somebody is sitting at. A script, a
|
|
39
|
+
* container and a CI job all get a server and no surprises.
|
|
40
|
+
*/
|
|
41
|
+
export declare function shouldOpenBrowser(options: Options, env: NodeJS.ProcessEnv): boolean;
|
|
42
|
+
export declare function main(): Promise<void>;
|
|
43
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AASH,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAsBD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAgD7C;AAkBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAGnF;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAoD1C"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npx @micacms/editor` — Mica against a project on this machine.
|
|
4
|
+
*
|
|
5
|
+
* The point of this file is that adopting Mica should not mean adopting this
|
|
6
|
+
* repository. Everything it does is set environment variables and hand over to
|
|
7
|
+
* the same entry point a container runs, so there is one server with one
|
|
8
|
+
* configuration path and this is a front door rather than a second one.
|
|
9
|
+
*
|
|
10
|
+
* Two defaults differ from a container's, and both are deliberate:
|
|
11
|
+
*
|
|
12
|
+
* - it binds to loopback. A container binds 0.0.0.0 because it has to; a
|
|
13
|
+
* laptop does not, and this editor has no authentication. Anything
|
|
14
|
+
* reachable by anyone else runs @micacms/self-hosted with an identity
|
|
15
|
+
* provider — see docs/self-hosting.md.
|
|
16
|
+
* - it talks to a person instead of to a log collector: one short banner
|
|
17
|
+
* saying where the editor is and what happens when you save, and the
|
|
18
|
+
* browser opened on it. `MICA_CLI` is how the server is told which of the
|
|
19
|
+
* two audiences it has.
|
|
20
|
+
*
|
|
21
|
+
* There is no state to keep anywhere: the local editor has no database, no
|
|
22
|
+
* account and no session, so nothing is written outside the content
|
|
23
|
+
* directory.
|
|
24
|
+
*
|
|
25
|
+
* Explicit environment wins over every flag default here, and a flag wins over
|
|
26
|
+
* a `.env` file, because `loadConfig` leaves already-set variables alone.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { pathToFileURL } from "node:url";
|
|
31
|
+
import { MANIFEST_PATH } from "@micacms/core";
|
|
32
|
+
import { DEFAULT_PORT, VERSION } from "@micacms/runtime";
|
|
33
|
+
const USAGE = `
|
|
34
|
+
mica — edit your Astro project's content, on this machine
|
|
35
|
+
|
|
36
|
+
Usage
|
|
37
|
+
$ npx @micacms/editor [content directory]
|
|
38
|
+
|
|
39
|
+
Options
|
|
40
|
+
-c, --content <dir> The content root (default: ./content)
|
|
41
|
+
-p, --port <number> Port to listen on (default: ${DEFAULT_PORT})
|
|
42
|
+
--host <address> Address to bind (default: 127.0.0.1)
|
|
43
|
+
--no-open Do not open the browser
|
|
44
|
+
-h, --help Show this
|
|
45
|
+
-v, --version Print the version
|
|
46
|
+
|
|
47
|
+
There is no sign-in here, because there is nothing to sign in to: this mode
|
|
48
|
+
has no identity provider, no account and no user. That is right for your own
|
|
49
|
+
machine and wrong everywhere else — a Mica other people reach runs as a
|
|
50
|
+
container with an identity provider. See docs/self-hosting.md.
|
|
51
|
+
`;
|
|
52
|
+
export function parse(argv) {
|
|
53
|
+
const options = { content: "./content" };
|
|
54
|
+
for (let index = 0; index < argv.length; index++) {
|
|
55
|
+
const flag = argv[index];
|
|
56
|
+
const value = () => {
|
|
57
|
+
const next = argv[++index];
|
|
58
|
+
if (next === undefined || next.startsWith("-"))
|
|
59
|
+
fail(`${flag} needs a value.`);
|
|
60
|
+
return next;
|
|
61
|
+
};
|
|
62
|
+
switch (flag) {
|
|
63
|
+
case "-c":
|
|
64
|
+
case "--content":
|
|
65
|
+
options.content = value();
|
|
66
|
+
break;
|
|
67
|
+
case "-p":
|
|
68
|
+
case "--port":
|
|
69
|
+
options.port = value();
|
|
70
|
+
break;
|
|
71
|
+
case "--host":
|
|
72
|
+
options.host = value();
|
|
73
|
+
break;
|
|
74
|
+
case "--open":
|
|
75
|
+
options.open = true;
|
|
76
|
+
break;
|
|
77
|
+
case "--no-open":
|
|
78
|
+
options.open = false;
|
|
79
|
+
break;
|
|
80
|
+
case "-h":
|
|
81
|
+
case "--help":
|
|
82
|
+
process.stdout.write(`${USAGE}\n`);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
case "-v":
|
|
85
|
+
case "--version":
|
|
86
|
+
process.stdout.write(`${VERSION}\n`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
default:
|
|
89
|
+
// A bare path is the content root, so `npx @micacms/editor ./site/content`
|
|
90
|
+
// does what it looks like it does.
|
|
91
|
+
if (!flag.startsWith("-"))
|
|
92
|
+
options.content = flag;
|
|
93
|
+
else
|
|
94
|
+
fail(`Unknown option "${flag}".`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return options;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A problem the person can fix, said the way a person reads.
|
|
101
|
+
*
|
|
102
|
+
* The title is the fact, the lines under it are what to do about it. Anything
|
|
103
|
+
* printed here has already decided the command is over: the caller exits.
|
|
104
|
+
*/
|
|
105
|
+
function report(title, lines) {
|
|
106
|
+
const body = lines.map((line) => (line ? ` ${line}` : "")).join("\n");
|
|
107
|
+
process.stderr.write(`\n ${title}\n\n${body}\n\n`);
|
|
108
|
+
}
|
|
109
|
+
function fail(message) {
|
|
110
|
+
report(message, ["Run `npx @micacms/editor --help` to see every option."]);
|
|
111
|
+
process.exit(64); // EX_USAGE
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Opening a browser is a courtesy, so it is offered only where it can be
|
|
115
|
+
* welcome: an interactive terminal that somebody is sitting at. A script, a
|
|
116
|
+
* container and a CI job all get a server and no surprises.
|
|
117
|
+
*/
|
|
118
|
+
export function shouldOpenBrowser(options, env) {
|
|
119
|
+
if (options.open !== undefined)
|
|
120
|
+
return options.open;
|
|
121
|
+
return Boolean(process.stdout.isTTY) && !env["CI"];
|
|
122
|
+
}
|
|
123
|
+
export async function main() {
|
|
124
|
+
const options = parse(process.argv.slice(2));
|
|
125
|
+
const content = path.resolve(options.content);
|
|
126
|
+
if (!existsSync(content)) {
|
|
127
|
+
report(`There is no content directory at ${content}.`, [
|
|
128
|
+
"Mica edits the directory your Astro project publishes content into —",
|
|
129
|
+
"the `root` in your `defineSite({ root: \"./content\" })`.",
|
|
130
|
+
"",
|
|
131
|
+
" npx @micacms/editor ./path/to/content",
|
|
132
|
+
"",
|
|
133
|
+
"If this is a new project, create the directory and Mica writes the first",
|
|
134
|
+
"entry into it.",
|
|
135
|
+
]);
|
|
136
|
+
process.exit(64); // EX_USAGE
|
|
137
|
+
}
|
|
138
|
+
// A far more useful message than the server's, because here we know the
|
|
139
|
+
// project is on this machine and can name the command that fixes it.
|
|
140
|
+
if (!existsSync(path.join(content, MANIFEST_PATH))) {
|
|
141
|
+
report(`There is no content model at ${path.join(content, MANIFEST_PATH)}.`, [
|
|
142
|
+
"Mica ships without one: your Astro project defines the model, and building",
|
|
143
|
+
"the project writes it there. Run this in the project, then start Mica again:",
|
|
144
|
+
"",
|
|
145
|
+
" npx astro build # or start the dev server once",
|
|
146
|
+
"",
|
|
147
|
+
"If you have not defined a model yet, docs/getting-started.md is five minutes.",
|
|
148
|
+
]);
|
|
149
|
+
process.exit(78); // EX_CONFIG
|
|
150
|
+
}
|
|
151
|
+
const defaults = {
|
|
152
|
+
CONTENT_ROOT: content,
|
|
153
|
+
HOST: options.host ?? "127.0.0.1",
|
|
154
|
+
PORT: options.port ?? String(DEFAULT_PORT),
|
|
155
|
+
// Tells the server it is talking to a person: one banner instead of the
|
|
156
|
+
// startup log lines a collector wants.
|
|
157
|
+
MICA_CLI: "true",
|
|
158
|
+
MICA_OPEN: String(shouldOpenBrowser(options, process.env)),
|
|
159
|
+
};
|
|
160
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
161
|
+
process.env[key] ??= value;
|
|
162
|
+
}
|
|
163
|
+
// MICA_PUBLIC_URL has to agree with the port, and only matters here for
|
|
164
|
+
// cookie scope and the Origin check — both of which are this process's own
|
|
165
|
+
// address.
|
|
166
|
+
process.env["MICA_PUBLIC_URL"] ??= `http://localhost:${process.env["PORT"]}`;
|
|
167
|
+
// The server is imported rather than spawned: one process, one signal
|
|
168
|
+
// handler, and a stack trace that points at the real thing.
|
|
169
|
+
await import("./index.js");
|
|
170
|
+
}
|
|
171
|
+
// Only when this file *is* the command. Importing it — which the tests do, and
|
|
172
|
+
// which a future `mica init` would — must not start a server.
|
|
173
|
+
//
|
|
174
|
+
// `realpathSync` matters: npm and npx run this file through a symlink in
|
|
175
|
+
// `node_modules/.bin`, and Node resolves `import.meta.url` to the symlink's
|
|
176
|
+
// real target. Comparing against the raw `argv[1]` never matched there, so
|
|
177
|
+
// `npx @micacms/editor` silently did nothing.
|
|
178
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
179
|
+
main().catch((error) => {
|
|
180
|
+
process.stderr.write(`\n${error instanceof Error ? error.stack : String(error)}\n`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local editor's configuration.
|
|
3
|
+
*
|
|
4
|
+
* Small on purpose: the variables the runtime reads, minus everything that
|
|
5
|
+
* describes a deployment. There is no `REPOSITORY` — this app *is* the local
|
|
6
|
+
* connector against a checkout — no `DATA_DIR`, because it keeps no state,
|
|
7
|
+
* and no OIDC or admission variable exists to be set, which is a stronger
|
|
8
|
+
* statement than ignoring one.
|
|
9
|
+
*
|
|
10
|
+
* The refusals are the containment for a server with no authentication:
|
|
11
|
+
* production is refused, a non-local public URL is refused, and any listening
|
|
12
|
+
* address but loopback is refused, with no override. Remote access is what
|
|
13
|
+
* the self-hosted distribution is.
|
|
14
|
+
*/
|
|
15
|
+
import { type RuntimeConfig } from "@micacms/runtime";
|
|
16
|
+
export type LocalConfig = RuntimeConfig & {
|
|
17
|
+
MICA_CLI: boolean;
|
|
18
|
+
MICA_OPEN: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): LocalConfig;
|
|
21
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EASL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAsD1B,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC;AAEpF,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,WAAW,CAkD5E"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local editor's configuration.
|
|
3
|
+
*
|
|
4
|
+
* Small on purpose: the variables the runtime reads, minus everything that
|
|
5
|
+
* describes a deployment. There is no `REPOSITORY` — this app *is* the local
|
|
6
|
+
* connector against a checkout — no `DATA_DIR`, because it keeps no state,
|
|
7
|
+
* and no OIDC or admission variable exists to be set, which is a stronger
|
|
8
|
+
* statement than ignoring one.
|
|
9
|
+
*
|
|
10
|
+
* The refusals are the containment for a server with no authentication:
|
|
11
|
+
* production is refused, a non-local public URL is refused, and any listening
|
|
12
|
+
* address but loopback is refused, with no override. Remote access is what
|
|
13
|
+
* the self-hosted distribution is.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { bool, ConfigError, deriveRuntime, describeConfigIssues, loadEnvFile, LOOPBACK, runtimeEnv, withoutBlanks, } from "@micacms/runtime";
|
|
17
|
+
const { NODE_ENV, LOG_LEVEL, HOST, PORT, MICA_PUBLIC_URL, CONTENT_ROOT, MICA_LOCAL_PUBLISH, MAX_UPLOAD_BYTES, ADMIN_ORIGINS, ADMIN_DIR, } = runtimeEnv;
|
|
18
|
+
const schema = z
|
|
19
|
+
.object({
|
|
20
|
+
NODE_ENV,
|
|
21
|
+
LOG_LEVEL,
|
|
22
|
+
HOST,
|
|
23
|
+
PORT,
|
|
24
|
+
MICA_PUBLIC_URL,
|
|
25
|
+
CONTENT_ROOT,
|
|
26
|
+
MICA_LOCAL_PUBLISH,
|
|
27
|
+
MAX_UPLOAD_BYTES,
|
|
28
|
+
ADMIN_ORIGINS,
|
|
29
|
+
ADMIN_DIR,
|
|
30
|
+
/**
|
|
31
|
+
* Set by the `mica` bin, and by nothing else. The audience for the
|
|
32
|
+
* startup output: a person at a terminal gets one banner, a log collector
|
|
33
|
+
* gets the structured lines.
|
|
34
|
+
*/
|
|
35
|
+
MICA_CLI: bool(false),
|
|
36
|
+
/** Whether that front door should open a browser once it is listening. */
|
|
37
|
+
MICA_OPEN: bool(false),
|
|
38
|
+
})
|
|
39
|
+
.transform((env) => ({
|
|
40
|
+
...deriveRuntime({
|
|
41
|
+
...env,
|
|
42
|
+
// Fixed, not configurable: the local editor edits a checkout. A remote
|
|
43
|
+
// repository is the self-hosted app's job.
|
|
44
|
+
REPOSITORY: "local",
|
|
45
|
+
REPOSITORY_ROOT: "content",
|
|
46
|
+
REPOSITORY_BRANCH: "main",
|
|
47
|
+
REPOSITORY_TOKEN: undefined,
|
|
48
|
+
REPOSITORY_PROJECT: undefined,
|
|
49
|
+
REPOSITORY_API_BASE: undefined,
|
|
50
|
+
// There is no proxy in front of a loopback server to trust.
|
|
51
|
+
TRUST_PROXY: false,
|
|
52
|
+
}),
|
|
53
|
+
HOST: env.HOST ?? "127.0.0.1",
|
|
54
|
+
}));
|
|
55
|
+
export function loadConfig(env = process.env) {
|
|
56
|
+
// Only when reading the ambient environment: tests pass an explicit object
|
|
57
|
+
// and must not pick up whatever `.env` sits in the working directory.
|
|
58
|
+
if (env === process.env)
|
|
59
|
+
loadEnvFile();
|
|
60
|
+
const parsed = schema.safeParse(withoutBlanks(env));
|
|
61
|
+
if (!parsed.success)
|
|
62
|
+
throw new ConfigError(describeConfigIssues(parsed.error));
|
|
63
|
+
const config = parsed.data;
|
|
64
|
+
/**
|
|
65
|
+
* The local editor has no authentication. It must never be reachable.
|
|
66
|
+
*
|
|
67
|
+
* This is the single most dangerous configuration Mica can be in, so it is
|
|
68
|
+
* refused at the three points where it stops being a developer's
|
|
69
|
+
* convenience: a production build, a base URL that is not the local
|
|
70
|
+
* machine, and a listening address that is not the local machine either.
|
|
71
|
+
*/
|
|
72
|
+
if (config.isProduction) {
|
|
73
|
+
throw new ConfigError("The local editor has no authentication, and NODE_ENV=production.\n" +
|
|
74
|
+
" There is no identity provider and no user: every request is allowed.\n" +
|
|
75
|
+
" A deployment runs @micacms/self-hosted with an OIDC provider instead —\n" +
|
|
76
|
+
" see docs/self-hosting.md.");
|
|
77
|
+
}
|
|
78
|
+
const host = new URL(config.MICA_PUBLIC_URL).hostname;
|
|
79
|
+
if (!LOOPBACK.has(host)) {
|
|
80
|
+
throw new ConfigError(`The local editor has no authentication, but MICA_PUBLIC_URL points at "${host}".\n` +
|
|
81
|
+
" It is for a developer's own machine only. Anything reachable by anyone\n" +
|
|
82
|
+
" else runs @micacms/self-hosted with an identity provider.");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The same rule for the socket. A base URL saying `localhost` while the
|
|
86
|
+
* process listens on `0.0.0.0` leaves an unauthenticated editor open to the
|
|
87
|
+
* whole network. There is deliberately no override.
|
|
88
|
+
*/
|
|
89
|
+
if (!LOOPBACK.has(config.HOST) && config.HOST !== "::1") {
|
|
90
|
+
throw new ConfigError(`The local editor has no authentication, but HOST is "${config.HOST}".\n` +
|
|
91
|
+
" It binds to the loopback address and nothing else. Use HOST=127.0.0.1,\n" +
|
|
92
|
+
" or run @micacms/self-hosted with an identity provider.");
|
|
93
|
+
}
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,IAAI,EACJ,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,QAAQ,EACR,UAAU,EACV,aAAa,GAEd,MAAM,kBAAkB,CAAC;AAE1B,MAAM,EACJ,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,SAAS,GACV,GAAG,UAAU,CAAC;AAEf,MAAM,MAAM,GAAG,CAAC;KACb,MAAM,CAAC;IACN,QAAQ;IACR,SAAS;IACT,IAAI;IACJ,IAAI;IACJ,eAAe;IACf,YAAY;IACZ,kBAAkB;IAClB,gBAAgB;IAChB,aAAa;IACb,SAAS;IAET;;;;OAIG;IACH,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC;IACrB,0EAA0E;IAC1E,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC;CACvB,CAAC;KACD,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACnB,GAAG,aAAa,CAAC;QACf,GAAG,GAAG;QACN,uEAAuE;QACvE,2CAA2C;QAC3C,UAAU,EAAE,OAAgB;QAC5B,eAAe,EAAE,SAAS;QAC1B,iBAAiB,EAAE,MAAM;QACzB,gBAAgB,EAAE,SAAS;QAC3B,kBAAkB,EAAE,SAAS;QAC7B,mBAAmB,EAAE,SAAS;QAC9B,4DAA4D;QAC5D,WAAW,EAAE,KAAK;KACnB,CAAC;IACF,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,WAAW;CAC9B,CAAC,CAAC,CAAC;AAIN,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,2EAA2E;IAC3E,sEAAsE;IACtE,IAAI,GAAG,KAAK,OAAO,CAAC,GAAG;QAAE,WAAW,EAAE,CAAC;IAEvC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,MAAM,IAAI,WAAW,CAAC,oBAAoB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/E,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;IAE3B;;;;;;;OAOG;IACH,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,oEAAoE;YAClE,0EAA0E;YAC1E,4EAA4E;YAC5E,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC;IACtD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,0EAA0E,IAAI,MAAM;YAClF,4EAA4E;YAC5E,6DAA6D,CAChE,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QACxD,MAAM,IAAI,WAAW,CACnB,wDAAwD,MAAM,CAAC,IAAI,MAAM;YACvE,4EAA4E;YAC5E,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/guards.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host allowlist — what replaced the local session's cookie.
|
|
3
|
+
*
|
|
4
|
+
* Under DNS rebinding, a page re-resolves its own name to `127.0.0.1` and the
|
|
5
|
+
* browser then treats requests to it as same-origin. Writes still fail on the
|
|
6
|
+
* Origin check; *reads* would succeed, and the only thing that used to stop
|
|
7
|
+
* them was a session cookie scoped to `localhost` that the rebound name never
|
|
8
|
+
* received. The cookie is gone — local mode keeps no session, no token and no
|
|
9
|
+
* database — so the rule is stated directly instead: this server answers only
|
|
10
|
+
* to names that are this machine.
|
|
11
|
+
*
|
|
12
|
+
* Smaller than what it replaces, standard for exactly this, and applied in
|
|
13
|
+
* front of everything, /health included.
|
|
14
|
+
*/
|
|
15
|
+
import type { MiddlewareHandler } from "hono";
|
|
16
|
+
import type { Env } from "@micacms/runtime";
|
|
17
|
+
export declare function hostGuard(): MiddlewareHandler<Env>;
|
|
18
|
+
//# sourceMappingURL=guards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAa5C,wBAAgB,SAAS,IAAI,iBAAiB,CAAC,GAAG,CAAC,CAgBlD"}
|
package/dist/guards.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host allowlist — what replaced the local session's cookie.
|
|
3
|
+
*
|
|
4
|
+
* Under DNS rebinding, a page re-resolves its own name to `127.0.0.1` and the
|
|
5
|
+
* browser then treats requests to it as same-origin. Writes still fail on the
|
|
6
|
+
* Origin check; *reads* would succeed, and the only thing that used to stop
|
|
7
|
+
* them was a session cookie scoped to `localhost` that the rebound name never
|
|
8
|
+
* received. The cookie is gone — local mode keeps no session, no token and no
|
|
9
|
+
* database — so the rule is stated directly instead: this server answers only
|
|
10
|
+
* to names that are this machine.
|
|
11
|
+
*
|
|
12
|
+
* Smaller than what it replaces, standard for exactly this, and applied in
|
|
13
|
+
* front of everything, /health included.
|
|
14
|
+
*/
|
|
15
|
+
const ALLOWED = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
16
|
+
/** The hostname part of a Host header, brackets kept for IPv6. */
|
|
17
|
+
function hostnameOf(header) {
|
|
18
|
+
if (header.startsWith("[")) {
|
|
19
|
+
const end = header.indexOf("]");
|
|
20
|
+
return end < 0 ? header : header.slice(0, end + 1);
|
|
21
|
+
}
|
|
22
|
+
return header.split(":")[0] ?? "";
|
|
23
|
+
}
|
|
24
|
+
export function hostGuard() {
|
|
25
|
+
return async (c, next) => {
|
|
26
|
+
const host = c.req.header("host") ?? "";
|
|
27
|
+
if (!ALLOWED.has(hostnameOf(host).toLowerCase())) {
|
|
28
|
+
return c.text("This is a local Mica editor. It answers only to this machine —\n" +
|
|
29
|
+
`"${host}" is not it.\n\n` +
|
|
30
|
+
"To let other people edit, deploy @micacms/self-hosted with an identity\n" +
|
|
31
|
+
"provider: https://github.com/amuedespacher/micacms/blob/main/docs/self-hosting.md\n", 403);
|
|
32
|
+
}
|
|
33
|
+
await next();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAEpE,kEAAkE;AAClE,SAAS,UAAU,CAAC,MAAc;IAChC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAExC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACjD,OAAO,CAAC,CAAC,IAAI,CACX,kEAAkE;gBAChE,IAAI,IAAI,kBAAkB;gBAC1B,0EAA0E;gBAC1E,qFAAqF,EACvF,GAAG,CACJ,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local editor's process entry point.
|
|
3
|
+
*
|
|
4
|
+
* The whole composition, in one page: the local connector, the
|
|
5
|
+
* auth-neutral runtime with `allowAll()`, the Host allowlist, and nothing
|
|
6
|
+
* else. No session service, no admission policy, no database — `npx
|
|
7
|
+
* @micacms/editor` writes nothing outside the content directory, and there is
|
|
8
|
+
* no auth code here to misconfigure because none is installed.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local editor's process entry point.
|
|
3
|
+
*
|
|
4
|
+
* The whole composition, in one page: the local connector, the
|
|
5
|
+
* auth-neutral runtime with `allowAll()`, the Host allowlist, and nothing
|
|
6
|
+
* else. No session service, no admission policy, no database — `npx
|
|
7
|
+
* @micacms/editor` writes nothing outside the content directory, and there is
|
|
8
|
+
* no auth code here to misconfigure because none is installed.
|
|
9
|
+
*/
|
|
10
|
+
import { localConnector } from "@micacms/connectors";
|
|
11
|
+
import { MANIFEST_PATH } from "@micacms/core";
|
|
12
|
+
import { ConfigError, createApp, createLogger, createRuntimeContext, loadSchema, SchemaError, schemaFingerprint, schemaOrigin, startServer, VERSION, } from "@micacms/runtime";
|
|
13
|
+
import { editorUiDist } from "./admin-dir.js";
|
|
14
|
+
import { portInUse, startupBanner, supportsColor } from "./banner.js";
|
|
15
|
+
import { loadConfig } from "./config.js";
|
|
16
|
+
import { hostGuard } from "./guards.js";
|
|
17
|
+
import { openInBrowser } from "./open.js";
|
|
18
|
+
async function main() {
|
|
19
|
+
let config;
|
|
20
|
+
try {
|
|
21
|
+
config = loadConfig();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof ConfigError) {
|
|
25
|
+
process.stderr.write(`\n${error.message}\n\n`);
|
|
26
|
+
process.exit(78); // EX_CONFIG
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
const logger = createLogger({ level: config.LOG_LEVEL, pretty: true });
|
|
31
|
+
// The repository comes first, because the content model is in it. A bad or
|
|
32
|
+
// absent model is a project mistake rather than a crash: exit the way a bad
|
|
33
|
+
// environment does, with the message on stderr instead of a stack trace.
|
|
34
|
+
const connector = localConnector({ root: config.contentRoot });
|
|
35
|
+
let model;
|
|
36
|
+
try {
|
|
37
|
+
model = await loadSchema(connector);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof SchemaError) {
|
|
41
|
+
process.stderr.write(`\n${error.message}\n\n`);
|
|
42
|
+
process.exit(78); // EX_CONFIG
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const schemaRevision = await connector.revision(MANIFEST_PATH);
|
|
47
|
+
const ctx = createRuntimeContext({
|
|
48
|
+
config,
|
|
49
|
+
model,
|
|
50
|
+
connector,
|
|
51
|
+
schemaRevision,
|
|
52
|
+
logger,
|
|
53
|
+
operational: () => ({
|
|
54
|
+
auth: "none — the local editor has no accounts and no sign-in",
|
|
55
|
+
database: null,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
const app = createApp(ctx, {
|
|
59
|
+
guards: [hostGuard()],
|
|
60
|
+
adminDir: config.ADMIN_DIR ?? editorUiDist(),
|
|
61
|
+
});
|
|
62
|
+
const fingerprint = await schemaFingerprint(model);
|
|
63
|
+
startServer({
|
|
64
|
+
app,
|
|
65
|
+
host: config.HOST,
|
|
66
|
+
port: config.PORT,
|
|
67
|
+
logger,
|
|
68
|
+
onListening() {
|
|
69
|
+
/**
|
|
70
|
+
* Two audiences, one event: the person who just typed a command gets an
|
|
71
|
+
* address and three facts; a log collector gets every field.
|
|
72
|
+
*/
|
|
73
|
+
if (config.MICA_CLI) {
|
|
74
|
+
const useColor = supportsColor(process.stdout, process.env);
|
|
75
|
+
process.stdout.write(startupBanner({
|
|
76
|
+
version: VERSION,
|
|
77
|
+
url: config.MICA_PUBLIC_URL,
|
|
78
|
+
contentRoot: connector.origin,
|
|
79
|
+
collections: Object.keys(model.collections).length,
|
|
80
|
+
singletons: Object.keys(model.singletons).length,
|
|
81
|
+
}, useColor));
|
|
82
|
+
if (config.MICA_OPEN)
|
|
83
|
+
openInBrowser(`${config.MICA_PUBLIC_URL}/admin/`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
logger.info("Mica local editor started", {
|
|
87
|
+
url: config.MICA_PUBLIC_URL,
|
|
88
|
+
listening: `${config.HOST}:${config.PORT}`,
|
|
89
|
+
auth: "none (local editor; loopback only)",
|
|
90
|
+
content: connector.origin,
|
|
91
|
+
schema: schemaOrigin(connector),
|
|
92
|
+
schemaFingerprint: fingerprint,
|
|
93
|
+
collections: Object.keys(model.collections).length,
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
portInUseMessage: () => portInUse(config.PORT, config.HOST, supportsColor(process.stderr, process.env)),
|
|
97
|
+
shutdown: () => ctx.shutdown(),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
main().catch((error) => {
|
|
101
|
+
process.stderr.write(`Failed to start: ${error instanceof Error ? error.stack : String(error)}\n`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/open.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opens a URL in whatever the desktop considers a browser.
|
|
3
|
+
*
|
|
4
|
+
* A courtesy, and treated as one: every failure is swallowed. A machine with no
|
|
5
|
+
* desktop session — over SSH, in a container, in CI — has nothing to open, and
|
|
6
|
+
* a server that refused to start because of it would be absurd.
|
|
7
|
+
*/
|
|
8
|
+
export declare function openInBrowser(url: string): void;
|
|
9
|
+
//# sourceMappingURL=open.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../src/open.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAmB/C"}
|
package/dist/open.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opens a URL in whatever the desktop considers a browser.
|
|
3
|
+
*
|
|
4
|
+
* A courtesy, and treated as one: every failure is swallowed. A machine with no
|
|
5
|
+
* desktop session — over SSH, in a container, in CI — has nothing to open, and
|
|
6
|
+
* a server that refused to start because of it would be absurd.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
export function openInBrowser(url) {
|
|
10
|
+
const [command, args] = process.platform === "darwin"
|
|
11
|
+
? ["open", [url]]
|
|
12
|
+
: process.platform === "win32"
|
|
13
|
+
? // The empty string is `start`'s window title. Without it, a quoted URL
|
|
14
|
+
// is taken as the title and nothing opens.
|
|
15
|
+
["cmd", ["/c", "start", "", url]]
|
|
16
|
+
: ["xdg-open", [url]];
|
|
17
|
+
try {
|
|
18
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
19
|
+
child.on("error", () => { });
|
|
20
|
+
// Detached and unreferenced, so a browser that outlives Mica does not keep
|
|
21
|
+
// the Node process alive after Ctrl+C.
|
|
22
|
+
child.unref();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// There is no browser here. That is not an error.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=open.js.map
|
package/dist/open.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open.js","sourceRoot":"","sources":["../src/open.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GACnB,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC3B,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC5B,CAAC,CAAC,uEAAuE;gBACvE,2CAA2C;gBAC3C,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;YACnC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAE5B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAQ,EAAE,IAAgB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACrF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC5B,2EAA2E;QAC3E,uCAAuC;QACvC,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@micacms/editor",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The local Mica editor — `npx @micacms/editor` against a checkout on your own machine. No authentication, no database, loopback only.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Alexander Müdespacher",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mica",
|
|
10
|
+
"cms",
|
|
11
|
+
"astro",
|
|
12
|
+
"git",
|
|
13
|
+
"markdoc",
|
|
14
|
+
"headless-cms"
|
|
15
|
+
],
|
|
16
|
+
"bin": {
|
|
17
|
+
"mica": "./dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"hono": "^4.6.20",
|
|
25
|
+
"zod": "^4.3.6",
|
|
26
|
+
"@micacms/editor-ui": "0.2.0",
|
|
27
|
+
"@micacms/core": "0.2.0",
|
|
28
|
+
"@micacms/runtime": "0.2.0",
|
|
29
|
+
"@micacms/connectors": "0.2.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.20.1",
|
|
33
|
+
"tsx": "^4.19.2",
|
|
34
|
+
"typescript": "^5.7.3"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.12.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"dev": "tsx watch --clear-screen=false --include ../../examples/muster-consulting/content/.mica/schema.json src/index.ts",
|
|
45
|
+
"start": "node dist/index.js",
|
|
46
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
47
|
+
"test": "tsc -p tsconfig.test.json && node --test --import tsx test/*.test.ts"
|
|
48
|
+
}
|
|
49
|
+
}
|