@marver-design/marver 0.13.0 → 0.15.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 (49) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +44 -20
  3. package/dist/{build-BxGrHFT2.mjs → build-DfuTQZlY.mjs} +46 -6
  4. package/dist/cli.mjs +21 -7
  5. package/dist/{daemon-BChkzDqQ.mjs → daemon-DalgvoA9.mjs} +1 -1
  6. package/dist/{dev-DLwt3Brb.mjs → dev-BxCmeU_H.mjs} +14 -4
  7. package/dist/{init-BpitOqRQ.mjs → init-QKNi9gvF.mjs} +66 -2
  8. package/dist/{manifest-CS6krOTe.mjs → manifest-BzxSMoDB.mjs} +24 -6
  9. package/dist/{marver-id-gate-B2uraTHS.mjs → marver-id-gate-D6By7XHj.mjs} +1 -1
  10. package/dist/{plugin-DNc4Jpae.mjs → plugin-DJyjmQeh.mjs} +60 -10
  11. package/dist/poster-CbpzSzJu.mjs +143 -0
  12. package/dist/{serve-EjEqsiYa.mjs → serve-Bcwfpvhl.mjs} +3 -2
  13. package/dist/{shot-Cyv3GN79.mjs → shot-BWhoz6cU.mjs} +204 -57
  14. package/dist/{shot-DkkwuCZ2.mjs → shot-By1AItpD.mjs} +3 -2
  15. package/docs/live-jam.md +177 -0
  16. package/docs/publish.md +270 -0
  17. package/docs/sharing.md +333 -0
  18. package/docs/slides.md +140 -0
  19. package/package.json +3 -1
  20. package/src/client/const.ts +13 -0
  21. package/src/client/content/chart-engine.ts +33 -0
  22. package/src/client/content/chart.tsx +138 -0
  23. package/src/client/content/index.tsx +30 -6
  24. package/src/client/content/slide.tsx +238 -0
  25. package/src/client/content/video.tsx +223 -0
  26. package/src/client/frame-host/bridge.js +6 -1
  27. package/src/client/shell/App.tsx +59 -13
  28. package/src/client/shell/LockedApp.tsx +7 -2
  29. package/src/client/shell/Play.tsx +138 -24
  30. package/src/client/shell/Toolbar.tsx +12 -3
  31. package/src/client/shell/canvas/FrameNode.tsx +5 -3
  32. package/src/client/shell/hash.ts +3 -1
  33. package/src/client/shell/icons.tsx +3 -0
  34. package/src/client/shell/play-order.ts +22 -0
  35. package/src/client/shell/store.ts +80 -9
  36. package/src/client/shell/styles.css +23 -27
  37. package/src/client/stage/main.tsx +54 -3
  38. package/src/shared/utm.ts +3 -2
  39. package/templates/AGENTS-embedded.md +20 -4
  40. package/templates/AGENTS-studio.md +20 -4
  41. package/templates/instructions/boards.md +47 -5
  42. package/templates/instructions/craft.md +17 -0
  43. package/templates/instructions/iterate.md +109 -14
  44. package/templates/instructions/jam.md +18 -2
  45. package/templates/instructions/publish.md +7 -0
  46. package/templates/instructions/reference/deck-layouts.md +230 -0
  47. package/templates/instructions/reference/deck-story.md +110 -0
  48. package/templates/instructions/shape.md +15 -1
  49. package/templates/instructions/slides.md +402 -0
