@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.
- package/CHANGELOG.md +73 -0
- package/README.md +41 -19
- package/dist/{build-BxGrHFT2.mjs → build-ByYafIhj.mjs} +37 -5
- package/dist/cli.mjs +19 -5
- package/dist/{daemon-BChkzDqQ.mjs → daemon-Bfucyf1o.mjs} +1 -1
- package/dist/{dev-DLwt3Brb.mjs → dev-yZMyQeUj.mjs} +4 -4
- package/dist/{init-BpitOqRQ.mjs → init-Dvaso7YO.mjs} +66 -2
- package/dist/{manifest-CS6krOTe.mjs → manifest-DvOmglFp.mjs} +7 -0
- package/dist/{marver-id-gate-B2uraTHS.mjs → marver-id-gate-D6By7XHj.mjs} +1 -1
- package/dist/{plugin-DNc4Jpae.mjs → plugin-BsmG5i2X.mjs} +21 -3
- package/dist/{serve-EjEqsiYa.mjs → serve-Bcwfpvhl.mjs} +3 -2
- package/dist/{shot-Cyv3GN79.mjs → shot-kbR_xzJH.mjs} +7 -1
- package/docs/live-jam.md +173 -0
- package/docs/publish.md +270 -0
- package/docs/sharing.md +333 -0
- package/docs/slides.md +134 -0
- package/package.json +3 -1
- package/src/client/const.ts +13 -0
- package/src/client/content/chart-engine.ts +33 -0
- package/src/client/content/chart.tsx +100 -0
- package/src/client/content/index.tsx +25 -4
- package/src/client/content/slide.tsx +238 -0
- package/src/client/content/video.tsx +126 -0
- package/src/client/shell/App.tsx +30 -12
- package/src/client/shell/LockedApp.tsx +7 -2
- package/src/client/shell/Play.tsx +138 -24
- package/src/client/shell/Toolbar.tsx +12 -3
- package/src/client/shell/canvas/FrameNode.tsx +5 -3
- package/src/client/shell/hash.ts +3 -1
- package/src/client/shell/icons.tsx +2 -0
- package/src/client/shell/play-order.ts +22 -0
- package/src/client/shell/store.ts +29 -8
- package/src/client/shell/styles.css +17 -27
- package/src/client/stage/main.tsx +54 -3
- package/src/shared/utm.ts +3 -2
- package/templates/AGENTS-embedded.md +1 -0
- package/templates/AGENTS-studio.md +1 -0
- package/templates/instructions/publish.md +7 -0
- package/templates/instructions/reference/deck-layouts.md +230 -0
- package/templates/instructions/reference/deck-story.md +110 -0
- 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
|
package/docs/live-jam.md
ADDED
|
@@ -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.
|
package/docs/publish.md
ADDED
|
@@ -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.
|