@marver-design/marver 0.13.0 → 0.14.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +41 -19
  3. package/dist/{build-BxGrHFT2.mjs → build-ByYafIhj.mjs} +37 -5
  4. package/dist/cli.mjs +19 -5
  5. package/dist/{daemon-BChkzDqQ.mjs → daemon-Bfucyf1o.mjs} +1 -1
  6. package/dist/{dev-DLwt3Brb.mjs → dev-yZMyQeUj.mjs} +4 -4
  7. package/dist/{init-BpitOqRQ.mjs → init-Dvaso7YO.mjs} +66 -2
  8. package/dist/{manifest-CS6krOTe.mjs → manifest-DvOmglFp.mjs} +7 -0
  9. package/dist/{marver-id-gate-B2uraTHS.mjs → marver-id-gate-D6By7XHj.mjs} +1 -1
  10. package/dist/{plugin-DNc4Jpae.mjs → plugin-BsmG5i2X.mjs} +21 -3
  11. package/dist/{serve-EjEqsiYa.mjs → serve-Bcwfpvhl.mjs} +3 -2
  12. package/dist/{shot-Cyv3GN79.mjs → shot-kbR_xzJH.mjs} +7 -1
  13. package/docs/live-jam.md +173 -0
  14. package/docs/publish.md +270 -0
  15. package/docs/sharing.md +333 -0
  16. package/docs/slides.md +134 -0
  17. package/package.json +3 -1
  18. package/src/client/const.ts +13 -0
  19. package/src/client/content/chart-engine.ts +33 -0
  20. package/src/client/content/chart.tsx +100 -0
  21. package/src/client/content/index.tsx +25 -4
  22. package/src/client/content/slide.tsx +238 -0
  23. package/src/client/content/video.tsx +126 -0
  24. package/src/client/shell/App.tsx +30 -12
  25. package/src/client/shell/LockedApp.tsx +7 -2
  26. package/src/client/shell/Play.tsx +138 -24
  27. package/src/client/shell/Toolbar.tsx +12 -3
  28. package/src/client/shell/canvas/FrameNode.tsx +5 -3
  29. package/src/client/shell/hash.ts +3 -1
  30. package/src/client/shell/icons.tsx +2 -0
  31. package/src/client/shell/play-order.ts +22 -0
  32. package/src/client/shell/store.ts +29 -8
  33. package/src/client/shell/styles.css +17 -27
  34. package/src/client/stage/main.tsx +54 -3
  35. package/src/shared/utm.ts +3 -2
  36. package/templates/AGENTS-embedded.md +1 -0
  37. package/templates/AGENTS-studio.md +1 -0
  38. package/templates/instructions/publish.md +7 -0
  39. package/templates/instructions/reference/deck-layouts.md +230 -0
  40. package/templates/instructions/reference/deck-story.md +110 -0
  41. package/templates/instructions/slides.md +398 -0