@@ -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.
@@ -0,0 +1,333 @@
1
+ # Sharing a Marver canvas
2
+
3
+ [Publishing](publish.md) decides which boards ship and how a canvas is gated.
4
+ **Sharing** decides who gets in and who may comment - person by person, from the
5
+ terminal or the browser.
6
+
7
+ The one thing worth holding in your head: **there is a single pure function that
8
+ answers "what can this person do here", and the doors that enforce access all
9
+ call it.** The gate, comment writes, and the browser dialog's owner routes read
10
+ from the same `share.json` and the same resolver, so the policy is one thing in
11
+ one place, not a rule re-implemented per door.
12
+
13
+ > **Sharing needs a place to keep the roster.** `marver share` and the dialog
14
+ > manage a roster of *people*, which needs three things: `MARVER_DATA_DIR` (a
15
+ > persistent volume for `share.json`), an **owner account** on the canvas, and
16
+ > the owner's **device credential** (`marver comments connect`, authenticated
17
+ > with `MARVER_CLI_TOKEN` on an identity canvas - see the
18
+ > [publishing guide](publish.md#marver-sign-in-marver_id_issuer---recommended-for-teams)).
19
+ > With those, a canvas gated by a **password** supports exact-email grants
20
+ > today. **Marver Sign In** adds what only a verified identity can do: domain
21
+ > grants (`@acme.com`), the request-access flow for refused visitors, the
22
+ > hosted browser dialog, and the front door. A password canvas without
23
+ > `MARVER_DATA_DIR` is one shared secret and nothing to share person by person.
24
+
25
+ ## The inputs sharing combines
26
+
27
+ Access is built from a few inputs, and the resolver combines them in one fixed
28
+ order (the precise order is in [How access is computed](#how-access-is-computed-the-resolver-precisely) below):
29
+
30
+ 1. **The blocklist** - the only *deny*, applied first. Its reach is narrower than
31
+ it sounds; see the note under `block` below.
32
+ 2. **The owner** - the canvas owner precedes principal matching and can always
33
+ open and administer the canvas.
34
+ 3. **Grants** - who was let in and at what level (`view` or `comment`), optionally
35
+ until a date. Grants are *additive*: the highest matching grant wins.
36
+ 4. **General access** - the floor for anyone admitted: `private`, `password`, or
37
+ `public`. What you can actually select is clamped by the gate you run (below).
38
+
39
+ Every result is then **clamped by the board's published ceiling** - `publish.json`
40
+ says a board tops out at `read` or `comment`, and no grant can exceed it. A grant
41
+ of `comment` on a board published `read` resolves to `view`.
42
+
43
+ ## `marver share` - the roster from the terminal
44
+
45
+ `marver share` calls the same owner-only routes the browser dialog does,
46
+ authenticated with the device credential [`marver comments connect`](publish.md#marver-sign-in-marver_id_issuer---recommended-for-teams)
47
+ stored. Connect once as the owner, then:
48
+
49
+ ```bash
50
+ marver share add sam@acme.com # grant view (the default)
51
+ marver share add sam@acme.com --role comment # grant comment instead
52
+ marver share add sam@acme.com --expires 2027-06-01 # a grant that lapses on its own
53
+ marver share add @acme.com --role comment # any verified @acme.com address (identity canvas only)
54
+ marver share remove sam@acme.com # take the grant back
55
+
56
+ marver share block troll@spam.com # see the note below - narrower than it reads
57
+ marver share unblock troll@spam.com
58
+
59
+ marver share general private # the stored floor (the gate may clamp it, below)
60
+ marver share general password
61
+ marver share general public
62
+
63
+ marver share list # the roster: general access, grants, blocklist
64
+ marver share requests # pending access requests
65
+ marver share requests --approve sam@acme.com # grant them - canvas-wide, at view
66
+ marver share requests --approve sam@acme.com --role comment
67
+ marver share requests --decline sam@acme.com # resolve it silently - no rejection reaches them
68
+
69
+ marver share explain sam@acme.com # the resolver's trace for one person (see caveats)
70
+ marver share who # granted principals x published boards
71
+ ```
72
+
73
+ **`block` is narrower than "refused everywhere."** The blocklist is the first and
74
+ only deny *inside the resolver*, but two things sit outside it, and the CLI says
75
+ so when you block: **"Blocking only bites while general access is Private."** If
76
+ general access is `password` or `public`, the same person can still enter
77
+ anonymously - there is no identity to block at an anonymous door. And a canvas
78
+ **owner** is admitted ahead of the blocklist (someone must be able to administer),
79
+ though comment authorization can still deny them. Blocking reliably denies an
80
+ *identified, non-owner* person and their access requests; it is a complete entry
81
+ denial only while general access is private.
82
+
83
+ **`explain` asks the canvas; `who` is a local convenience view.** Since 0.13.0
84
+ `marver share explain` calls the canvas's own `share/explain` route - the owner
85
+ role, domain principals, and the winning step included - so it is the enforcing
86
+ trace. `who` still runs the resolver locally over the roster it fetched, and
87
+ takes three shortcuts the enforcing doors do not:
88
+
89
+ - it does not pass the owner role, so a fresh owner with no grant of their own is
90
+ explained as an ordinary ungranted person (the server's own `explain` route
91
+ fills the owner role in; the dialog is the accurate view for the owner);
92
+ - it treats a `@domain` argument as having no address to match, so a domain row
93
+ in `who` shows `(per member)` rather than an effective role;
94
+ - it prints each step's role but not which step *won* - the resolver marks a
95
+ winning step, and the CLI renderer drops that mark, so read the trace as the
96
+ inputs, not a highlighted verdict.
97
+
98
+ So `explain` is reliable for anyone; `who` lists *granted principals* down the side (not the owner,
99
+ who holds no grant row), the blocklist, and - when general access is open - an
100
+ anonymous row; it is the grant matrix, not a census of everyone who could get in.
101
+
102
+ Domain grants (`marver share add @acme.com`) need Marver Sign In: a canvas that
103
+ cannot verify who holds an address cannot verify their domain.
104
+
105
+ ## `publish.json` v2 - the ceiling, and the board's shape
106
+
107
+ `publish.json` is where a board's **ceiling** lives - the most any grant can
108
+ resolve to there - and, in the v2 row shape, a few facts about how the board
109
+ presents. Every 0.11 canvas parses unchanged: a bare `"read"` or `"comment"`
110
+ string is still a valid row. (This is a **schema** version; it is unrelated to
111
+ the read-privacy work also called "v2" further down.)
112
+
113
+ ```jsonc
114
+ {
115
+ "boards": {
116
+ "roadmap": "comment", // v1 row: the ceiling, nothing more
117
+
118
+ "brand": { // v2 row: an object
119
+ "max": "read", // required - the ceiling ("read" | "comment")
120
+ "type": "design", // optional - the artifact type (default "mix")
121
+ "open": "focus", // optional - the landing view
122
+ "lock": true // optional - freeze the landing view (needs "open")
123
+ }
124
+ },
125
+ "reveal": { "structure": true, "source": false }
126
+ }
127
+ ```
128
+
129
+ - **`max`** (`"read"` | `"comment"`) is the ceiling. This is the only field that
130
+ affects *access*; everything else is presentation.
131
+ - **`type`** is one of `doc`, `slides`, `design`, `sketch`, `refs`, `mix`
132
+ (default `mix`). It picks the board's default landing view and its card icon;
133
+ nothing infers a type from content.
134
+ - **`open`** names the landing view: `canvas`, `board`, `present`, `focus`,
135
+ or `slides`. `canvas` and `board` both land on the canvas; `present`,
136
+ `focus`, and `slides` are their own modes. Absent means the type decides
137
+ (`slides` → slides mode, `doc` → focus, `design` / `sketch` / `refs` →
138
+ board, `mix` → canvas).
139
+ - **`lock`** freezes the canvas in the `open` view (no view switcher). It
140
+ requires `open` - freezing an unnamed mode is meaningless, so the build refuses
141
+ it.
142
+ - **`transition`** (`"fade"` default | `"none"`) and **`chrome`** (`"full"`
143
+ default | `"minimal"` | `"none"`) shape slide playback only. They are
144
+ accepted on a board whose `type` or `open` is `slides` and refused
145
+ elsewhere. A slides board plays only its `slide: true` frames: non-slide
146
+ frames on it warn at build, a board with none errors (0.13.0 let `slides`
147
+ alias `present`; 0.14.0 plays only slides - to keep the old behaviour set
148
+ a non-slides `type` such as `mix` AND `open: "present"`, or add
149
+ `slide: true` to the frames).
150
+ - **`reveal.source`** defaults **off** on a published canvas: the bundle would
151
+ otherwise carry every frame's repo path, which is a disclosure the moment a
152
+ canvas is shared beyond its own repo. `reveal.structure` defaults on.
153
+
154
+ Publishing stays **default-closed**: no `publish.json` and no explicit
155
+ `--boards`/`--all-boards` flag means nothing ships. A board absent from the
156
+ policy is absent from the bundle - which is also the read boundary in v1 (see
157
+ ["What v1 does not do"](#what-v1-does-not-do)).
158
+
159
+ ## General access, and what your gate can enforce
160
+
161
+ `marver share general <mode>` is clamped to what the **gate you run can actually
162
+ enforce** - and it is clamped when you *set* it, not when access is resolved. When
163
+ you ask for more than the gate allows, the owner API stores the operative
164
+ (clamped) mode, and the CLI tells you what it stored and why, so the roster never
165
+ holds a state the server cannot enforce:
166
+
167
+ | Gate you run | `private` | `password` | `public` |
168
+ |---|---|---|---|
169
+ | **Marver Sign In** (`MARVER_ID_ISSUER`) | private | private | private |
170
+ | **Password** (`MARVER_PASSWORD`, no issuer) | private | password | password |
171
+ | **No gate** (neither set) | public | public | public |
172
+
173
+ So on an identity canvas general access is always `private` - membership is the
174
+ whole point, and there is no anonymous door to open. On a password canvas,
175
+ `public` clamps to `password` (the password would otherwise be theater). A canvas
176
+ with no gate is `public` by definition. When the CLI clamps your request it tells
177
+ you what it stored and why.
178
+
179
+ ## `share.json` - the grant store
180
+
181
+ The roster lives in `share.json` on your volume (`MARVER_DATA_DIR`), beside
182
+ `auth.json`. It is a plain JSON file on the server, written mode `0600` (readable
183
+ by the OS account running Marver, not by a browser owner - browser owners inspect
184
+ it through the CLI or the dialog, both of which go through the owner API). You
185
+ rarely touch it by hand, but three things about how it behaves are worth knowing:
186
+
187
+ - **It is created once, at serve boot, from your live 0.11 state.** Every existing
188
+ account gets a canvas-wide `comment` grant (clamped per board) because that is
189
+ exactly what they could do the day before. Until that first boot, every door
190
+ falls back to the legacy rules it always had - upgrading changes nothing you
191
+ did not ask for.
192
+ - **A grant carries a per-board ratchet, not just a role.** A grant records what
193
+ you *asked for* (`assigned`) separately from what each board currently resolves
194
+ to (`boardRole`). Reads always take `min(current ceiling, boardRole)`, and every
195
+ boot re-clamps entries *down* to the ceiling, never up. So raising a board's
196
+ ceiling later does **not** silently re-promote everyone who was granted under
197
+ the old, lower one - the entry stays low until you explicitly re-confirm it.
198
+ This is the non-promotion invariant, and it is the reason a ceiling change is
199
+ always safe.
200
+ - **It fails closed.** A present-but-corrupt or malformed `share.json` denies
201
+ rather than falling back to open - a policy typo must never quietly grant the
202
+ ceiling. A *missing* file is the pre-migration signal and keeps legacy rules; a
203
+ broken one stops the doors.
204
+
205
+ ## Access requests - when someone refused wants in
206
+
207
+ When a person is refused at the identity gate, the canvas offers them a request
208
+ form instead of a dead end. The refused visitor has no session to authenticate
209
+ with, so the canvas mints a **short-lived, origin-bound, single-purpose request
210
+ token** and accepts a request only against it (the `marver-reqaccess+jwt`
211
+ contract). The request lands as one pending row per address:
212
+
213
+ - it surfaces in `marver share requests` and in the dialog's requests list;
214
+ - **approving grants canvas-wide at the role you choose** (`view` by default) -
215
+ in v1 an approval covers every published board, and the CLI and dialog both
216
+ say so;
217
+ - **declining resolves the row silently** - no rejection email reaches the asker;
218
+ - a repeat ask from the same address replaces its note, it does not pile up a
219
+ second row, and a row expires on its own after 30 days.
220
+
221
+ ## The front door (`app.marver.design`)
222
+
223
+ The front door is the signed-in home page at `app.marver.design`: a person signs
224
+ in once and sees the canvases they can reach, each row lit by a short summary the
225
+ **canvas itself signs and serves**. The front door holds no roster and makes no
226
+ access decision - it asks each canvas, and each canvas answers for itself.
227
+
228
+ For a canvas operator there are two things to know:
229
+
230
+ - **Your canvas answers its own summary probes, and only signed ones.** The front
231
+ door presents a `marver-summary+jwt` (audience = your exact origin, authorized
232
+ party = the app, valid ≤120s); the canvas verifies it against the identity
233
+ service's published keys before answering, and the answer is signed by the
234
+ canvas so the browser can pin it. Owner mutations from the dialog carry a
235
+ separate `marver-owner-api+jwt` (≤300s), and outbound mail rides a
236
+ `marver-relay+jwt`. You do not configure any of this - it is the wire, listed
237
+ so you can recognize it.
238
+ - **You can keep your canvas out of it.** `share: { frontDoor: false }` in
239
+ `design/config.ts` makes the canvas stop answering front-door identity and
240
+ summary probes - the app receives no signed summary from this canvas, so it has
241
+ nothing to show. The privacy tradeoff around the front door is disclosed in full
242
+ in the [publishing guide](publish.md#who-can-open-your-canvas); this is the
243
+ switch that makes your canvas silent to it.
244
+
245
+ ## How access is computed (the resolver, precisely)
246
+
247
+ For anyone who wants to check rather than trust, here is the exact order the one
248
+ resolver (`resolveAccess`) runs, top to bottom:
249
+
250
+ 1. **Blocklist.** If the address is blocked, every board is `none` and the person
251
+ is refused, and nothing below runs. (An anonymous caller - past a password
252
+ gate, or on a public canvas - has no identity to block, so this step never
253
+ fires for them, which is why blocking does not stop anonymous entry.)
254
+ 2. **Owner.** The canvas owner precedes principal matching and resolves to
255
+ `comment` on every board - still clamped by the ceiling, so an unpublished
256
+ board is empty even for the owner.
257
+ 3. **Grants, additive, highest wins.** Every live grant matching the address
258
+ (exact, or by verified domain) contributes its `boardRole`, read through the
259
+ read-time ceiling `min`. The highest contribution per board wins.
260
+ 4. **General access.** If the operative mode is not `private`, everyone admitted
261
+ gets `view` as a floor.
262
+ 5. **Ceiling clamp.** Every board's result is `min(result, publish.json ceiling)`.
263
+
264
+ `entry` (may they open the canvas at all) is "at least `view` on at least one
265
+ board". Note the gate's own entry check short-circuits step 1 for the owner - the
266
+ owner is always admitted so the canvas can be administered - which is the one
267
+ place the blocklist does not have the last word.
268
+
269
+ ## Mail, mentions, and the bell (v1.1)
270
+
271
+ Sharing's mail rides one relay at the identity service, and the canvas can only
272
+ ever choose a **template** - never a subject or a body. Activity mail carries
273
+ exactly three variables beyond the origin: the commenter's display name, a
274
+ comment snippet capped at 180 characters, and your own unsubscribe link -
275
+ which means the identity service holds those snippets for its delivery window.
276
+ That is a deliberate, disclosed widening (every collaboration product's
277
+ mention mail shows who and what; a mail that names neither gets ignored).
278
+ Transactional mail (invites, approvals, requests) carries no name and no
279
+ content, and `share: { notify: false }` opts out of all of it. Three transactional moments (you were invited, your request
280
+ was approved, someone asked for access) shipped with v1; v1.1 adds the two
281
+ **activity** moments that pull collaborators back:
282
+
283
+ - **A thread you are in moved.** A fresh reply mails the thread's other
284
+ participants - the 10 most recent distinct people, at most one mail per
285
+ person per thread per 6-hour window, and only for replies just written
286
+ (history imports and syncs never mail anyone).
287
+ - **Somebody named you.** Typing `@` in the comment composer offers the people
288
+ already visible in the canvas's comments; a completed `@Name` mails that
289
+ person once for that comment and rings their bell on the front door. You can
290
+ only mention people you can already see - the roster is never disclosed, and
291
+ in the browser mentions travel as the same opaque ids authors do, so no
292
+ member's address ever reaches another viewer.
293
+
294
+ While you are ON the canvas, the same moments notify in place: a mention (or a
295
+ reply in your thread) raises a bottom-right pill with the author's face and
296
+ message plus a soft ping, and the mentioned thread's pin pulses in accent blue
297
+ until you open it. The mail's button deep-links straight to that thread.
298
+
299
+ Only people who still resolve to at least `view` are mailed - a revoked or
300
+ blocked participant's mail stops with their access. Every activity mail carries
301
+ an **unsubscribe link**: it can mute replies, mentions, or all activity mail,
302
+ for that canvas or everywhere, and it mutes **email only** - the bell keeps
303
+ working. Invitations and approvals are never muted (an invite you cannot
304
+ receive is a lockout, not a courtesy). `share: { notify: false }` still
305
+ declines the relay entirely, activity mail included.
306
+
307
+ ## What v1 does not do
308
+
309
+ Sharing v1 controls **who gets in** and **who may comment**. It does **not** yet
310
+ do per-person *read* privacy: every admitted person can read every published
311
+ board. The read boundary in v1 is the bundle itself - a board you do not publish
312
+ is not in the build, so the way to keep something from an audience today is a
313
+ separate canvas for that audience. ("v2" here means this next release, the
314
+ read-privacy one - a different thing from the `publish.json` v2 *row schema*
315
+ above, which ships now.)
316
+
317
+ Three consequences, stated plainly because they are easy to assume otherwise:
318
+
319
+ - **Grants are canvas-scoped.** `share.json` is already shaped for board-scoped
320
+ grants, but the CLI and dialog accept `canvas` scope only - a board-scoped grant
321
+ would open the whole bundle while reading as "just this board", so the door
322
+ refuses it until read privacy makes it real.
323
+ - **Approvals are canvas-wide.** An approved access request covers every published
324
+ board, and the surfaces say so. The request records what the refused link
325
+ pointed at, but that target is context in v1, not an enforced scope.
326
+ - **A deep link is presentation, not a wall.** A single-frame or single-board
327
+ link lands that visit in the right view, but the rest of the canvas stays
328
+ reachable by URL. The door renders because there is one frame to show, not
329
+ because the others are protected.
330
+
331
+ All three become enforced in the read-privacy release, when boards are *served*
332
+ per person rather than *bundled*. The schema is already ready for it; v1 declines
333
+ the operations it cannot honor rather than pretending to.