@marver-design/marver 0.11.0 → 0.11.1

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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  Notable changes to `@marver-design/marver`. Format follows [Keep a Changelog](https://keepachangelog.com); versions follow semver.
4
4
 
5
+ ## 0.11.1 - 2026-08-27
6
+
7
+ ### Fixed
8
+
9
+ - **The password gate's cookie now gets `Secure` from the pinned origin too.** 0.11.0 fixed
10
+ this for the collaboration session cookie and missed the gate's own, which is set in a
11
+ different file and is the one most likely to be sitting behind a plain `proxy_pass` - it is
12
+ the door on every password-mode canvas. Same rule, same reason: `MARVER_PUBLIC_ORIGIN`
13
+ decides when it is set, and `X-Forwarded-Proto` is the fallback only when there is nothing
14
+ better. Set `MARVER_PUBLIC_ORIGIN=https://your-canvas.example.com` and the thirty-day gate
15
+ cookie is https-only regardless of what your proxy tells the process.
16
+
17
+ Canvases behind a proxy that does send `X-Forwarded-Proto` - Railway, Fly, Vercel, Caddy,
18
+ and nginx configured with `proxy_set_header` - were never affected.
19
+
5
20
  ## 0.11.0 - 2026-08-27
6
21
 
7
22
  ### Added
@@ -105,15 +120,18 @@ Notable changes to `@marver-design/marver`. Format follows [Keep a Changelog](ht
105
120
 
106
121
  ### Fixed
107
122
 
108
- - **`Secure` on the session cookie no longer depends on a header your proxy may not send.**
109
- A canvas decided whether it was on https by reading `X-Forwarded-Proto`, and nginx's own
110
- documented `proxy_pass http://localhost:PORT` sets no `X-Forwarded-*` at all. A canvas
111
- served over https behind that configuration saw no header, concluded "not secure", and
112
- issued a thirty-day session cookie the browser would happily send over plain http. Where
123
+ - **`Secure` on the collaboration session cookie no longer depends on a header your proxy may
124
+ not send.** A canvas decided whether it was on https by reading `X-Forwarded-Proto`, and
125
+ nginx's own documented `proxy_pass http://localhost:PORT` sets no `X-Forwarded-*` at all. A
126
+ canvas served over https behind that configuration saw no header, concluded "not secure",
127
+ and issued a thirty-day session cookie the browser would happily send over plain http. Where
113
128
  `MARVER_PUBLIC_ORIGIN` is set it now decides - a deliberate statement by whoever deployed
114
129
  the canvas, rather than a guess about a proxy that may not be speaking. The header remains
115
130
  the fallback only where there is nothing better, which is development on loopback.
116
131
 
132
+ This release fixed the collaboration session cookie only. The password gate's own cookie
133
+ still guessed from the header - see 0.11.1.
134
+
117
135
  - **The collaboration credential has moved out of your repository, because `marver dev` was
118
136
  serving it.** It lived at `design/.local/collab.json`, and the dev server puts the repository
119
137
  on the web so frames can import from it. Authored frames run same-origin, so any frame could
package/README.md CHANGED
@@ -28,6 +28,7 @@ Frames appear on the canvas the moment the files land. That's the loop.
28
28
  - **Everything hot-reloads.** The agent writes, you watch it land - live.
29
29
  - **True viewports.** Each frame is a real iframe: drag its edge and your actual breakpoints fire.
30
30
  - **Your agent answers on the canvas.** Tag `@marver` in a comment and it picks up the job, edits the real source, and replies in the thread - no wiring, on by default. See [Live Jam](#live-jam).
31
+ - **Feedback without a signup wall.** Publish the canvas, invite people by email, and they sign in as themselves - one free Marver account, Google or an emailed code, and it opens every canvas you ever share with them. Or keep it entirely self-hosted behind a shared password. See [Collaboration](#collaboration).
31
32
  - **No AI inside.** The designer is the coding agent you already run and pay for. `init` generates the `design/AGENTS.md` contract that teaches it the whole workflow.
32
33
 
33
34
  ## The canvas
@@ -40,9 +41,21 @@ Frames appear on the canvas the moment the files land. That's the loop.
40
41
 
41
42
  ## Collaboration
42
43
 
43
- - **Comments.** Google-Docs-style feedback pinned to actual elements. Press `c`, click a div inside a frame, write - the thread lives on that element and survives edits via a layered anchor (source semantics → structure → fuzzy text). Viewers on a published canvas comment with real names and avatars (invite-link accounts, no email infrastructure). `marver dev` syncs the same threads into `design/comments/*.jsonl`, where your agent works the queue: `npx marver comments list --open --json` → fork a variant → `resolve --addressed-in`. Live via SSE; one deploy, no extra services.
44
+ - **Comments.** Google-Docs-style feedback pinned to actual elements. Press `c`, click a div inside a frame, write - the thread lives on that element and survives edits via a layered anchor (source semantics → structure → fuzzy text). Viewers on a published canvas comment with real names and avatars - either a Marver account they already have, or an invite-link account on that canvas alone. `marver dev` syncs the same threads into `design/comments/*.jsonl`, where your agent works the queue: `npx marver comments list --open --json` → fork a variant → `resolve --addressed-in`. Live via SSE; one deploy, no extra services.
44
45
  - **Laser mode.** Press `l`: every element in every frame gets depth-hued outlines plus a hover label - the fastest way to see structure. Click any element to copy its full address (frame file + CSS path) for the agent.
45
- - **Publishing.** `npx marver build` exports a static canvas (default-closed: `design/publish.json` names what ships); `npx marver serve` hosts it with an optional password gate. One deploy on Railway, Docker, or any static host - the [publishing guide](docs/publish.md) has the one-pagers.
46
+ - **Publishing.** `npx marver build` exports a static canvas (default-closed: `design/publish.json` names what ships, and whether each board is `read` or `comment`); `npx marver serve` hosts it. One deploy on Railway, Docker, or any static host - the [publishing guide](docs/publish.md) has the one-pagers.
47
+
48
+ - **Two ways to let people in.** They are alternatives, not layers - pick one per canvas.
49
+
50
+ **Marver Sign In** (new in 0.11). Set `MARVER_ID_ISSUER=https://id.marver.design` and reviewers sign in as themselves, with Google or a six-digit code emailed to them. One free Marver account opens *every* canvas gated this way, so the second board you share costs them nothing: no new signup, no new password, no link to keep. You invite an email address and they are in.
51
+
52
+ That is the whole difference, and it is the difference between "I'll look later" and a comment actually landing on the board. It also means their real name and face ride along, so a thread is from a person rather than from an address.
53
+
54
+ **A shared password** (`MARVER_PASSWORD`). Fully self-hosted, no account anywhere but your own volume, nothing about your reviewers leaves your infrastructure - and still fully supported, not a legacy path. The trade is that every canvas is an island: reviewers claim an invite link and pick a name and password *on that canvas*, and do it again for the next one. Fine for one board, a toll on the fifth.
55
+
56
+ Either way the canvas runs on your infrastructure and stores its own comments and members. With Marver Sign In the identity service only ever tells your canvas that a verified address matched an entry on your invite list - it never sees your frames, your files, or your comments. Rights stay yours: `design/publish.json` decides which boards are readable and which are commentable, and `marver comments invite`/`revoke` decides who is on the list.
57
+
58
+ *Coming next:* one home for your account - every canvas you have been invited to, every canvas you have shared, and the access each one carries, in a single list. Today the account page at `id.marver.design` shows the canvases you have approved and lets you revoke them.
46
59
 
47
60
  ## Live Jam
48
61
 
@@ -61,7 +74,7 @@ The same glow, driven from the terminal. When your agent takes a request, it cre
61
74
  | `npx marver init` | Scaffold `design/` in this repo (safe to re-run; refreshes managed files) |
62
75
  | `npx marver dev` / `canvas` | Start the local canvas - hot reload, comments, Live Jam armed (`--port`, default 5199) |
63
76
  | `npx marver build` | Static export → `design/.dist`; what ships comes from `design/publish.json` (default-closed) |
64
- | `npx marver serve` | Serve the export; `MARVER_PASSWORD` gates it, `MARVER_DATA_DIR` persists comments + accounts |
77
+ | `npx marver serve` | Serve the export; `MARVER_ID_ISSUER` or `MARVER_PASSWORD` gates it, `MARVER_DATA_DIR` persists comments + accounts |
65
78
  | `npx marver comments …` | The agent's queue: `connect <url>` · `sync` · `list` · `reply` · `resolve` · `invite <email>` · `revoke <email>` |
66
79
  | `npx marver work …` | Working glow from the terminal: `start <scene/frame …>` · `done … \| --all` · `list` |
67
80
 
@@ -1,7 +1,7 @@
1
1
  import { i as ROUTE, n as NAME } from "./cli.mjs";
2
2
  import { i as detectHost, n as loadConfig } from "./config-t9coJ-Pq.mjs";
3
3
  import { r as scanFrames } from "./manifest-C2tzkNaC.mjs";
4
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-vE7sxEeT.mjs";
4
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DjBFxEQ7.mjs";
5
5
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
6
6
  import { basename, dirname, join, sep } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
package/dist/cli.mjs CHANGED
@@ -46,7 +46,7 @@ cli.command("init", "Scaffold design/ in this repo").option("--mode <mode>", "st
46
46
  });
47
47
  });
48
48
  for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot reload, comments, Live Jam)"], ["canvas", "Start the local canvas - same as dev"]]) cli.command(name, desc).option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default 5199)").action(async (opts) => {
49
- const { dev } = await import("./dev-CLBjRs0J.mjs");
49
+ const { dev } = await import("./dev-BWIL2ewj.mjs");
50
50
  let port;
51
51
  if (opts.port !== void 0) {
52
52
  const n = Number(opts.port);
@@ -56,7 +56,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
56
56
  await dev(resolve(opts.root), port);
57
57
  });
58
58
  cli.command("build", "Static export → design/.dist (what ships comes from design/publish.json - publishing is default-closed)").option("--boards <names>", "Publish only these boards (comma-separated); overrides the publish policy").option("--all-boards", "Publish every board - the loud override for the default-closed policy").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
59
- const { buildSite } = await import("./build-Cr_ZLMdq.mjs");
59
+ const { buildSite } = await import("./build-DI31MFTB.mjs");
60
60
  try {
61
61
  const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
62
62
  await buildSite(resolve(opts.root), boards, opts.allBoards === true);
@@ -66,7 +66,7 @@ cli.command("build", "Static export → design/.dist (what ships comes from desi
66
66
  }
67
67
  });
