@marver-design/marver 0.3.1 → 0.5.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/CHANGELOG.md +151 -0
- package/README.md +2 -0
- package/dist/auth-B36fMCM3.mjs +245 -0
- package/dist/{build-Ckmyci3O.mjs → build-BrCl9hJS.mjs} +56 -14
- package/dist/cli.mjs +15 -6
- package/dist/collab-CXy8gqoz.mjs +297 -0
- package/dist/comments-Ba8mU600.mjs +90 -0
- package/dist/comments-odHzYdO3.mjs +179 -0
- package/dist/dev-DdeU-Jst.mjs +162 -0
- package/dist/{init-DolP_Ld4.mjs → init-DsCUmlCW.mjs} +2 -1
- package/dist/{manifest-DW-T52MM.mjs → manifest-C8FODq2S.mjs} +26 -1
- package/dist/{plugin-Crp4CAma.mjs → plugin-BtSGAm2h.mjs} +169 -19
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/serve-D_KBK7Oy.mjs +431 -0
- package/dist/sync-CkBk-tUk.mjs +248 -0
- package/package.json +3 -1
- package/src/client/content/diagram.tsx +46 -2
- package/src/client/content/index.tsx +6 -2
- package/src/client/content/md.ts +29 -0
- package/src/client/content/palette.ts +6 -0
- package/src/client/frame-host/bridge.js +269 -3
- package/src/client/frame-host/serialize.ts +195 -0
- package/src/client/shell/App.tsx +131 -17
- package/src/client/shell/Comments.tsx +464 -0
- package/src/client/shell/Play.tsx +24 -0
- package/src/client/shell/canvas/Canvas.tsx +66 -29
- package/src/client/shell/canvas/FrameNode.tsx +130 -10
- package/src/client/shell/canvas/frame-registry.ts +18 -0
- package/src/client/shell/canvas/snapshots.ts +233 -0
- package/src/client/shell/comments-store.ts +180 -0
- package/src/client/shell/cursor-arrow-dark.svg +1 -0
- package/src/client/shell/cursor-arrow.svg +1 -0
- package/src/client/shell/hash.ts +14 -3
- package/src/client/shell/icons.tsx +6 -0
- package/src/client/shell/labels.ts +10 -0
- package/src/client/shell/perf.ts +92 -0
- package/src/client/shell/store.ts +101 -6
- package/src/client/shell/styles.css +196 -7
- package/src/client/stage/main.tsx +2 -0
- package/src/shared/events.ts +103 -0
- package/templates/AGENTS-embedded.md +13 -1
- package/templates/AGENTS-studio.md +13 -1
- package/templates/instructions/boards.md +6 -3
- package/templates/instructions/configure.md +28 -0
- package/templates/instructions/iterate.md +38 -0
- package/templates/instructions/publish.md +129 -0
- package/templates/instructions/reference/color.md +22 -1
- package/templates/instructions/shape.md +23 -16
- package/dist/dev-C2oTKuXe.mjs +0 -97
- package/dist/serve-OtA9Nlow.mjs +0 -207
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Publish - put the canvas on a URL, collaboration included
|
|
2
|
+
|
|
3
|
+
Publishing is three decisions, then one build and one process. A coding agent can
|
|
4
|
+
run the whole thing; nothing here needs a human at a dashboard except pasting env
|
|
5
|
+
vars if the host has no CLI.
|
|
6
|
+
|
|
7
|
+
## The three decisions
|
|
8
|
+
|
|
9
|
+
1. **Which boards ship, with which rights** - `design/publish.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "boards": { "release-review": "comment", "roadmap": "read" } }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`read` = visible to anyone past the gate. `comment` = signed-in accounts can
|
|
16
|
+
also pin threads. Boards not listed do not ship at all - their frames are not
|
|
17
|
+
even in the bundle. `marver build` FAILS without this file (default-closed);
|
|
18
|
+
`--boards a,b` overrides ad hoc (grants comment), `--all-boards` ships
|
|
19
|
+
everything loudly.
|
|
20
|
+
|
|
21
|
+
2. **The canvas password** - `MARVER_PASSWORD` at serve time. This is the READ
|
|
22
|
+
boundary: one shared password for GUESTS. Members never need it - their own
|
|
23
|
+
account signs them in at the gate, and invite links skip it entirely (the
|
|
24
|
+
token is the authorization). Rotating it therefore only affects guests.
|
|
25
|
+
No env var = an open canvas.
|
|
26
|
+
|
|
27
|
+
3. **Collaboration on or off** - `MARVER_DATA_DIR` at serve time. Set it to a
|
|
28
|
+
path on a PERSISTENT disk and the serve grows accounts + live comments.
|
|
29
|
+
Leave it unset for a static, comment-free canvas. Setting it to ephemeral
|
|
30
|
+
container disk is the one real deploy mistake - comments would vanish on
|
|
31
|
+
redeploy; marver fails loudly if the dir cannot be created.
|
|
32
|
+
|
|
33
|
+
## The serve contract (env vars, complete list)
|
|
34
|
+
|
|
35
|
+
| Var | Meaning |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `PORT` | listen port (hosts inject this) |
|
|
38
|
+
| `MARVER_PASSWORD` | canvas password (guests' read credential); unset = open |
|
|
39
|
+
| `MARVER_DATA_DIR` | persistent dir for `comments/` + `auth.json`; unset = no collaboration |
|
|
40
|
+
| `MARVER_OWNER_EMAIL` | prints a single-use OWNER claim link in the deploy logs on first boot |
|
|
41
|
+
| `MARVER_TRUSTED_PROXY` | set to `1` behind a reverse proxy (Railway, Fly) so rate limits see real IPs |
|
|
42
|
+
|
|
43
|
+
## The host contract (works on any volume-capable host)
|
|
44
|
+
|
|
45
|
+
The host does two things, and the deploy config names both. **`design/.dist` is
|
|
46
|
+
gitignored - it is built ON THE HOST at deploy time, never committed.**
|
|
47
|
+
|
|
48
|
+
- **build command**: `<install> && npx marver build` (respects `publish.json`,
|
|
49
|
+
seeds comment logs into the bundle)
|
|
50
|
+
- **start command**: `npx marver serve` (reads `PORT` + the env vars above)
|
|
51
|
+
- a **persistent volume** mounted at some path, named by `MARVER_DATA_DIR`
|
|
52
|
+
|
|
53
|
+
The `@marver-design/marver` dependency must resolve from the registry (a local
|
|
54
|
+
`link:`/`file:` dep cannot ride to a remote host) - a normal `^0.4.0` in
|
|
55
|
+
`package.json` is all it takes.
|
|
56
|
+
|
|
57
|
+
## Railway quickstart
|
|
58
|
+
|
|
59
|
+
Commit a `railway.json` so `railway up` knows how to build and serve:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"$schema": "https://railway.com/railway.schema.json",
|
|
64
|
+
"build": { "builder": "NIXPACKS", "buildCommand": "pnpm install && npx marver build" },
|
|
65
|
+
"deploy": { "startCommand": "npx marver serve" }
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then, once per service:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
railway init # or `railway link` an existing service
|
|
73
|
+
railway volume add --mount-path /data
|
|
74
|
+
railway variables --set MARVER_PASSWORD=<pw> --set MARVER_DATA_DIR=/data \
|
|
75
|
+
--set MARVER_OWNER_EMAIL=<owner@email> --set MARVER_TRUSTED_PROXY=1
|
|
76
|
+
railway up # uploads the repo; Railway runs build then start
|
|
77
|
+
railway logs # ← the owner claim LINK prints here, once (see below)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Republishing is just `railway up` again: the server unions the seeded logs on
|
|
81
|
+
boot, so collected feedback is NEVER clobbered by a new build. Run ONE instance -
|
|
82
|
+
the event log is single-writer by design.
|
|
83
|
+
|
|
84
|
+
## What the deployed gate offers (so you know what you're wiring)
|
|
85
|
+
|
|
86
|
+
On a collaboration canvas the gate has three doors, one credential each:
|
|
87
|
+
- **Guest** - the canvas password → read-only across published boards.
|
|
88
|
+
- **Member** - "Sign in instead" → their own email + password → read + comment.
|
|
89
|
+
A member session IS gate passage; they never touch the shared password again.
|
|
90
|
+
- **Invited** - opening an invite link → set a display name + password (+ optional
|
|
91
|
+
avatar) → account created, read + comment. The link skips the canvas password.
|
|
92
|
+
|
|
93
|
+
## Wiring people up (after first deploy)
|
|
94
|
+
|
|
95
|
+
`MARVER_OWNER_EMAIL` makes the first boot print an owner bootstrap in the logs:
|
|
96
|
+
a browser link (`<url>/#/i/<token>`) AND the exact repo command. The token is
|
|
97
|
+
single-use. Claim it from the repo so this machine can mint invites:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# 1. claim the owner account - copy the command the deploy logs printed:
|
|
101
|
+
marver comments connect https://canvas.example.com --invite <token-from-logs>
|
|
102
|
+
|
|
103
|
+
# 2. invite each colleague - one command, one link, no email infrastructure
|
|
104
|
+
marver comments invite colleague@company.com
|
|
105
|
+
# → prints a single invite LINK (<url>/#/i/<token>). Send it over Slack/DM
|
|
106
|
+
# with the canvas password. It opens straight into the claim (name +
|
|
107
|
+
# password + optional avatar). Single-use; 7-day expiry.
|
|
108
|
+
|
|
109
|
+
# 3. the loop is now closed
|
|
110
|
+
marver dev # two-way syncs comments every 30s
|
|
111
|
+
marver comments list --open # the agent's work queue
|
|
112
|
+
marver comments reply <thread> --body "..." # answer in the loop
|
|
113
|
+
marver comments resolve <thread> --addressed-in <frame> # close with a receipt
|
|
114
|
+
marver comments revoke <email> # kills the account + its sessions, mid-flight
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Share the canvas as `<url>/#/b/<board>` (with the canvas password), or copy-link
|
|
118
|
+
on any thread for a deep link straight to it - the gate carries deep links
|
|
119
|
+
through sign-in.
|
|
120
|
+
|
|
121
|
+
## What lands where (so you can reason about persistence)
|
|
122
|
+
|
|
123
|
+
- `design/publish.json` - the policy, git-tracked, part of the repo.
|
|
124
|
+
- `<MARVER_DATA_DIR>/comments/<board>.jsonl` - the live event log, on the volume.
|
|
125
|
+
- `<MARVER_DATA_DIR>/auth.json` - accounts (scrypt), sessions, invites, on the volume.
|
|
126
|
+
- `design/comments/<board>.jsonl` - the dev-side mirror, git-tracked: feedback
|
|
127
|
+
has history, and the volume has an off-site replica for free.
|
|
128
|
+
- `design/.local/collab.json` - THIS machine's device credential. Gitignored;
|
|
129
|
+
never commit it.
|
|
@@ -46,8 +46,29 @@ present). Focus indicators need contrast AND a clearly visible change of appeara
|
|
|
46
46
|
Check interactive states, text over images, and BOTH themes.
|
|
47
47
|
Anything conveyed by color alone also needs text, shape, icon, or position.
|
|
48
48
|
|
|
49
|
+
## Content-frame families (one palette, prose + diagrams)
|
|
50
|
+
|
|
51
|
+
In content frames (`Md`, `Diagram`), color-code by MEANING using the built-in family
|
|
52
|
+
names - never hand-roll hex or mermaid `classDef`. The same six families work in both,
|
|
53
|
+
so a sentence and the diagram beside it read as one color language:
|
|
54
|
+
|
|
55
|
+
- **Prose:** `:blue[the shipper's world]`, `:orange[the carrier market]`, `:purple[the
|
|
56
|
+
driver pool]`, `:green[...]`, `:red[...]`, `:gray[...]` inside any `Md` block.
|
|
57
|
+
- **Diagram nodes:** tag a node with the family - `HQ:::blue`, `Carriers:::orange`,
|
|
58
|
+
`Drivers:::purple`. No `classDef` needed; they're injected for you.
|
|
59
|
+
|
|
60
|
+
Pick ONE family per concept and hold it everywhere it appears (the intro word, the
|
|
61
|
+
diagram box, the section heading). That consistency is what lets a reader link the
|
|
62
|
+
picture to the prose at a glance. Reserve `gray` for the neutral/background ("the platform",
|
|
63
|
+
"out of scope"); use the vivid families for the actors that matter.
|
|
64
|
+
|
|
65
|
+
Node text: write labels as `Head :: gloss` - marver renders the head bold on top
|
|
66
|
+
with the gloss lighter and smaller below, so a box scans as label-then-detail,
|
|
67
|
+
never a run-on. Just the ` :: ` token; no backticks or `**` needed.
|
|
68
|
+
|
|
49
69
|
## Verify
|
|
50
70
|
|
|
51
71
|
Every color has a stable role; attention lands on the intended action; the palette
|
|
52
72
|
holds across quiet, dense, error, and empty states; both themes are composed; the
|
|
53
|
-
result is recognizably THIS product, not a generic colorful treatment.
|
|
73
|
+
result is recognizably THIS product, not a generic colorful treatment. In content
|
|
74
|
+
frames, each concept keeps ONE family across prose and diagram.
|
|
@@ -110,27 +110,34 @@ a quadrant for prioritization, a state diagram for lifecycle logic, a sequence
|
|
|
110
110
|
for API choreography. The syntax reference is the Mermaid docs:
|
|
111
111
|
https://mermaid.js.org/intro/ - pull the one page you need, apply, return.
|
|
112
112
|
|
|
113
|
-
**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
**Two pieces of built-in sugar make a flowchart read well with zero fiddling -
|
|
114
|
+
use them, don't hand-roll their raw mermaid equivalents.**
|
|
115
|
+
|
|
116
|
+
*Label hierarchy (`::`).* Write a node label as `Head :: gloss` and marver
|
|
117
|
+
renders the **head bold** on top with the gloss on a lighter, smaller line
|
|
118
|
+
below - no backticks, no `**`, no `<br>`. A box should scan as label-then-
|
|
119
|
+
detail, so lead with the actor and let the example follow:
|
|
118
120
|
|
|
119
121
|
```
|
|
120
122
|
flowchart LR
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
class C win
|
|
126
|
-
class D stop
|
|
123
|
+
S["Shipper :: the company that needs freight moved"]:::blue
|
|
124
|
+
C["Carrier :: the trucking company that hauls it"]:::orange
|
|
125
|
+
D["Driver :: the person behind the wheel"]:::purple
|
|
126
|
+
S --> C --> D
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
*Family colors (`:::name`).* Tag a node with a built-in family and it gets a
|
|
130
|
+
filled, on-brand color with a legible border in both themes - no `classDef`.
|
|
131
|
+
The SAME six names work in `Md` prose (`:blue[the shipper's world]`), so a
|
|
132
|
+
sentence and the diagram beside it read as one color language:
|
|
133
|
+
`blue orange purple green red gray`. Pick ONE family per concept and hold it
|
|
134
|
+
everywhere; reserve `gray` for the neutral/background actor. Never encode
|
|
135
|
+
meaning in color ALONE - the label carries it too.
|
|
136
|
+
|
|
137
|
+
The marver theme already accent-washes every default node (both modes), so a
|
|
138
|
+
plain flowchart looks designed with zero effort - never hand-set grays "to be
|
|
139
|
+
safe" and never re-theme (init directives are stripped anyway). Check in BOTH
|
|
140
|
+
themes (`d`).
|
|
134
141
|
|
|
135
142
|
## Images and mood boards
|
|
136
143
|
|
package/dist/dev-C2oTKuXe.mjs
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
|
-
import { a as loadConfig, o as detectHost } from "./manifest-DW-T52MM.mjs";
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-Crp4CAma.mjs";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
|
|
7
|
-
import react from "@vitejs/plugin-react";
|
|
8
|
-
//#region src/server/dev.ts
|
|
9
|
-
/** packageDir = the installed marver package root (dist/cli.js lives one level down). */
|
|
10
|
-
function packageDir() {
|
|
11
|
-
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
-
}
|
|
13
|
-
async function dev(root, portFlag) {
|
|
14
|
-
const config = await loadConfig(root);
|
|
15
|
-
const host = detectHost(root);
|
|
16
|
-
const pkgDir = packageDir();
|
|
17
|
-
const clientDir = join(pkgDir, "src", "client");
|
|
18
|
-
const plugins = [react()];
|
|
19
|
-
if (host.tailwind === 4) {
|
|
20
|
-
const tw = await tailwind4Plugin(root);
|
|
21
|
-
if (tw) plugins.push(...tw);
|
|
22
|
-
else console.warn(`[marver] tailwindcss v4 detected but @tailwindcss/vite could not be loaded - utility classes WILL be missing from frames. Fix: npm i -D @tailwindcss/vite (used only by the canvas, never by your app's build).`);
|
|
23
|
-
}
|
|
24
|
-
let css;
|
|
25
|
-
if (host.tailwind === 3) css = await tailwind3Css(root) ?? void 0;
|
|
26
|
-
plugins.push(marverPlugin({
|
|
27
|
-
root,
|
|
28
|
-
clientDir,
|
|
29
|
-
config,
|
|
30
|
-
detectedThemeCss: host.themeCss
|
|
31
|
-
}));
|
|
32
|
-
const logger = createLogger("info");
|
|
33
|
-
const noise = (msg) => /Sourcemap for .*node_modules.*points to/.test(msg);
|
|
34
|
-
const warnBase = logger.warn.bind(logger);
|
|
35
|
-
const warnOnceBase = logger.warnOnce.bind(logger);
|
|
36
|
-
logger.warn = (msg, opts) => {
|
|
37
|
-
if (!noise(msg)) warnBase(msg, opts);
|
|
38
|
-
};
|
|
39
|
-
logger.warnOnce = (msg, opts) => {
|
|
40
|
-
if (!noise(msg)) warnOnceBase(msg, opts);
|
|
41
|
-
};
|
|
42
|
-
const server = await createServer({
|
|
43
|
-
configFile: false,
|
|
44
|
-
root,
|
|
45
|
-
css,
|
|
46
|
-
customLogger: logger,
|
|
47
|
-
plugins,
|
|
48
|
-
server: {
|
|
49
|
-
port: portFlag ?? config.port,
|
|
50
|
-
strictPort: false,
|
|
51
|
-
fs: { allow: [.../* @__PURE__ */ new Set([
|
|
52
|
-
root,
|
|
53
|
-
pkgDir,
|
|
54
|
-
searchForWorkspaceRoot(root)
|
|
55
|
-
])] },
|
|
56
|
-
watch: { ignored: [
|
|
57
|
-
"**/design/manifest.json",
|
|
58
|
-
"**/design/boards/**",
|
|
59
|
-
"**/design/.local/**",
|
|
60
|
-
"**/design/.dist/**",
|
|
61
|
-
"**/.next/**",
|
|
62
|
-
"**/.turbo/**",
|
|
63
|
-
"**/.vercel/**",
|
|
64
|
-
"**/.output/**",
|
|
65
|
-
"**/dist/**",
|
|
66
|
-
"**/build/**",
|
|
67
|
-
"**/out/**",
|
|
68
|
-
"**/coverage/**"
|
|
69
|
-
] }
|
|
70
|
-
},
|
|
71
|
-
resolve: {
|
|
72
|
-
dedupe: ["react", "react-dom"],
|
|
73
|
-
tsconfigPaths: true
|
|
74
|
-
},
|
|
75
|
-
optimizeDeps: {
|
|
76
|
-
exclude: [PKG],
|
|
77
|
-
include: [
|
|
78
|
-
"react",
|
|
79
|
-
"react-dom",
|
|
80
|
-
"react-dom/client",
|
|
81
|
-
"react/jsx-runtime",
|
|
82
|
-
"react/jsx-dev-runtime",
|
|
83
|
-
`${PKG} > marked`,
|
|
84
|
-
`${PKG} > mermaid`
|
|
85
|
-
],
|
|
86
|
-
entries: [join(clientDir, "frame-host", "index.html"), "design/**/*.{tsx,jsx}"]
|
|
87
|
-
},
|
|
88
|
-
logLevel: "info"
|
|
89
|
-
});
|
|
90
|
-
await server.listen();
|
|
91
|
-
const addr = server.httpServer?.address();
|
|
92
|
-
const port = typeof addr === "object" && addr ? addr.port : config.port;
|
|
93
|
-
console.log(`\n ${NAME} canvas → http://localhost:${port}/\n`);
|
|
94
|
-
return server;
|
|
95
|
-
}
|
|
96
|
-
//#endregion
|
|
97
|
-
export { dev };
|
package/dist/serve-OtA9Nlow.mjs
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
-
import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import { createHmac, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
5
|
-
import { createServer } from "node:http";
|
|
6
|
-
//#region src/server/serve.ts
|
|
7
|
-
/**
|
|
8
|
-
* `marver serve` - a zero-dependency static server for design/.dist with an optional
|
|
9
|
-
* password gate (SPEC-M2 §4b). No MARVER_PASSWORD → plain static serving. With it, the
|
|
10
|
-
* bundle is never sent pre-auth: every unauthenticated request gets the gate page, auth
|
|
11
|
-
* is a POST compare + an HMAC-signed 30-day cookie keyed off the password itself (all
|
|
12
|
-
* instances agree, nothing stored server-side).
|
|
13
|
-
*/
|
|
14
|
-
const MIME = {
|
|
15
|
-
".html": "text/html; charset=utf-8",
|
|
16
|
-
".js": "text/javascript",
|
|
17
|
-
".mjs": "text/javascript",
|
|
18
|
-
".css": "text/css",
|
|
19
|
-
".json": "application/json",
|
|
20
|
-
".svg": "image/svg+xml",
|
|
21
|
-
".png": "image/png",
|
|
22
|
-
".ico": "image/x-icon",
|
|
23
|
-
".webmanifest": "application/manifest+json",
|
|
24
|
-
".woff": "font/woff",
|
|
25
|
-
".woff2": "font/woff2",
|
|
26
|
-
".map": "application/json",
|
|
27
|
-
".txt": "text/plain"
|
|
28
|
-
};
|
|
29
|
-
const COOKIE = "mv_a";
|
|
30
|
-
const MONTH = 2592e3;
|
|
31
|
-
function serve(root, portFlag) {
|
|
32
|
-
const dist = join(root, "design", ".dist");
|
|
33
|
-
if (!existsSync(join(dist, "index.html"))) {
|
|
34
|
-
console.error(`[${NAME}] design/.dist not found - run \`npx ${NAME} build\` first.`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
const realDist = realpathSync(dist);
|
|
38
|
-
let meta = {
|
|
39
|
-
name: "Marver",
|
|
40
|
-
branding: true
|
|
41
|
-
};
|
|
42
|
-
try {
|
|
43
|
-
meta = {
|
|
44
|
-
...meta,
|
|
45
|
-
...JSON.parse(readFileSync(join(dist, "meta.json"), "utf8"))
|
|
46
|
-
};
|
|
47
|
-
} catch {}
|
|
48
|
-
const password = process.env.MARVER_PASSWORD ?? "";
|
|
49
|
-
const verifier = password ? scryptSync(password, "marver-gate", 32) : null;
|
|
50
|
-
const cookieKey = randomBytes(32);
|
|
51
|
-
const sign = (exp) => createHmac("sha256", cookieKey).update(`v1.${exp}`).digest("hex");
|
|
52
|
-
const authed = (req) => {
|
|
53
|
-
const m = /(?:^|;\s*)mv_a=(\d+)\.([0-9a-f]+)/.exec(String(req.headers.cookie ?? ""));
|
|
54
|
-
if (!m) return false;
|
|
55
|
-
const exp = Number(m[1]);
|
|
56
|
-
if (!Number.isFinite(exp) || exp * 1e3 < Date.now()) return false;
|
|
57
|
-
const want = Buffer.from(sign(exp)), got = Buffer.from(m[2]);
|
|
58
|
-
return want.length === got.length && timingSafeEqual(want, got);
|
|
59
|
-
};
|
|
60
|
-
const server = createServer((req, res) => {
|
|
61
|
-
const url = new URL(req.url ?? "/", "http://x");
|
|
62
|
-
if (verifier) {
|
|
63
|
-
if (req.method === "POST" && url.pathname === "/__mv/auth") {
|
|
64
|
-
let body = "";
|
|
65
|
-
req.on("data", (c) => {
|
|
66
|
-
body += c;
|
|
67
|
-
if (body.length > 1e4) req.destroy();
|
|
68
|
-
});
|
|
69
|
-
req.on("end", () => {
|
|
70
|
-
const form = new URLSearchParams(body);
|
|
71
|
-
const given = form.get("password") ?? "";
|
|
72
|
-
if (timingSafeEqual(scryptSync(given, "marver-gate", 32), verifier)) {
|
|
73
|
-
const exp = Math.floor(Date.now() / 1e3) + MONTH;
|
|
74
|
-
const secure = req.headers["x-forwarded-proto"] === "https" ? "; Secure" : "";
|
|
75
|
-
res.setHeader("set-cookie", `${COOKIE}=${exp}.${sign(exp)}; Path=/; Max-Age=${MONTH}; HttpOnly; SameSite=Lax${secure}`);
|
|
76
|
-
const next = form.get("next") ?? "";
|
|
77
|
-
res.statusCode = 303;
|
|
78
|
-
res.setHeader("location", /^#\/[\w\/?&=%.,~-]*$/.test(next) ? `/${next}` : "/");
|
|
79
|
-
return res.end();
|
|
80
|
-
}
|
|
81
|
-
return gate(res, meta, "Wrong password - try again");
|
|
82
|
-
});
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const cosmetic = url.pathname.startsWith("/__mv/favicon/") || /^\/__mv\/logo\.(svg|png)$/.test(url.pathname);
|
|
86
|
-
if (!authed(req) && !cosmetic) return gate(res, meta);
|
|
87
|
-
}
|
|
88
|
-
let path;
|
|
89
|
-
try {
|
|
90
|
-
path = decodeURIComponent(url.pathname);
|
|
91
|
-
} catch {
|
|
92
|
-
res.statusCode = 400;
|
|
93
|
-
return res.end("bad request");
|
|
94
|
-
}
|
|
95
|
-
if (path.endsWith("/")) path += "index.html";
|
|
96
|
-
let file = resolve(dist, path.slice(1));
|
|
97
|
-
try {
|
|
98
|
-
const real = realpathSync(file);
|
|
99
|
-
const rel = relative(realDist, real);
|
|
100
|
-
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("outside dist");
|
|
101
|
-
file = real;
|
|
102
|
-
} catch {
|
|
103
|
-
file = join(dist, "index.html");
|
|
104
|
-
}
|
|
105
|
-
if (!extname(file)) file = join(dist, "index.html");
|
|
106
|
-
try {
|
|
107
|
-
const content = readFileSync(file);
|
|
108
|
-
res.setHeader("content-type", MIME[extname(file)] ?? "application/octet-stream");
|
|
109
|
-
const cache = verifier ? "private, no-store" : file.startsWith(join(dist, "assets")) ? "public, max-age=31536000, immutable" : "no-store";
|
|
110
|
-
res.setHeader("cache-control", cache);
|
|
111
|
-
res.end(content);
|
|
112
|
-
} catch {
|
|
113
|
-
res.statusCode = 404;
|
|
114
|
-
res.end("not found");
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
const port = portFlag ?? (Number(process.env.PORT) || 4199);
|
|
118
|
-
server.on("error", (err) => {
|
|
119
|
-
if (err.code === "EADDRINUSE") {
|
|
120
|
-
console.error(`[${NAME}] port ${port} is already in use - another \`${NAME} serve\` still running? Stop it, or pass --port <n>.`);
|
|
121
|
-
process.exit(1);
|
|
122
|
-
}
|
|
123
|
-
throw err;
|
|
124
|
-
});
|
|
125
|
-
server.listen(port, () => {
|
|
126
|
-
console.log(`\n ${NAME} serving design/.dist → http://localhost:${port}/`);
|
|
127
|
-
console.log(verifier ? " gate: ON (MARVER_PASSWORD set)\n" : " gate: off - set MARVER_PASSWORD to require a password\n");
|
|
128
|
-
});
|
|
129
|
-
return server;
|
|
130
|
-
}
|
|
131
|
-
/** The Marver logo mark (ParallelogramDuo, same as the shell's sidebar). */
|
|
132
|
-
const MARK_AT = (size) => `<svg viewBox="0 0 256 256" width="${size}" height="${size}" fill="currentColor" aria-hidden><path d="M239.29,59.28l-64.8,144a8,8,0,0,1-7.3,4.72H24a8,8,0,0,1-7.3-11.28l64.8-144A8,8,0,0,1,88.81,48H232A8,8,0,0,1,239.29,59.28Z" opacity=".1"/><path d="M245.43,47.31A15.94,15.94,0,0,0,232,40H88.81a16,16,0,0,0-14.59,9.43l-64.8,144A16,16,0,0,0,24,216H167.19a16,16,0,0,0,14.59-9.43l64.8-144A16,16,0,0,0,245.43,47.31ZM167.19,200H24L88.81,56H232Z"/></svg>`;
|
|
133
|
-
const MARK = MARK_AT(17);
|
|
134
|
-
const MARK_LG = MARK_AT(24);
|
|
135
|
-
/** The gate: the canvas's own antechamber - light ground, dot grid, glass card in the
|
|
136
|
-
* shell's exact token language. The visitor is one password away from stepping in. */
|
|
137
|
-
function gate(res, meta, error) {
|
|
138
|
-
const name = meta.name ? meta.name[0].toUpperCase() + meta.name.slice(1) : "Marver";
|
|
139
|
-
const appMark = meta.logo ? `<img src="${esc(meta.logo)}" alt="" width="24" height="24" />` : MARK_LG;
|
|
140
|
-
const title = meta.branding ? `${name} | Marver - Visualize your software` : name;
|
|
141
|
-
const desc = meta.branding ? `${name}, shared as a live Marver canvas - real screens and prototypes, built from the codebase. Marver is the agent-native design canvas.` : `${name} - a private design canvas.`;
|
|
142
|
-
res.statusCode = 200;
|
|
143
|
-
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
144
|
-
res.setHeader("cache-control", "no-store");
|
|
145
|
-
res.end(`<!doctype html>
|
|
146
|
-
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
147
|
-
<title>${esc(title)}</title>
|
|
148
|
-
<meta name="description" content="${esc(desc)}" />
|
|
149
|
-
<meta name="robots" content="noindex" />
|
|
150
|
-
<meta name="theme-color" content="#e7e9ef" />
|
|
151
|
-
<meta property="og:title" content="${esc(title)}" />
|
|
152
|
-
<meta property="og:description" content="${esc(desc)}" />
|
|
153
|
-
${meta.branding ? "<meta property=\"og:site_name\" content=\"Marver\" />" : ""}
|
|
154
|
-
<meta property="og:type" content="website" />
|
|
155
|
-
<meta name="twitter:card" content="summary" />
|
|
156
|
-
<link rel="icon" href="/__mv/favicon/favicon.ico" sizes="48x48" />
|
|
157
|
-
<link rel="icon" type="image/png" sizes="32x32" href="/__mv/favicon/favicon-32x32.png" />
|
|
158
|
-
<link rel="icon" type="image/png" sizes="16x16" href="/__mv/favicon/favicon-16x16.png" />
|
|
159
|
-
<link rel="apple-touch-icon" href="/__mv/favicon/apple-touch-icon.png" />
|
|
160
|
-
<style>
|
|
161
|
-
* { box-sizing: border-box; margin: 0 }
|
|
162
|
-
body { min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
163
|
-
background-color: #e7e9ef;
|
|
164
|
-
background-image: radial-gradient(#c9cbd5 1px, transparent 1px); background-size: 20px 20px;
|
|
165
|
-
font: 500 14px -apple-system, system-ui, sans-serif; color: #18181b;
|
|
166
|
-
-webkit-font-smoothing: antialiased }
|
|
167
|
-
main { display: flex; flex-direction: column; align-items: center; gap: 22px; padding: 16px }
|
|
168
|
-
form { width: 340px; padding: 24px; border-radius: 24px; display: flex; flex-direction: column; gap: 14px;
|
|
169
|
-
background: rgba(255, 255, 255, .64); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
|
170
|
-
border: 1px solid rgba(24, 24, 27, .1);
|
|
171
|
-
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .65), 0 1px 2px rgba(24, 24, 27, .05), 0 4px 12px -6px rgba(24, 24, 27, .12) }
|
|
172
|
-
header { display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 4px }
|
|
173
|
-
header svg, header img { color: #0088ff; flex: none; border-radius: 6px }
|
|
174
|
-
h1 { font-size: 17px; font-weight: 600; letter-spacing: -.01em }
|
|
175
|
-
p { font-size: 12.5px; line-height: 1.5; color: rgba(24, 24, 27, .66); padding: 0 4px }
|
|
176
|
-
input { height: 40px; padding: 0 16px; border-radius: 999px; border: 1px solid rgba(24, 24, 27, .14);
|
|
177
|
-
background: #fff; color: #18181b; font: inherit; outline: none; transition: border-color .15s, box-shadow .15s }
|
|
178
|
-
input::placeholder { color: rgba(24, 24, 27, .35) }
|
|
179
|
-
input:focus { border-color: #0088ff; box-shadow: 0 0 0 3px rgba(0, 136, 255, .18) }
|
|
180
|
-
button { height: 40px; border: 0; border-radius: 999px; background: #18181b; color: #fafafa;
|
|
181
|
-
font: 600 13px -apple-system, system-ui, sans-serif; cursor: pointer; transition: background .15s }
|
|
182
|
-
button:hover { background: #000 }
|
|
183
|
-
.err { font-size: 12px; color: #b42318; padding: 0 4px }
|
|
184
|
-
footer a { display: inline-flex; align-items: center; gap: 7px; font: 600 12.5px -apple-system, system-ui, sans-serif;
|
|
185
|
-
color: rgba(24, 24, 27, .55); text-decoration: none }
|
|
186
|
-
footer a > svg:first-of-type { width: 20px; height: 20px; color: #0088ff }
|
|
187
|
-
footer .md, footer .up { transition: color .15s, opacity .15s }
|
|
188
|
-
footer a:hover .md { color: #0088ff; text-decoration: underline; text-underline-offset: 3px }
|
|
189
|
-
footer .up { opacity: .45; margin-left: -3px }
|
|
190
|
-
footer a:hover .up { opacity: 1; color: #0088ff }
|
|
191
|
-
</style></head>
|
|
192
|
-
<body><main>
|
|
193
|
-
<form method="post" action="/__mv/auth">
|
|
194
|
-
<header>${appMark}<h1>${esc(name)}</h1></header>
|
|
195
|
-
<p>You're one step from the canvas. This space is private - enter the password to step inside.</p>
|
|
196
|
-
${error ? `<div class="err">${esc(error)}</div>` : ""}
|
|
197
|
-
<input type="password" name="password" placeholder="Password" autofocus autocomplete="current-password" />
|
|
198
|
-
<input type="hidden" name="next" />
|
|
199
|
-
<button type="submit">Open the canvas</button>
|
|
200
|
-
</form>
|
|
201
|
-
<script>document.querySelector('[name=next]').value = location.hash<\/script>
|
|
202
|
-
${meta.branding ? `<footer><a href="https://marver.design" target="_blank" rel="noopener">${MARK} <span>Powered by <span class="md">Marver.design</span></span> <svg class="up" viewBox="0 0 256 256" width="11" height="11" fill="currentColor" aria-hidden><path d="M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z"/></svg></a></footer>` : ""}
|
|
203
|
-
</main></body></html>`);
|
|
204
|
-
}
|
|
205
|
-
const esc = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
206
|
-
//#endregion
|
|
207
|
-
export { serve };
|