@@ -1,4 +1,4 @@
1
- import { i as ROUTE } from "./cli.mjs";
1
+ import { a as slideSize, i as ROUTE } from "./cli.mjs";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
@@ -38,6 +38,12 @@ const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
38
38
  function planShot(frame, viewports) {
39
39
  const cw = typeof frame.contentWidth === "number" && Number.isFinite(frame.contentWidth) && frame.contentWidth > 0 ? frame.contentWidth : void 0;
40
40
  const vpObj = frame.viewport ? viewports[frame.viewport] : void 0;
41
+ const sl = slideSize(frame);
42
+ if (sl) return {
43
+ width: sl.width,
44
+ initialHeight: sl.height,
45
+ fullHeight: false
46
+ };
41
47
  const fallback = viewports.mobile ?? {
42
48
  width: 390,
43
49
  height: 844
@@ -0,0 +1,173 @@
1
+ # Live Jam
2
+
3
+ Tag `@marver` in a canvas comment while `npx marver dev` is running. The dev server spawns
4
+ your own coding-agent CLI headless with that one job, the frame lights up with a working
5
+ glow, the agent edits the real frame source, and its reply lands back in the same thread.
6
+ No round trip to the terminal. Marver ships no AI: the agent that acts is the one you
7
+ already run and pay for.
8
+
9
+ ## It arms itself
10
+
11
+ Live Jam is on by default (since 0.9.0). Marver speaks seven agent CLIs - `claude`,
12
+ `codex`, `cursor`, `droid` (Factory), `opencode`, `grok`, and `pi` - which also covers the
13
+ apps built on them: Factory drives `droid`, Cursor drives `cursor-agent`, Conductor drives
14
+ `claude`. Marver looks for one in this order:
15
+
16
+ 1. **The tool running the process wins.** Most CLIs export env markers into what they spawn
17
+ (`CLAUDECODE` for Claude Code, `CODEX_SANDBOX` for Codex, `CURSOR_AGENT` for Cursor,
18
+ `OPENCODE` for opencode, `PI_CODING_AGENT` for pi), and `marver init` is usually run by
19
+ the agent itself. That is evidence, not a guess. droid and grok set no marker in the
20
+ shells they spawn, so this step cannot see them - name them in config or let PATH decide.
21
+ 2. **Otherwise, whatever is on PATH**, in the order above - `claude` first.
22
+
23
+ That second step is a guess, so the answer is made visible rather than clever: `init` prints
24
+ the agent it chose and writes it into `design/config.ts` in plain sight, and `marver dev`
25
+ names it at boot (`jam: on (claude)`). One word to correct, once per repo.
26
+
27
+ The candidate has to be executable on `PATH` under its bare name, because the daemon spawns
28
+ it without a shell. A shell alias or function is invisible to it. Cursor is the one agent
29
+ whose binary differs from its config name: marver spawns `cursor-agent`, never the bare
30
+ `agent` - both Cursor and grok install an `agent` name, so the short one is a coin flip.
31
+
32
+ With no agent CLI installed, jam stays off and both `init` and `marver dev` say so instead
33
+ of going quiet. Workspaces created before 0.9.0 need no re-init; they resolve the same way
34
+ at every dev boot.
35
+
36
+ ## The config block
37
+
38
+ ```ts
39
+ // design/config.ts
40
+ jam: { agent: "claude", concurrency: 6 },
41
+ ```
42
+
43
+ | Key | Default | What it does |
44
+ |---|---|---|
45
+ | `agent` | detected | The CLI the daemon spawns: `"claude"`, `"codex"`, `"cursor"`, `"droid"`, `"opencode"`, `"grok"`, or `"pi"` |
46
+ | `concurrency` | `6` | Frames worked on at once (1-16). The same frame never gets two agents |
47
+ | `subagents` | `true` | Inside one job, fan out one subagent per frame |
48
+
49
+ Shorthands: `jam: "codex"` names the agent and takes the rest of the defaults, `jam: true`
50
+ is the default block, and **`jam: false` is the off switch**.
51
+
52
+ A named agent is never quietly swapped for another. If `jam.agent` names something marver
53
+ cannot spawn, or names a CLI that is not on PATH, Live Jam turns off with a printed reason
54
+ rather than answering your comments with a tool you did not choose. A `design/config.ts`
55
+ that fails to parse also leaves jam off, since it may have been the file that said
56
+ `jam: false`.
57
+
58
+ ## What the agent may do
59
+
60
+ Be clear-eyed about what this is. The real protection is not a sandbox - it is that the
61
+ agent doing the work is **your own**, running on **your machine**, and **every change it
62
+ makes is a diff you review** before anything is resolved. On top of that, marver removes the
63
+ one tool that would turn a prompt-injected comment into silent damage: an unrestricted
64
+ **shell**. Each CLI is spawned so the model can read and edit files but cannot open a shell
65
+ (or, for Codex and Cursor, only a shell the OS sandbox contains and cuts off from the
66
+ network):
67
+
68
+ | | How it is spawned |
69
+ |---|---|
70
+ | **Claude Code** | `claude -p --permission-mode acceptEdits` with an allowlist of Read, Edit, Write, Glob, Grep, WebSearch, WebFetch - and `--disallowedTools Bash`, so there is no shell |
71
+ | **Codex** | `codex exec -s workspace-write`, its own OS sandbox, which bounds what commands touch and blocks network egress, but still lets the model run shell commands |
72
+ | **Cursor** | `cursor-agent -p --sandbox enabled --trust` - cursor's print mode carries a shell, so like Codex it runs inside the OS sandbox (verified: network egress is blocked); `--trust` only answers the workspace-trust prompt for the repo you already run `marver dev` in, and `--force` is never passed |
73
+ | **droid** | `droid exec --auto low` for file edits, with `--disabled-tools` removing the shell (`Execute`), the delegation tools (`Task`, missions), and the Slack/connector tools |
74
+ | **opencode** | `opencode run --pure` (no external plugins) with a per-run DEFAULT-DENY `OPENCODE_PERMISSION` grant - read/edit/search/web/task allowed by name, everything else (bash included) denied - never its all-approving `--auto` flag |
75
+ | **grok** | `grok -p --tools read_file,search_replace,list_dir,grep,todo_write` - an ALLOWLIST of read/edit tools only, so the shell, web, and subagents are simply absent (a deny-list is a footgun: it can miss a tool's real name); `--permission-mode acceptEdits` auto-applies the edits |
76
+ | **pi** | `pi -p --tools read,edit,write,grep,find,ls --no-extensions --no-skills` - pi has no runtime permission system, so the tool allowlist IS the jail, and bash is not on it |
77
+
78
+ The honest limits: file tools that take a path (Read/Edit/Write, and their equivalents) are
79
+ not themselves jailed to `design/` - on the CLIs without an OS sandbox they can, if a
80
+ comment talks the model into it, touch a file elsewhere in the repo or the machine. And Web
81
+ access, where a CLI keeps it, can carry data outward. This is the same boundary Claude Code
82
+ has always run under, and it is why the two rules above still do the real work: it is your
83
+ agent, and you review the diff. Do not point Live Jam at a repo, or run it on a machine, you
84
+ would not hand that same agent directly.
85
+
86
+ Web access stays on where the CLI offers it (Claude Code, Codex, opencode): reference sites
87
+ and real brand SVGs are how a frame stops looking like a placeholder. The agent never
88
+ resolves a thread; you do that after reviewing.
89
+
90
+ The missing sense that no-shell used to cost - "does my frame actually RENDER?" - is a
91
+ server capability instead, rendered in the machine's own headless Chrome (no bundled
92
+ browser, CDP over Node's built-in WebSocket) and written as a PNG under
93
+ `design/.local/shots/`. Two transports reach it, because the no-shell jail rules out the
94
+ obvious one:
95
+
96
+ - **The file-drop inbox** (works for every agent, including Claude Code, which has no shell
97
+ and whose WebFetch refuses localhost). The agent writes
98
+ `design/.local/shots/<slug>.request.json` with `{"frame":"<id>","theme":"<t>"}`; the dev
99
+ server renders and writes `<slug>.result.json` with the PNG path or an error, which the
100
+ agent Reads.
101
+ - **`npx marver shot <frame>`** / `GET /api/shot?frame=<id>&theme=<t>` for humans and
102
+ shell-ful agents - the same renderer, one line.
103
+
104
+ The generated jam instructions tell every agent to shoot and LOOK before replying "done".
105
+ A frame that never mounts, or a dev server that isn't reachable, comes back as an honest
106
+ `{"ok":false,"error":...}` carrying the real cause - never a blank that reads as success.
107
+
108
+ One prerequisite marver cannot arrange: **the CLI has to be logged in** (`droid` and
109
+ `cursor-agent login` and `grok login` each have their own flow; opencode and pi can also
110
+ read provider API keys from the environment). An unauthenticated CLI fails the job; the
111
+ daemon retries once, then replies that it could not finish - the dev log and
112
+ `design/.local/jam-logs/` say why.
113
+
114
+ ## The trust boundary
115
+
116
+ Only comments written on the owner's machine can start work. A mention becomes eligible
117
+ solely by way of the local dev server's owner-gated endpoint (a CSRF double-submit cookie
118
+ plus an Origin allowlist), which appends it to `design/.local/jam-ledger`. The daemon runs
119
+ a job only for an event id that is already in that ledger, so a drive-by comment on a
120
+ published canvas cannot trigger one, and neither can a collaborator comment that arrived
121
+ through `marver comments sync`.
122
+
123
+ The ledger and the job journal both carry a device stamp, because gitignore is a convention
124
+ and not provenance: a repo can force-add its own `.local/`. Jam state that arrived with a
125
+ clone is read as absent. The stamp is derived from the machine rather than stored, so
126
+ marver still writes nothing outside `design/`.
127
+
128
+ The larger caution is unchanged and worth stating plainly: `marver dev` imports and executes
129
+ `design/config.ts`, so running a dev server in a repo you do not trust is already running
130
+ that repo's code. Live Jam adds no new hole to that; it does not make it safe.
131
+
132
+ ## Provenance
133
+
134
+ Every jam reply is stamped with who acted: your dev user, the harness that ran, and the
135
+ model when the agent names one. Claude Code, Cursor, droid, grok, and pi report their model
136
+ in the stream; `codex exec` and `opencode run` report none, so their replies carry the
137
+ harness without a model rather than a guessed one.
138
+
139
+ ## Parallelism
140
+
141
+ Two knobs stack, and they are not the same thing. `jam.concurrency` is how many jobs the
142
+ daemon runs at once - different frames, different comments. `jam.subagents` is fan-out
143
+ *inside* one job, one subagent per frame, which is what makes a five-frame ask land
144
+ together instead of in series. Claude Code, Codex, and opencode fan out (opencode's
145
+ subagents verifiably inherit the jail); pi has no subagent tool, and droid and grok have
146
+ theirs removed in the spawn itself (`--disabled-tools Task`, `--no-subagents`) until a
147
+ child is proven to inherit the parent's confinement. Their jobs simply run the frames in
148
+ sequence - the prompt only ever says the agent MAY fan out.
149
+
150
+ Two honest caveats in the same spirit as the config-execution one above: cursor's own
151
+ permission rules (`~/.cursor/cli-config.json`, a repo's `.cursor/cli.json`) and a repo's
152
+ own `opencode.json` agent block can widen what those CLIs allow - that is your
153
+ configuration speaking, and marver does not override it.
154
+
155
+ ## When a mention does nothing
156
+
157
+ - **It has to be your machine, your repo, with `marver dev` running.** That is the trust
158
+ boundary above, working as intended.
159
+ - **The first boot after upgrading to 0.9.0 rebaselines the job journal**, because an
160
+ existing journal predates the device stamp. Any `@marver` left unprocessed while the
161
+ server was down is marked seen instead of run. Re-comment to pick it up.
162
+ - **Check the boot line.** `marver dev` prints `jam: on (<agent>)` when it is armed, and
163
+ prints the reason when it is not.
164
+ - **Read the raw run log.** Every job's full agent output lands in
165
+ `design/.local/jam-logs/` (last 10 kept) - an auth failure or permission refusal
166
+ explains itself there. The generated `design/instructions/jam.md` carries the full
167
+ troubleshooting drill, written for your agent to run: it checks the boot line, the log,
168
+ and the CLI's own headless auth, fixes what belongs to the workspace, and files what
169
+ belongs to marver at [github.com/TNEP4/marver/issues](https://github.com/TNEP4/marver/issues) -
170
+ with the patch, when it found one while debugging.
171
+
172
+ `npx marver comments list --open --json` reads the same threads without the live loop, for
173
+ catching up or for a one-off answer.
@@ -0,0 +1,270 @@
1
+ # Publishing a Marver canvas
2
+
3
+ A published canvas is a static site: `marver build` bundles the shell, the prototype
4
+ stage, and your frames with all data inlined - it fetches nothing, saves nothing, and
5
+ runs on any static host. `marver serve` hosts it, optionally behind a gate - a shared
6
+ password, or Marver Sign In (see [Who can open your canvas](#who-can-open-your-canvas)).
7
+ Give it a persistent volume (`MARVER_DATA_DIR`) and comments + viewer accounts persist
8
+ across deploys (`MARVER_OWNER_EMAIL` bootstraps the first owner account).
9
+
10
+ Publishing is default-closed: `design/publish.json` names the boards that ship and
11
+ their rights (`"read"` or `"comment"`) - no policy and no explicit flag means no build.
12
+ A board row can also be an object that says how the board presents - its `type`
13
+ (`doc`, `slides`, `design`, `sketch`, `refs`, `mix`), the view visitors land in
14
+ (`open`: `canvas`, `board`, `present`, `focus`, `slides`), `lock` to freeze them
15
+ there, and for decks `transition` / `chrome` - the fields are spelled out in the
16
+ [sharing guide](sharing.md#publishjson-v2---the-ceiling-and-the-boards-shape) and,
17
+ for decks, the [slides guide](slides.md).
18
+
19
+ ```bash
20
+ npx marver build # boards named in design/publish.json → design/.dist
21
+ npx marver build --boards checkout # only these boards - the frame filter is applied
22
+ # at BUILD time; excluded frames never enter the bundle
23
+ MARVER_PASSWORD=secret npx marver serve # a shared password, or:
24
+ MARVER_ID_ISSUER=https://id.marver.design \
25
+ MARVER_PUBLIC_ORIGIN=https://your.canvas \
26
+ MARVER_DATA_DIR=/data npx marver serve # accounts, one sign-in across canvases
27
+ ```
28
+
29
+ Deep links work verbatim: any `#/b/...` or `#/p/...` URL copied from your dev canvas
30
+ opens the same view on the published site, as long as its board and frame shipped.
31
+
32
+ **What ships**: the boards you list, the frames they reference, and the host `public/`
33
+ directory in full (the `--boards` filter covers frames, not public assets). A flow you
34
+ publish must have all its `data-goto` targets on a published board.
35
+
36
+ ## Who can open your canvas
37
+
38
+ Three choices, and the canvas is public until you make one.
39
+
40
+ | | **Open** | **Password** | **Marver Sign In** |
41
+ |---|---|---|---|
42
+ | Set | nothing | `MARVER_PASSWORD` | `MARVER_ID_ISSUER` + `MARVER_PUBLIC_ORIGIN` |
43
+ | People prove | nothing | they know a secret | who they are |
44
+ | Named accounts | none | only with `MARVER_DATA_DIR` | always (needs `MARVER_DATA_DIR`) |
45
+ | Who decides entry | - | you | you |
46
+ | Outbound requests | none | none | public keys only |
47
+ | Best for | a public canvas | one link to a small group | a team, across canvases |
48
+
49
+ `MARVER_DATA_DIR` is what turns a gate into accounts. Without it the password gate
50
+ is exactly one shared secret and nothing else - no per-person identity, no comments
51
+ that persist, and nothing for `marver comments invite` to write to. Marver Sign In
52
+ requires it outright, because identity accounts need somewhere to live.
53
+
54
+ They are alternatives, not layers. Setting both `MARVER_PASSWORD` and
55
+ `MARVER_ID_ISSUER` would weaken your invite list to "an account OR whoever has the
56
+ password", so the identity gate replaces the password gate rather than sitting
57
+ beside it.
58
+
59
+ **Whichever you choose, the guest list stays yours.** This is the part worth
60
+ reading twice: with Marver Sign In, the identity service proves *who somebody is*
61
+ and has no say in *where they may go*. Your canvas decides that, from a list it
62
+ holds. Somebody with a perfectly valid Marver account who is not on your list
63
+ gets nothing.
64
+
65
+ And the honest fine print, because this is a promise we publish rather than a
66
+ slogan: Marver stores which canvases a person has *signed in to*, and issues
67
+ short-lived tokens for them. What someone can **do** on your canvas is answered
68
+ by your canvas to their own browser, cached only there, and never sent to us.
69
+ Two things do cross the line, both disclosed and both optional: an invite email
70
+ means the identity service learns that an address was invited to your origin
71
+ (that is what sending the mail requires - decline it entirely with
72
+ `share: { notify: false }` in `design/config.ts` and use the dialog's "copy
73
+ invite message" instead), and the front door at `app.marver.design` learns
74
+ which origins a person probes (`share: { frontDoor: false }` keeps your canvas
75
+ silent there). The app ships no third-party analytics and no summary telemetry.
76
+
77
+ **Sharing, precisely.** Sharing controls who may **comment**, and who gets in
78
+ at all. What a person who is in can **see** is decided at build time by
79
+ `design/publish.json` - boards you do not publish are not in the bundle. One
80
+ canvas per audience is the read boundary today; per-person read arrives in v2,
81
+ served rather than bundled. Managing that roster - granting people, blocking
82
+ them, approving access requests, and reading who-sees-what from the terminal -
83
+ is its own guide: [Sharing a Marver canvas](sharing.md).
84
+
85
+ ### Sovereign accounts (`MARVER_PASSWORD` + `MARVER_DATA_DIR`)
86
+
87
+ ```bash
88
+ MARVER_PASSWORD=secret MARVER_DATA_DIR=/data npx marver serve
89
+ ```
90
+
91
+ Everything stays here. Accounts live in `MARVER_DATA_DIR` as scrypt hashes, invites
92
+ are minted by you, and the canvas makes no outbound request of any kind - there is
93
+ no service to depend on and nothing to phone home to. If you want a canvas that
94
+ still works in ten years on a disconnected network, this is it.
95
+
96
+ `MARVER_PASSWORD` on its own is a simpler thing: one shared secret in front of the
97
+ bundle, with no accounts behind it. Add the volume when you want named people.
98
+
99
+ Auth is an HMAC-signed 30-day cookie with a per-boot secret (a server restart
100
+ re-prompts), and each password attempt pays an scrypt cost. Invite people with
101
+ `marver comments invite <email>`; they claim it in the browser and choose a
102
+ password.
103
+
104
+ **Set `MARVER_PUBLIC_ORIGIN` here too if you serve over https.** It is required for
105
+ Marver Sign In and optional here, but it is what puts `Secure` on that 30-day
106
+ cookie. Without it the canvas has to guess from `X-Forwarded-Proto`, and nginx's
107
+ own documented `proxy_pass http://localhost:PORT` sends no `X-Forwarded-*` at all -
108
+ so an https canvas behind that config drops `Secure` and the cookie will travel
109
+ over plain http. Proxies that do set the header (Railway, Fly, Vercel, Caddy,
110
+ nginx with `proxy_set_header`) were never affected. Fixed in 0.11.1; on 0.11.0 the
111
+ gate cookie guessed regardless.
112
+
113
+ The costs are the ordinary costs of passwords. It is one secret shared by
114
+ everybody, so removing one person means rotating it for all of them; there is no
115
+ password reset; and a person with five canvases keeps five passwords.
116
+
117
+ ### Marver Sign In (`MARVER_ID_ISSUER`) - recommended for teams
118
+
119
+ ```bash
120
+ MARVER_ID_ISSUER=https://id.marver.design \
121
+ MARVER_PUBLIC_ORIGIN=https://your.canvas \
122
+ MARVER_DATA_DIR=/data \
123
+ npx marver serve
124
+ ```
125
+
126
+ The gate asks people who they are instead of asking for a shared secret. They sign
127
+ in once - with Google, or a code emailed to them - and every canvas you gate this
128
+ way opens without another password. Nobody types a canvas password, so there is
129
+ none to leak, rotate, or forget, and revoking one person revokes them.
130
+
131
+ A Marver account is **free**, and there is exactly one of it. That is the point:
132
+ the account is not per canvas, so the second board you share with somebody costs
133
+ them nothing - no signup, no password to store, no invite link to keep. The first
134
+ canvas is where they pay the thirty seconds; every one after that is a click. If
135
+ you have ever watched a review die because a reviewer could not find the link, that
136
+ is the friction this removes.
137
+
138
+ This is the better default for a team, and it is the one we run ourselves. What it
139
+ costs you is honest to state:
140
+
141
+ - **A dependency.** If the identity service is unreachable, sign-in fails closed:
142
+ existing sessions keep working, new ones are refused, nothing falls back to open.
143
+ - **The service learns when somebody signs in**, and to which canvas origin. It
144
+ does not learn whether you let them in, what is on the canvas, or anything else.
145
+ - **It is a hosted service**, so it is the one part of a self-hosted canvas that is
146
+ not self-hosted. The protocol is ordinary ES256 + JWKS and the verifying half
147
+ lives in this repo (`src/server/marver-id.ts`), so a different issuer is a
148
+ configuration change, not a fork.
149
+
150
+ A canvas with no `MARVER_ID_ISSUER` set makes no outbound request at all. Opting
151
+ out is the default, and this section is the only reason to opt in.
152
+
153
+ #### Configuration
154
+
155
+ `MARVER_PUBLIC_ORIGIN` is **required** - always, including in development - and the
156
+ canvas refuses to start without it. Every assertion is bound to this exact origin
157
+ (scheme, host and port), so one minted for one canvas is inert at another.
158
+
159
+ It is configuration rather than inference on purpose, and the reason is worth
160
+ knowing if you deploy behind a proxy. The canvas used to work this out for itself
161
+ when the connection looked local, which is wrong in the most ordinary setup there
162
+ is: nginx's documented `proxy_pass http://localhost:PORT` rewrites `Host` to the
163
+ upstream and adds no `X-Forwarded-*` headers at all, so a request from the open
164
+ internet arrives looking exactly like one from your own machine. There is no signal
165
+ here a proxy cannot erase, so the canvas asks instead of guessing.
166
+
167
+ It must be a bare origin - https anywhere, or http on loopback - with no path or
168
+ query. Cookie security follows it, not any forwarded header:
169
+
170
+ ```bash
171
+ MARVER_PUBLIC_ORIGIN=https://canvas.example.com # deployed
172
+ MARVER_PUBLIC_ORIGIN=http://localhost:4199 # development
173
+ ```
174
+
175
+ #### Who gets in
176
+
177
+ An address may enter if it already has an account on this canvas, holds an
178
+ unexpired invite, or is `MARVER_OWNER_EMAIL` on a canvas with no accounts yet. You
179
+ invite people exactly as before; Marver Sign In only removes the password step from
180
+ claiming it.
181
+
182
+ People are matched on the stable identity behind the address, not the address
183
+ itself, so somebody whose email changes keeps their account and their history. Their
184
+ other sessions are signed out when that happens - a session records the address it
185
+ was minted for, and leaving it alive would hand it to whoever claims that address
186
+ next. A rename onto an address someone else already holds is refused outright.
187
+
188
+ > **Managing people needs `MARVER_CLI_TOKEN`.** `marver comments invite`,
189
+ > `revoke` and `sync` authenticate the CLI with a password, and an identity
190
+ > account has none. Set `MARVER_CLI_TOKEN` on the canvas to a generated secret of
191
+ > 32 characters or more and hand the same value back:
192
+ >
193
+ > ```bash
194
+ > # on the canvas: MARVER_CLI_TOKEN=$(openssl rand -hex 24)
195
+ > MARVER_CLI_TOKEN='<that same value>' marver comments connect https://canvas.example.com
196
+ > ```
197
+ >
198
+ > `--token` works too, but a secret on the command line is visible to anything
199
+ > that can list processes, so prefer the variable.
200
+ >
201
+ > Generate it, do not choose it: nothing rate-limits this credential and nothing
202
+ > slows a guess down, so its entropy is the whole defence. Use hex rather than
203
+ > base64 - an `Authorization` header carries letters, digits, `_` and `-`, and the
204
+ > canvas refuses to start on a value it could never accept. It acts as whoever
205
+ > owns the canvas, so let the owner sign in once first.
206
+ >
207
+ > `connect` trades it for an ordinary session and stores THAT in
208
+ > `~/.marver/canvases/`, so neither the secret nor the session lands in your repo.
209
+ >
210
+ > **To revoke it, rotate `MARVER_CLI_TOKEN`.** Every session it ever minted stops
211
+ > working the moment the variable changes; sessions people hold in their browsers
212
+ > are untouched. `marver comments revoke` cannot help here - the session acts as
213
+ > the owner, and a canvas refuses to remove its last owner - so rotation is the
214
+ > lever, and it is the reason each device session records which secret minted it.
215
+ > (One instance at a time, as ever: during a rolling restart an old replica still
216
+ > honours old sessions until it drains.)
217
+ >
218
+ > It is a deployment variable rather than something a page hands out, and that is
219
+ > deliberate. A browser-approved sign-in for the CLI was built for this and then
220
+ > removed before release: authored frames run same-origin in a canvas, so frame
221
+ > JavaScript could have driven the approval itself and walked away with a
222
+ > long-lived credential; no header distinguishes a frame from the page around it,
223
+ > because they are the same origin. Per-member CLI credentials still want the
224
+ > frame isolation this release does not have, so the one credential is the
225
+ > operator's.
226
+
227
+ ### The gate footer
228
+
229
+ The gate footer ("Powered by Marver.design") is the honor system, not enforcement:
230
+ Marver is free, the gate is fully personalized to your app, and that one line is how
231
+ the tool spreads - we'd love it if you keep it. It's yours to remove, no strings:
232
+ `share: { branding: false }` in `design/config.ts` (this also strips every Marver
233
+ mention from the page metadata, the sign-in screens included).
234
+
235
+ ### Name the canvas
236
+
237
+ `share: { name: "Your App" }` in `design/config.ts`. That name titles the gate,
238
+ labels the brand pill, and becomes `utm_campaign` on every powered-by link the
239
+ canvas emits, so site analytics can tell which canvas sent a visitor. Unset, the gate
240
+ falls back to your `package.json` name and the canvas shell to the root
241
+ directory name - which inside a container is usually `app`, so every unnamed
242
+ containerised canvas reports as one campaign.
243
+
244
+ ## Railway (the one-pager)
245
+
246
+ 1. Push your repo to GitHub and create a Railway service from it.
247
+ 2. Build command: `npm ci && npx marver build`
248
+ 3. Start command: `npx marver serve` (Railway's `$PORT` is picked up automatically)
249
+ 4. Variables: `MARVER_PASSWORD=<your password>`
250
+
251
+ Deploy. The repo itself is the deployable - nothing to export, nothing to sync.
252
+
253
+ ## Docker (everywhere else)
254
+
255
+ ```dockerfile
256
+ FROM node:22-slim
257
+ WORKDIR /app # set share.name in design/config.ts - the fallback name is this directory
258
+ COPY . .
259
+ RUN npm ci && npx marver build
260
+ ENV PORT=8080
261
+ EXPOSE 8080
262
+ CMD ["npx", "marver", "serve"]
263
+ ```
264
+
265
+ ## Cloudflare Pages + Access (email/domain allowlists)
266
+
267
+ For teams that want per-email policies instead of one password: build in CI
268
+ (`npx marver build`, output directory `design/.dist`), deploy to Pages, then put
269
+ Cloudflare Access in front with your email or domain rules. Google login and audit
270
+ logs come free; Marver ships no auth code at all in this setup.