68
68
  cli.command("serve", "Serve design/.dist (set MARVER_PASSWORD to gate it)").option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default $PORT or 4199)").action(async (opts) => {
69
- const { serve } = await import("./serve-CZ1KEQxd.mjs");
69
+ const { serve } = await import("./serve-3APqaNSB.mjs");
70
70
  let port;
71
71
  if (opts.port !== void 0) {
72
72
  const n = Number(opts.port);
@@ -1,33 +1,9 @@
1
1
  import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
2
2
  import { a as claimInvite, c as issueDeviceSession, d as publicUser, f as revokeUser, g as updateProfile, h as signOut, l as ownerName, m as signIn, o as createInvite, p as sessionUser, s as inviteInfo } from "./auth-B5yuwnOq.mjs";
3
+ import { t as secureSuffix } from "./secure-cookie-_K1Hsx8H.mjs";
3
4
  import { readFileSync } from "node:fs";
4
5
  import { join } from "node:path";
5
6
  import { randomBytes } from "node:crypto";
6
- //#region src/server/secure-cookie.ts
7
- /**
8
- * Is this deployment https, for the purpose of setting `Secure` on a cookie?
9
- *
10
- * The obvious answer - read `X-Forwarded-Proto` - is wrong in exactly the case
11
- * that matters, and it is the same trap that made `MARVER_PUBLIC_ORIGIN`
12
- * mandatory for identity mode: nginx's own documented `proxy_pass
13
- * http://localhost:PORT` sets no `X-Forwarded-*` at all. A canvas served over
14
- * https behind that config sees no header, decides "not secure", and issues a
15
- * thirty-day session cookie the browser will happily send over plain http.
16
- *
17
- * So the pinned origin wins whenever it is set. It is a deliberate statement by
18
- * whoever deployed the canvas, rather than a guess about a proxy that may not be
19
- * speaking. The header remains the fallback for a canvas with no pinned origin,
20
- * where a guess is all there is - and http on loopback is the ordinary
21
- * development case, where `Secure` would break the cookie entirely.
22
- */
23
- function isSecureDeployment(req) {
24
- const pinned = (process.env.MARVER_PUBLIC_ORIGIN ?? "").trim();
25
- if (pinned) return pinned.toLowerCase().startsWith("https://");
26
- return req.headers["x-forwarded-proto"] === "https";
27
- }
28
- /** `; Secure`, or nothing - the suffix a Set-Cookie line wants. */
29
- const secureSuffix = (req) => isSecureDeployment(req) ? "; Secure" : "";
30
- //#endregion
31
7
  //#region src/server/collab.ts
32
8
  const MONTH = 2592e3;
33
9
  const MAX_BODY = 262144;
@@ -1,6 +1,6 @@
1
1
  import { n as NAME, r as PKG } from "./cli.mjs";
2
2
  import { i as detectHost, n as loadConfig } from "./config-t9coJ-Pq.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-vE7sxEeT.mjs";
3
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DjBFxEQ7.mjs";
4
4
  import { realpathSync } from "node:fs";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -305,7 +305,7 @@ function apiMiddleware(root, opts = {}) {
305
305
  }
306
306
  const dir = join(root, "design", ".local");
307
307
  mkdirSync(dir, { recursive: true });
308
- const { validAvatar } = await import("./collab-C-n-uQdm.mjs");
308
+ const { validAvatar } = await import("./collab-CWaG3Q4w.mjs");
309
309
  let cur = {};
310
310
  try {
311
311
  cur = JSON.parse(readFileSync(join(dir, "profile.json"), "utf8"));
@@ -0,0 +1,26 @@
1
+ //#region src/server/secure-cookie.ts
2
+ /**
3
+ * Is this deployment https, for the purpose of setting `Secure` on a cookie?
4
+ *
5
+ * The obvious answer - read `X-Forwarded-Proto` - is wrong in exactly the case
6
+ * that matters, and it is the same trap that made `MARVER_PUBLIC_ORIGIN`
7
+ * mandatory for identity mode: nginx's own documented `proxy_pass
8
+ * http://localhost:PORT` sets no `X-Forwarded-*` at all. A canvas served over
9
+ * https behind that config sees no header, decides "not secure", and issues a
10
+ * thirty-day session cookie the browser will happily send over plain http.
11
+ *
12
+ * So the pinned origin wins whenever it is set. It is a deliberate statement by
13
+ * whoever deployed the canvas, rather than a guess about a proxy that may not be
14
+ * speaking. The header remains the fallback for a canvas with no pinned origin,
15
+ * where a guess is all there is - and http on loopback is the ordinary
16
+ * development case, where `Secure` would break the cookie entirely.
17
+ */
18
+ function isSecureDeployment(req) {
19
+ const pinned = (process.env.MARVER_PUBLIC_ORIGIN ?? "").trim();
20
+ if (pinned) return pinned.toLowerCase().startsWith("https://");
21
+ return req.headers["x-forwarded-proto"] === "https";
22
+ }
23
+ /** `; Secure`, or nothing - the suffix a Set-Cookie line wants. */
24
+ const secureSuffix = (req) => isSecureDeployment(req) ? "; Secure" : "";
25
+ //#endregion
26
+ export { secureSuffix as t };
@@ -1,4 +1,5 @@
1
1
  import { n as NAME } from "./cli.mjs";
2
+ import { t as secureSuffix } from "./secure-cookie-_K1Hsx8H.mjs";
2
3
  import { t as poweredByUrl } from "./utm-CxC3QN5X.mjs";
3
4
  import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
4
5
  import { extname, isAbsolute, join, relative, resolve } from "node:path";
@@ -51,7 +52,7 @@ async function serve(root, portFlag) {
51
52
  let operatorCheck = null;
52
53
  let sessionCheck = null;
53
54
  if (process.env.MARVER_DATA_DIR) {
54
- const { collabHandler } = await import("./collab-C-n-uQdm.mjs");
55
+ const { collabHandler } = await import("./collab-CWaG3Q4w.mjs");
55
56
  const { appendEvents, readLog } = await import("./comments-DZyobpxG.mjs").then((n) => n.n);
56
57
  const { dataDir } = await import("./comments-DZyobpxG.mjs").then((n) => n.n);
57
58
  const dir = dataDir();
@@ -142,7 +143,7 @@ async function serve(root, portFlag) {
142
143
  const given = form.get("password") ?? "";
143
144
  if (timingSafeEqual(scryptSync(given, "marver-gate", 32), verifier)) {
144
145
  const exp = Math.floor(Date.now() / 1e3) + MONTH;
145
- const secure = req.headers["x-forwarded-proto"] === "https" ? "; Secure" : "";
146
+ const secure = secureSuffix(req);
146
147
  res.setHeader("set-cookie", `${COOKIE}=${exp}.${sign(exp)}; Path=/; Max-Age=${MONTH}; HttpOnly; SameSite=Lax${secure}`);
147
148
  const next = form.get("next") ?? "";
148
149
  res.statusCode = 303;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marver-design/marver",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "The agent-native design canvas. A design/ folder, one command, a canvas of live frames built from your repo's real components - comment @marver and your own coding agent does the work. The tool ships no AI.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -152,6 +152,16 @@ somebody owns it there is nobody for the token to be. `connect` trades it for an
152
152
  ordinary session and stores THAT, so the secret never lands in your repo.
153
153
  Rotating `MARVER_CLI_TOKEN` ends every session it minted.
154
154
 
155
+ Then invite people by address, and send them nothing:
156
+
157
+ ```bash
158
+ marver comments invite colleague@company.com
159
+ # → in identity mode the ADDRESS is the invitation. They open the canvas URL,
160
+ # sign in as themselves, and the invite is spent by that sign-in. There is
161
+ # no link to forward and no canvas password to send with it - the claim link
162
+ # is deliberately shut off in identity mode, so do not go looking for one.
163
+ ```
164
+
155
165
  It is a deployment variable and not something a page hands out, because authored
156
166
  frames run same-origin in a canvas: frame code can read `mv_c` and ride the
157
167
  viewer's session, so anything a browser can mint, a frame can mint silently. A
@@ -192,7 +202,11 @@ through sign-in.
192
202
  - `<MARVER_DATA_DIR>/comments/<board>.jsonl` - the live event log, on the volume.
193
203
  - `<MARVER_DATA_DIR>/auth.json` - accounts (scrypt), sessions, invites, on the volume.
194
204
  - `design/comments/<board>.jsonl` - the dev-side mirror, git-tracked: feedback
195
- has history, and the volume has an off-site replica for free.
205
+ has history, and the volume has an off-site replica for free. **Each event
206
+ carries its author's email address** - that is how the canvas decides who may
207
+ edit or resolve their own thread. Harmless in a private repo; if yours is
208
+ public, or may become public, gitignore `design/comments/` and let the volume
209
+ be the record. The canvas keeps its own copy either way, so nothing is lost.
196
210
  - `~/.marver/canvases/<project-hash>.json` - THIS machine's device credential, kept
197
211
  OUTSIDE the repo because `marver dev` serves the repo;
198
212
  never commit it.