@marver-design/marver 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +41 -19
  3. package/dist/{build-DmfxJc6L.mjs → build-ByYafIhj.mjs} +37 -5
  4. package/dist/cli.mjs +21 -7
  5. package/dist/{collab-BmaOjwP1.mjs → collab-mAiNZlDj.mjs} +232 -43
  6. package/dist/{comments-pNxjTrGN.mjs → comments-DHB_8BRa.mjs} +3 -3
  7. package/dist/{daemon-WjonKlPz.mjs → daemon-Bfucyf1o.mjs} +3 -3
  8. package/dist/{dev-BXq7yE1p.mjs → dev-yZMyQeUj.mjs} +5 -5
  9. package/dist/{events-C9k-ceOj.mjs → events-B3LBn74P.mjs} +3 -1
  10. package/dist/{init-BpitOqRQ.mjs → init-Dvaso7YO.mjs} +66 -2
  11. package/dist/{manifest-CS6krOTe.mjs → manifest-DvOmglFp.mjs} +7 -0
  12. package/dist/{marver-id-gate-DOjnZakC.mjs → marver-id-gate-D6By7XHj.mjs} +8 -5
  13. package/dist/{plugin-Boy7I_JH.mjs → plugin-BsmG5i2X.mjs} +25 -7
  14. package/dist/{profile-ChpTPd-X.mjs → profile-BjAPAJSb.mjs} +1 -1
  15. package/dist/{serve-BeLLizNg.mjs → serve-Bcwfpvhl.mjs} +4 -3
  16. package/dist/{share-C64b1tdN.mjs → share-Gqo_Ygqw.mjs} +14 -15
  17. package/dist/{shot-Cyv3GN79.mjs → shot-kbR_xzJH.mjs} +7 -1
  18. package/dist/{summary-Bf9tiGrE.mjs → summary-C7CAypbB.mjs} +1 -1
  19. package/dist/{sync-WYxDN9IT.mjs → sync-BZaCWqK-.mjs} +1 -1
  20. package/docs/live-jam.md +173 -0
  21. package/docs/publish.md +270 -0
  22. package/docs/sharing.md +333 -0
  23. package/docs/slides.md +134 -0
  24. package/package.json +3 -1
  25. package/src/client/const.ts +13 -0
  26. package/src/client/content/chart-engine.ts +33 -0
  27. package/src/client/content/chart.tsx +100 -0
  28. package/src/client/content/index.tsx +25 -4
  29. package/src/client/content/slide.tsx +238 -0
  30. package/src/client/content/video.tsx +126 -0
  31. package/src/client/shell/App.tsx +49 -15
  32. package/src/client/shell/Comments.tsx +94 -17
  33. package/src/client/shell/LockedApp.tsx +7 -2
  34. package/src/client/shell/Play.tsx +138 -24
  35. package/src/client/shell/Toolbar.tsx +12 -3
  36. package/src/client/shell/canvas/FrameNode.tsx +5 -3
  37. package/src/client/shell/comments-store.ts +59 -4
  38. package/src/client/shell/hash.ts +3 -1
  39. package/src/client/shell/icons.tsx +2 -0
  40. package/src/client/shell/mentions.ts +108 -1
  41. package/src/client/shell/play-order.ts +22 -0
  42. package/src/client/shell/sound.ts +32 -0
  43. package/src/client/shell/store.ts +41 -12
  44. package/src/client/shell/styles.css +58 -27
  45. package/src/client/stage/main.tsx +54 -3
  46. package/src/shared/events.ts +12 -3
  47. package/src/shared/utm.ts +3 -2
  48. package/templates/AGENTS-embedded.md +1 -0
  49. package/templates/AGENTS-studio.md +1 -0
  50. package/templates/instructions/publish.md +7 -0
  51. package/templates/instructions/reference/deck-layouts.md +230 -0
  52. package/templates/instructions/reference/deck-story.md +110 -0
  53. package/templates/instructions/slides.md +398 -0
@@ -1,50 +1,138 @@
1
1
  import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
2
2
  import { A as reconfirmGrant, C as commentAllowed, D as operativeMode, E as loadShare, F as setBlocked, I as setGeneralMode, L as shareState, M as resolveAccess, N as resolveRequest, O as provisionVerdict, P as rosterEtag, S as ceilingsFromRights, T as loadRequests, _ as sessionUser, a as claimInvite, b as updateProfile, c as issueDeviceSession, d as opaqueId, f as ownerName, g as sessionIsOperator, h as revokeUser, j as removeGrant, k as putRequest, l as loadStore, m as publicUser, o as createInvite, s as inviteInfo, u as normEmail, v as signIn, w as entryAllowed, y as signOut, z as upsertGrant } from "./auth-DdUKeVKt.mjs";
3
- import { canvasIdentity, deriveThreads, loadSeen, markSeen, signCanvasJws, unreadCount, verifyCanvasJws } from "./summary-Bf9tiGrE.mjs";
3
+ import { canvasIdentity, deriveThreads, loadSeen, markSeen, signCanvasJws, unreadCount, verifyCanvasJws } from "./summary-C7CAypbB.mjs";
4
4
  import { t as secureSuffix } from "./secure-cookie-_K1Hsx8H.mjs";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { createHash, randomBytes } from "node:crypto";
8
8
  //#region src/server/notify.ts
9
9
  /**
10
- * The canvas's half of the notification relay (04-solution §2.3, §9.5).
10
+ * The canvas's half of the notification relay (04-solution §2.3, §9.5; v1.1
11
+ * additions in 07-v1.1 §A/§B).
11
12
  *
12
- * Three transactional moments, exactly: you were invited (a view/comment grant
13
- * landed on an address), your request was approved, someone asked for access
14
- * (to the owner - and that mail carries only "someone"). Delivery is the ID
15
- * service's relay; the canvas never talks to Resend. The action token is
16
- * signed by the canvas identity key - owner authorization is implicit in the
17
- * signature, since only the canvas server holds it.
13
+ * Transactional moments: you were invited, your request was approved, someone
14
+ * asked for access. Activity moments (v1.1): a thread you are in moved
15
+ * (`reply`), somebody named you (`mentioned`). Delivery is the ID service's
16
+ * relay; the canvas never talks to Resend. The action token is signed by the
17
+ * canvas identity key - owner authorization is implicit in the signature,
18
+ * since only the canvas server holds it.
18
19
  *
19
20
  * Fire-and-forget, always: a notification that failed must never be the reason
20
- * a grant did not land. Idempotency lives at the relay, keyed on
21
- * (origin, template, eventId) - the eventId is derived from the STATE
22
- * TRANSITION, so a re-invite at the same role sends nothing and a role change
23
- * sends one.
21
+ * a comment or grant did not land - delivery is best-effort at this boundary,
22
+ * durable only once the relay's outbox row exists. Idempotency lives at the
23
+ * relay, keyed on (origin, template, eventId) - the eventId is derived from
24
+ * the STATE TRANSITION, so a re-invite at the same role sends nothing, a
25
+ * mention mails once per person per comment ever, and replies throttle to one
26
+ * mail per recipient per thread per window.
27
+ *
28
+ * All relay requests ride ONE promise chain: however wide a reply fans out,
29
+ * this process holds at most one in-flight relay request (each bounded by its
30
+ * own 5s timeout) - a comment-authorized caller cannot burst sockets.
24
31
  */
25
32
  const transitionId = (...parts) => createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 32);
26
- function relayNotify(ctx, template, recipient, eventId) {
33
+ let chain = Promise.resolve();
34
+ const capped = (s, cap) => {
35
+ const t = s?.replace(/\s+/g, " ").trim();
36
+ return t ? t.length > cap ? `${t.slice(0, cap - 1)}…` : t : void 0;
37
+ };
38
+ function relayNotify(ctx, template, recipient, eventId, content) {
27
39
  if (!ctx.enabled || !ctx.issuer || !ctx.origin) return;
28
40
  if (recipient.startsWith("@") || !recipient.includes("@")) return;
29
- const now = Math.floor(Date.now() / 1e3);
30
- let token;
31
- try {
32
- token = signCanvasJws(ctx.dataDir, {
33
- origin: ctx.origin,
34
- template,
35
- recipient,
36
- eventId,
37
- iat: now,
38
- exp: now + 600
39
- }, "marver-relay+jwt");
40
- } catch {
41
- return;
41
+ const { dataDir, origin } = ctx;
42
+ const actor = capped(content?.actor, 60);
43
+ const snippet = capped(content?.snippet, 180);
44
+ const link = content?.link && content.link.length <= 160 ? content.link : void 0;
45
+ const url = `${ctx.issuer.replace(/\/+$/, "")}/relay/notify`;
46
+ chain = chain.then(() => {
47
+ const now = Math.floor(Date.now() / 1e3);
48
+ let token;
49
+ try {
50
+ token = signCanvasJws(dataDir, {
51
+ origin,
52
+ template,
53
+ recipient,
54
+ eventId,
55
+ iat: now,
56
+ exp: now + 600,
57
+ ...actor ? { actor } : {},
58
+ ...snippet ? { snippet } : {},
59
+ ...link ? { link } : {}
60
+ }, "marver-relay+jwt");
61
+ } catch {
62
+ return;
63
+ }
64
+ return fetch(url, {
65
+ method: "POST",
66
+ headers: { authorization: `Bearer ${token}` },
67
+ signal: AbortSignal.timeout(5e3)
68
+ }).then(() => void 0, () => void 0);
69
+ });
70
+ }
71
+ const FRESH_MS = 9e5;
72
+ const REPLY_WINDOW_MS = 216e5;
73
+ const MAX_REPLY_RECIPIENTS = 10;
74
+ /**
75
+ * Mail the people a fresh comment batch concerns. `accepted` is what
76
+ * appendEvents actually took (never the raw POST body); `log` is the board's
77
+ * full event list including them; `allowed` is the resolver's answer for THIS
78
+ * board (≥ view), injected so this stays pure of store plumbing. Recipients
79
+ * are canonical emails - by the time events land here the write boundary has
80
+ * already inverted the projection.
81
+ *
82
+ * Windows use SERVER time, never ev.ts - a client-datable timestamp must not
83
+ * pick its own throttle bucket. The board is in every key because comment ids
84
+ * are only board-local.
85
+ */
86
+ function notifyCommentActivity(ctx, board, accepted, log, allowed) {
87
+ if (!ctx.enabled || !ctx.issuer || !ctx.origin) return;
88
+ for (const [eventId, job] of activityJobs(board, accepted, log, allowed, Date.now())) relayNotify(ctx, job.template, job.recipient, eventId, {
89
+ actor: job.actor,
90
+ snippet: job.snippet,
91
+ link: job.link
92
+ });
93
+ }
94
+ /** The pure half: which (template, recipient, transition) a batch owes. Split
95
+ * out so the fan-out rules are testable without a relay or a signing key. */
96
+ function activityJobs(board, accepted, log, allowed, now) {
97
+ const jobs = /* @__PURE__ */ new Map();
98
+ const threadLink = (threadId) => threadId ? `/#/b/${encodeURIComponent(board)}?c=${encodeURIComponent(threadId)}` : void 0;
99
+ for (const ev of accepted) {
100
+ if (ev.type !== "create" && ev.type !== "reply" || Math.abs(now - ev.ts) > FRESH_MS) continue;
101
+ const author = ev.author?.email?.toLowerCase();
102
+ const mentioned = /* @__PURE__ */ new Set();
103
+ for (const m of ev.mentions ?? []) {
104
+ const email = m.email?.toLowerCase();
105
+ if (!email || email === author || !allowed(email)) continue;
106
+ mentioned.add(email);
107
+ jobs.set(transitionId("mention", email, board, ev.commentId ?? ev.id), {
108
+ template: "mentioned",
109
+ recipient: email,
110
+ actor: ev.author?.name,
111
+ snippet: ev.body,
112
+ link: threadLink(ev.type === "reply" ? ev.parentId : ev.commentId)
113
+ });
114
+ }
115
+ if (ev.type !== "reply" || !ev.parentId) continue;
116
+ const recent = [];
117
+ for (let i = log.length - 1; i >= 0 && recent.length < MAX_REPLY_RECIPIENTS; i--) {
118
+ const p = log[i];
119
+ if (p.id === ev.id) continue;
120
+ const email = (p.type === "reply" ? p.parentId === ev.parentId : p.type === "create" && p.commentId === ev.parentId) ? p.author?.email?.toLowerCase() : void 0;
121
+ if (email && email !== author && !mentioned.has(email) && !recent.includes(email)) recent.push(email);
122
+ }
123
+ const window = String(Math.floor(now / REPLY_WINDOW_MS));
124
+ for (const email of recent) {
125
+ if (!allowed(email)) continue;
126
+ jobs.set(transitionId("reply", email, board, ev.parentId, window), {
127
+ template: "reply",
128
+ recipient: email,
129
+ actor: ev.author?.name,
130
+ snippet: ev.body,
131
+ link: threadLink(ev.parentId)
132
+ });
133
+ }
42
134
  }
43
- fetch(`${ctx.issuer.replace(/\/+$/, "")}/relay/notify`, {
44
- method: "POST",
45
- headers: { authorization: `Bearer ${token}` },
46
- signal: AbortSignal.timeout(5e3)
47
- }).catch(() => {});
135
+ return jobs;
48
136
  }
49
137
  //#endregion
50
138
  //#region src/server/collab.ts
@@ -97,15 +185,41 @@ function validAvatar(s) {
97
185
  webp: (b) => b.toString("latin1", 0, 4) === "RIFF" && b.toString("latin1", 8, 12) === "WEBP"
98
186
  }[m[1]]?.(head) ?? false;
99
187
  }
188
+ const EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
189
+ const OPAQUE_ID = /^[0-9a-f]{24}$/;
190
+ /** Mention shape rules (07-v1.1 §B). One generic refusal for every violation -
191
+ * no per-cause oracle. The browser form carries the opaque `id` and NEVER an
192
+ * email (a browser-supplied email is a membership probe); the operator form
193
+ * (CLI sync - canonical bytes) carries `email` and never an id. */
194
+ function badMentions(ev, operator) {
195
+ if (ev.mentions === void 0) return false;
196
+ if (ev.type !== "create" && ev.type !== "reply") return true;
197
+ if (!Array.isArray(ev.mentions) || ev.mentions.length > 8) return true;
198
+ const seen = /* @__PURE__ */ new Set();
199
+ for (const m of ev.mentions) {
200
+ if (!m || typeof m !== "object" || Object.keys(m).some((k) => k !== "id" && k !== "email" && k !== "label")) return true;
201
+ if (typeof m.label !== "string" || !m.label.trim() || m.label.length > 80 || /^@?marver\b/i.test(m.label.trim())) return true;
202
+ const who = operator ? m.email : m.id;
203
+ if (operator ? typeof m.email !== "string" || !EMAIL_SHAPE.test(m.email) || m.email.length > 254 || m.id !== void 0 : typeof m.id !== "string" || !OPAQUE_ID.test(m.id) || m.email !== void 0) return true;
204
+ const key = who.toLowerCase();
205
+ if (seen.has(key)) return true;
206
+ seen.add(key);
207
+ }
208
+ return false;
209
+ }
100
210
  /** Reject anything the log must not absorb. Returns an error string or null.
101
211
  * Events are append-only and synced by id, so acceptance is forever - validate hard. */
102
- function validateEvents(incoming, log, u, board) {
212
+ function validateEvents(incoming, log, u, board, opts = {}) {
103
213
  const now = Date.now();
104
214
  const creates = /* @__PURE__ */ new Map();
215
+ const comments = /* @__PURE__ */ new Set();
105
216
  const authors = /* @__PURE__ */ new Map();
106
217
  for (const ev of log) {
107
218
  if (ev.type === "create" && ev.commentId && !creates.has(ev.commentId)) creates.set(ev.commentId, ev);
108
- if ((ev.type === "create" || ev.type === "reply") && ev.commentId && ev.author?.email && !authors.has(ev.commentId)) authors.set(ev.commentId, ev.author.email.toLowerCase());
219
+ if ((ev.type === "create" || ev.type === "reply") && ev.commentId) {
220
+ comments.add(ev.commentId);
221
+ if (ev.author?.email && !authors.has(ev.commentId)) authors.set(ev.commentId, ev.author.email.toLowerCase());
222
+ }
109
223
  }
110
224
  const me = u.email.toLowerCase();
111
225
  for (const ev of incoming) {
@@ -117,7 +231,9 @@ function validateEvents(incoming, log, u, board) {
117
231
  if (typeof ev.ts !== "number" || ev.ts < 15778368e5 || ev.ts > now + 6e4) return "event timestamp out of bounds";
118
232
  if (ev.board !== void 0 && ev.board !== board) return "event board does not match the endpoint";
119
233
  if (ev.body !== void 0 && (typeof ev.body !== "string" || ev.body.length > MAX_BODY_TEXT)) return "body too long";
120
- if ((ev.type === "create" || ev.type === "reply" || ev.type === "react" || ev.type === "edit" || ev.type === "reanchor") && ev.author?.email?.toLowerCase() !== me) return "event author must be the signed-in account";
234
+ if ((ev.type === "create" || ev.type === "reply") && !ev.body?.trim()) return "a comment needs a body";
235
+ if (badMentions(ev, !!opts.operator)) return "invalid mentions";
236
+ if ((ev.type === "create" || ev.type === "reply" || ev.type === "react" || ev.type === "edit" || ev.type === "reanchor" || ev.type === "profile") && ev.author?.email?.toLowerCase() !== me) return "event author must be the signed-in account";
121
237
  if (typeof ev.commentId !== "string" || !ID_RE.test(ev.commentId)) {
122
238
  if (ev.type !== "profile") return "event needs a commentId";
123
239
  }
@@ -125,10 +241,13 @@ function validateEvents(incoming, log, u, board) {
125
241
  case "create":
126
242
  if (creates.has(ev.commentId)) return "a thread with that id already exists";
127
243
  creates.set(ev.commentId, ev);
244
+ comments.add(ev.commentId);
128
245
  if (ev.author?.email) authors.set(ev.commentId, ev.author.email.toLowerCase());
129
246
  break;
130
247
  case "reply":
131
248
  if (typeof ev.parentId !== "string" || !creates.has(ev.parentId) && !incoming.some((x) => x.type === "create" && x.commentId === ev.parentId)) return "reply parent does not exist";
249
+ if (comments.has(ev.commentId)) return "a comment with that id already exists";
250
+ comments.add(ev.commentId);
132
251
  if (ev.author?.email) authors.set(ev.commentId, ev.author.email.toLowerCase());
133
252
  break;
134
253
  case "edit": {
@@ -238,15 +357,67 @@ function collabHandler(dataDir, distDir) {
238
357
  return hit;
239
358
  };
240
359
  const project = (ev) => {
241
- if (!ev.author?.email) return ev;
242
- const { email, ...rest } = ev.author;
243
- return {
244
- ...ev,
245
- author: {
246
- ...rest,
247
- id: opaqueId(dataDir, email)
248
- }
360
+ let out = ev;
361
+ if (ev.author?.email) {
362
+ const { email, ...rest } = ev.author;
363
+ out = {
364
+ ...out,
365
+ author: {
366
+ ...rest,
367
+ id: opaqueId(dataDir, email)
368
+ }
369
+ };
370
+ }
371
+ if (ev.mentions !== void 0) {
372
+ const safe = Array.isArray(ev.mentions) ? ev.mentions.flatMap((m) => {
373
+ if (!m || typeof m !== "object") return [];
374
+ const label = typeof m.label === "string" ? m.label : "";
375
+ if (typeof m.email === "string") return [{
376
+ id: opaqueId(dataDir, m.email),
377
+ label
378
+ }];
379
+ if (typeof m.id === "string") return [{
380
+ id: m.id,
381
+ label
382
+ }];
383
+ return [];
384
+ }) : [];
385
+ out = {
386
+ ...out,
387
+ mentions: safe.length ? safe : void 0
388
+ };
389
+ }
390
+ return out;
391
+ };
392
+ /** Every opaque id the canvas can resolve back to an address: accounts, plus
393
+ * every author and mention across the boards' logs. Broader than any one
394
+ * composer can see, and safely so - ids are HMACs under the canvas secret,
395
+ * so a valid id for an unseen person cannot be manufactured to probe with. */
396
+ const mentionTargets = () => {
397
+ const map = /* @__PURE__ */ new Map();
398
+ const add = (email) => {
399
+ if (email) map.set(opaqueId(dataDir, email), normEmail(email));
249
400
  };
401
+ for (const usr of loadStore(dataDir).users) add(usr.email);
402
+ for (const st of threads.values()) for (const ev of st.events) {
403
+ add(ev.author?.email);
404
+ for (const mn of ev.mentions ?? []) add(mn.email);
405
+ }
406
+ return map;
407
+ };
408
+ /** May this address still read this board - the activity-mail eligibility
409
+ * filter, asked at enqueue time (delivery cannot re-check; 07-v1.1 §A). */
410
+ const mailAllowed = (board, email) => {
411
+ const store = shareState(dataDir);
412
+ if (!store) return true;
413
+ const owner = loadStore(dataDir).users.find((x) => x.role === "owner");
414
+ const userRole = owner && normEmail(owner.email) === normEmail(email) ? "owner" : "member";
415
+ return (resolveAccess({
416
+ email,
417
+ userRole,
418
+ store,
419
+ ceilings
420
+ }).boards[board] ?? "none") !== "none";
250
421
  };
251
422
  const rawTransport = (req) => {
252
423
  const tok = /^Bearer ([\w-]+)$/.exec(String(req.headers.authorization ?? ""))?.[1];
@@ -761,14 +932,32 @@ function collabHandler(dataDir, distDir) {
761
932
  const b = await readBody(req);
762
933
  const incoming = Array.isArray(b.events) ? b.events : [];
763
934
  if (incoming.length > 100) return json(res, 400, { error: "too many events in one push" }), true;
764
- const bad = validateEvents(incoming, readLog(commentsDir, m[1]), u, m[1]);
935
+ const operator = rawTransport(req);
936
+ const bad = validateEvents(incoming, readLog(commentsDir, m[1]), u, m[1], { operator });
765
937
  if (bad) return json(res, 400, { error: bad }), true;
938
+ if (!operator && incoming.some((ev) => ev.mentions?.length)) {
939
+ const targets = mentionTargets();
940
+ for (const ev of incoming) {
941
+ if (!ev.mentions?.length) continue;
942
+ const mapped = [];
943
+ for (const mn of ev.mentions) {
944
+ const email = targets.get(mn.id);
945
+ if (!email || email === normEmail(u.email)) return json(res, 400, { error: "invalid mentions" }), true;
946
+ mapped.push({
947
+ email,
948
+ label: mn.label
949
+ });
950
+ }
951
+ ev.mentions = mapped;
952
+ }
953
+ }
766
954
  const fresh = appendEvents(commentsDir, m[1], incoming);
767
955
  if (fresh.length) {
768
956
  const st = threads.get(m[1]);
769
957
  threads.set(m[1], deriveThreads([...st?.events ?? [], ...fresh]));
770
958
  }
771
959
  broadcast(m[1], fresh.map(project));
960
+ if (fresh.length) notifyCommentActivity(notifyCtx, m[1], fresh, threads.get(m[1])?.events ?? [], (email) => mailAllowed(m[1], email));
772
961
  return json(res, 200, { accepted: fresh.length }), true;
773
962
  }
774
963
  return false;
@@ -1,8 +1,8 @@
1
1
  import { n as NAME } from "./cli.mjs";
2
- import { n as replay } from "./events-C9k-ceOj.mjs";
2
+ import { n as replay } from "./events-B3LBn74P.mjs";
3
3
  import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
4
- import { a as syncOnce, i as loadCollab, n as connectClaim, r as connectToken, t as connect } from "./sync-WYxDN9IT.mjs";
5
- import { n as localProfile } from "./profile-ChpTPd-X.mjs";
4
+ import { a as syncOnce, i as loadCollab, n as connectClaim, r as connectToken, t as connect } from "./sync-BZaCWqK-.mjs";
5
+ import { n as localProfile } from "./profile-BjAPAJSb.mjs";
6
6
  import { join } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { randomUUID } from "node:crypto";
@@ -1,7 +1,7 @@
1
- import { n as replay } from "./events-C9k-ceOj.mjs";
1
+ import { n as replay } from "./events-B3LBn74P.mjs";
2
2
  import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
3
- import { n as localProfile } from "./profile-ChpTPd-X.mjs";
4
- import { i as toFrameId } from "./manifest-CS6krOTe.mjs";
3
+ import { n as localProfile } from "./profile-BjAPAJSb.mjs";
4
+ import { i as toFrameId } from "./manifest-DvOmglFp.mjs";
5
5
  import { workActivity } from "./work-CLrmY-vQ.mjs";
6
6
  import { r as deviceId, t as has } from "./ledger-Bu0BjqIe.mjs";
7
7
  import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import { n as NAME, r as PKG } from "./cli.mjs";
2
- import { l as detectHost, s as loadConfig } from "./manifest-CS6krOTe.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-Boy7I_JH.mjs";
2
+ import { l as detectHost, s as loadConfig } from "./manifest-DvOmglFp.mjs";
3
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BsmG5i2X.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";
@@ -247,7 +247,7 @@ async function dev(root, portFlag) {
247
247
  console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
248
248
  try {
249
249
  const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
250
- const { installedVersion } = await import("./plugin-Boy7I_JH.mjs").then((n) => n.i);
250
+ const { installedVersion } = await import("./plugin-BsmG5i2X.mjs").then((n) => n.i);
251
251
  const stale = staleManagedInstructions(root);
252
252
  if (stale.length) {
253
253
  const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
@@ -255,7 +255,7 @@ async function dev(root, portFlag) {
255
255
  console.log(` instructions predate ${v ? NAME + " " + v : "the installed version"} (${shown}) - run: npx ${NAME} init\n`);
256
256
  }
257
257
  } catch {}
258
- const { loadCollab, syncOnce } = await import("./sync-WYxDN9IT.mjs").then((n) => n.o);
258
+ const { loadCollab, syncOnce } = await import("./sync-BZaCWqK-.mjs").then((n) => n.o);
259
259
  if (loadCollab(root)) console.log(` comments: syncing with the published canvas\n`);
260
260
  let syncing = false;
261
261
  const tick = async () => {
@@ -285,7 +285,7 @@ async function dev(root, portFlag) {
285
285
  });
286
286
  }
287
287
  if (config.jam) {
288
- const { startJam } = await import("./daemon-WjonKlPz.mjs");
288
+ const { startJam } = await import("./daemon-Bfucyf1o.mjs");
289
289
  const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
290
290
  if (jam) {
291
291
  const close = server.close.bind(server);
@@ -26,6 +26,7 @@ function replay(events) {
26
26
  resolved: false,
27
27
  agent: ev.agent,
28
28
  agentMeta: ev.agentMeta,
29
+ mentions: ev.mentions,
29
30
  replies: [],
30
31
  reactions: {}
31
32
  });
@@ -40,7 +41,8 @@ function replay(events) {
40
41
  body: ev.body,
41
42
  ts: ev.ts,
42
43
  agent: ev.agent,
43
- agentMeta: ev.agentMeta
44
+ agentMeta: ev.agentMeta,
45
+ mentions: ev.mentions
44
46
  });
45
47
  break;
46
48
  }
@@ -1,5 +1,5 @@
1
1
  import { n as NAME } from "./cli.mjs";
2
- import { a as writeManifest, c as detectAgent, l as detectHost, o as DEFAULTS, r as scanFrames, u as readJson } from "./manifest-CS6krOTe.mjs";
2
+ import { a as writeManifest, c as detectAgent, l as detectHost, o as DEFAULTS, r as scanFrames, u as readJson } from "./manifest-DvOmglFp.mjs";
3
3
  import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, managedFile, pkgDir } from "./managed-HwHVNI3h.mjs";
4
4
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join, relative } from "node:path";
@@ -143,7 +143,71 @@ function init(root, opts) {
143
143
  refresh("providers.tsx", [providersTemplate(null, null), providersTemplate(null, host.toaster, host.routerPkg)], providersTemplate(host.router, host.toaster, host.routerPkg));
144
144
  refresh("tsconfig.json", [STANDALONE_TSCONFIG], tsconfigNow());
145
145
  }
146
- write(".gitignore", ".local/\n.dist/\n.dist-seeds/\n");
146
+ write(".gitignore", ".local/\n.dist/\n.dist-seeds/\nslides-inspiration/\n");
147
+ {
148
+ const gi = join(design, ".gitignore");
149
+ const cur = readFileSync(gi, "utf8");
150
+ if (!cur.split("\n").some((l) => l.trim() === "slides-inspiration/" || l.trim() === "slides-inspiration")) {
151
+ writeFileSync(gi, cur + (cur.endsWith("\n") || cur === "" ? "" : "\n") + "slides-inspiration/\n");
152
+ created.push("design/.gitignore (+ slides-inspiration/)");
153
+ }
154
+ }
155
+ write("slides.md", [
156
+ "# This project's slide layouts and house rules",
157
+ "",
158
+ "The agent reads BOTH the shipped doctrine (instructions/slides.md) and this",
159
+ "file - and this file WINS where they disagree. Add your own layouts, house",
160
+ "rules, and banned moves here; it is yours and marver never touches it.",
161
+ "",
162
+ "To grow it from examples: drop decks (PPTX, PDF, screenshots) into",
163
+ "design/slides-inspiration/ and ask the agent to \"study my inspiration\" -",
164
+ "it will propose additions here for your review.",
165
+ "",
166
+ "## The deck look",
167
+ "",
168
+ "How a deck wears this brand. The agent drafts it from DESIGN.md and",
169
+ "theme.css on the first deck (a reviewed edit, building on provisionally",
170
+ "with the theme's tokens); anything it cannot settle stays `TBD` for you.",
171
+ "Each line shows the shape with an example - replace the example, keep the",
172
+ "shape.",
173
+ "",
174
+ "- **Tokens** (`--marver-slide-*` in theme.css, read by the Slide root):",
175
+ " ground / ink / muted, each with a `-dark` twin; accent, one value for",
176
+ " both themes. e.g. ground = the page background, ink = the heading",
177
+ " colour, accent = the primary button.",
178
+ "- **Type** (`--marver-slide-font`): one family, the theme's; the type",
179
+ " roles carry their weights. e.g. Inter.",
180
+ "- **The mark**: which asset, where, how big. e.g. wordmark SVG, bottom-left",
181
+ " of the cover and the closing only, 120px wide - never on content slides.",
182
+ "- **Colour meaning** (charts, cards, badges): hue = category, fixed across",
183
+ " the deck. e.g. accent = us, slate = competitors, muted = the baseline;",
184
+ " green / amber / red reserved for status.",
185
+ "- **Backgrounds allowed**: e.g. the flat ground; a theme gradient on covers",
186
+ " and sections only; no photos behind text without a scrim.",
187
+ "- **Imagery**: e.g. product screenshots on a device-less frame, real people,",
188
+ " no stock; illustrations in the product's line style only. Evidentiary",
189
+ " images (a report fragment, a log line, a reasoning trace, a real",
190
+ " screen) are evidence objects, not decoration - hero material when the",
191
+ " message warrants it.",
192
+ "- **Tempo** (`--marver-slide-tempo`): e.g. 350ms - one value, every deck.",
193
+ "- **Numbers**: e.g. $ and k / M (lowercase k), fiscal years as FY26,",
194
+ " negatives in brackets, one decimal on percentages.",
195
+ "- **Voice**: three words the deck sounds like, and the words it never",
196
+ " uses. e.g. direct, warm, specific; never \"leverage\", \"seamless\", \"journey\".",
197
+ "- **Terminology**: user-facing word → never-shown internal word.",
198
+ " e.g. \"Comments\" → \"Enrichment\".",
199
+ "- **End card**: yes / no. e.g. yes - the mark on the ground, no text; the",
200
+ " ask lives on the slide before it.",
201
+ "",
202
+ "## Layouts",
203
+ "",
204
+ "(none yet - the shipped recipe list and atlas apply)",
205
+ "",
206
+ "## House rules",
207
+ "",
208
+ "(none yet)",
209
+ ""
210
+ ].join("\n"));
147
211
  write(".gitattributes", "comments/*.jsonl merge=union\n");
148
212
  write("scenes/_layout.tsx", readFileSync(join(templates, "root-layout.tsx"), "utf8"));
149
213
  if (!existsSync(join(design, "boards"))) {
@@ -382,6 +382,10 @@ function extractMeta(src) {
382
382
  const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(['"\`])([^'"\`]*)\\1`).exec(body);
383
383
  return r ? r[2] : void 0;
384
384
  };
385
+ const pickBool = (key) => {
386
+ const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(true|false)\\b`).exec(body);
387
+ return r ? r[1] === "true" : void 0;
388
+ };
385
389
  const out = {};
386
390
  const title = pick("title");
387
391
  if (title) out.title = title;
@@ -395,6 +399,8 @@ function extractMeta(src) {
395
399
  if (variant) out.variant = variant;
396
400
  const intent = pick("intent");
397
401
  if (intent) out.intent = intent;
402
+ const slide = pickBool("slide");
403
+ if (slide !== void 0) out.slide = slide;
398
404
  return out;
399
405
  }
400
406
  /** Content-frame detection: LEXICAL by convention - the frame file
@@ -484,6 +490,7 @@ function scanFrames(root) {
484
490
  if (meta.theme) entry.theme = meta.theme;
485
491
  if (meta.of) entry.variantGroup = meta.of;
486
492
  if (meta.variant) entry.variant = meta.variant;
493
+ if (meta.slide) entry.slide = true;
487
494
  const content = contentScan(src);
488
495
  if (content || meta.intent) {
489
496
  entry.intent = meta.intent ?? content.intent;
@@ -1,6 +1,6 @@
1
1
  import { i as avatarSourceFor, n as attachAvatar, p as provisionFromMarverId } from "./auth-DdUKeVKt.mjs";
2
2
  import { TransactionStore, browserBinding, verifyAssertion } from "./marver-id-CVCoaMGI.mjs";
3
- import { t as poweredByUrl } from "./serve-BeLLizNg.mjs";
3
+ import { t as poweredByUrl } from "./serve-Bcwfpvhl.mjs";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { isIP } from "node:net";
6
6
  import { lookup } from "node:dns/promises";
@@ -422,7 +422,7 @@ function marverIdHandler(dir, issuer, canvasName, branding = true, ceilings = {}
422
422
  const { shareState, provisionVerdict } = await import("./auth-DdUKeVKt.mjs").then((n) => n.R);
423
423
  const share = shareState(dir);
424
424
  if (!(share ? provisionVerdict(share, result.identity.email) === "blocked" : false)) {
425
- const { signCanvasJws } = await import("./summary-Bf9tiGrE.mjs");
425
+ const { signCanvasJws } = await import("./summary-C7CAypbB.mjs");
426
426
  const now = Math.floor(Date.now() / 1e3);
427
427
  request = signCanvasJws(dir, {
428
428
  aud: origin,
@@ -545,9 +545,12 @@ function finishPage(canvasName, host, switchUrl, branding = true) {
545
545
  p.lead { margin: 0; padding: 0 4px; font-size: 12.5px; line-height: 1.5;
546
546
  color: rgba(24,24,27,.66) }
547
547
  p.lead strong { color: #18181b; font-weight: 600 }
548
- a.cta { height: 40px; border-radius: 999px; background: #18181b; color: #fafafa;
549
- font: 600 13px -apple-system, system-ui, sans-serif; text-decoration: none;
550
- display: flex; align-items: center; justify-content: center }
548
+ /* .cta, not a.cta: the request-access <button> wears the same dark pill as the
549
+ switch-account <a> - scoping to the tag left the button naked (a flat native
550
+ strip that read as disabled) */
551
+ .cta { height: 40px; border-radius: 999px; background: #18181b; color: #fafafa;
552
+ font: 600 13px -apple-system, system-ui, sans-serif; text-decoration: none;
553
+ display: flex; align-items: center; justify-content: center }
551
554
  footer a { display: inline-flex; align-items: center; gap: 7px;
552
555
  font: 600 12.5px -apple-system, system-ui, sans-serif;
553
556
  color: rgba(24,24,27,.5); text-decoration: none }
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
3
- import { n as localProfile, t as isConnected } from "./profile-ChpTPd-X.mjs";
4
- import { a as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-CS6krOTe.mjs";
3
+ import { n as localProfile, t as isConnected } from "./profile-BjAPAJSb.mjs";
4
+ import { a as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-DvOmglFp.mjs";
5
5
  import { copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
6
6
  import { basename, dirname, join, resolve, sep } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
@@ -125,6 +125,24 @@ function apiMiddleware(root, opts = {}) {
125
125
  const path = url.pathname.slice(`${ROUTE}/api/`.length);
126
126
  if (req.method === "GET" && !cookie(req, "mv_c")) res.setHeader("set-cookie", `mv_c=${randomBytes(16).toString("base64url")}; Path=/; Max-Age=${CSRF_MAX_AGE}; SameSite=Lax`);
127
127
  try {
128
+ if (path === "policy" && req.method === "GET") try {
129
+ const raw = JSON.parse(readFileSync(join(root, "design", "publish.json"), "utf8"));
130
+ const boards = {};
131
+ for (const [n, level] of Object.entries(raw?.boards ?? {})) {
132
+ if (typeof level !== "object" || level === null) continue;
133
+ const p2 = level;
134
+ boards[n] = {
135
+ ...typeof p2.type === "string" ? { type: p2.type } : {},
136
+ ...typeof p2.open === "string" ? { open: p2.open } : {},
137
+ ...p2.lock === true ? { lock: true } : {},
138
+ ...typeof p2.transition === "string" ? { transition: p2.transition } : {},
139
+ ...typeof p2.chrome === "string" ? { chrome: p2.chrome } : {}
140
+ };
141
+ }
142
+ return json(res, 200, { boards });
143
+ } catch {
144
+ return json(res, 200, { boards: {} });
145
+ }
128
146
  if (path === "boards" && req.method === "GET") {
129
147
  mkdirSync(boardsDir, { recursive: true });
130
148
  return json(res, 200, readdirSync(boardsDir).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp")).map((f) => {
@@ -266,7 +284,7 @@ function apiMiddleware(root, opts = {}) {
266
284
  const frameId = url.searchParams.get("frame") ?? "";
267
285
  const theme = url.searchParams.get("theme") ?? "light";
268
286
  const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
269
- const { shootFrame } = await import("./shot-Cyv3GN79.mjs");
287
+ const { shootFrame } = await import("./shot-kbR_xzJH.mjs");
270
288
  const r = await shootFrame({
271
289
  root,
272
290
  viewports: opts.viewports ?? {},
@@ -306,7 +324,7 @@ function apiMiddleware(root, opts = {}) {
306
324
  }
307
325
  const dir = join(root, "design", ".local");
308
326
  mkdirSync(dir, { recursive: true });
309
- const { validAvatar } = await import("./collab-BmaOjwP1.mjs");
327
+ const { validAvatar } = await import("./collab-mAiNZlDj.mjs");
310
328
  let cur = {};
311
329
  try {
312
330
  cur = JSON.parse(readFileSync(join(dir, "profile.json"), "utf8"));
@@ -397,7 +415,7 @@ function apiMiddleware(root, opts = {}) {
397
415
  }
398
416
  }
399
417
  if (path === "sync" && req.method === "POST") {
400
- const { loadCollab, syncOnce } = await import("./sync-WYxDN9IT.mjs").then((n) => n.o);
418
+ const { loadCollab, syncOnce } = await import("./sync-BZaCWqK-.mjs").then((n) => n.o);
401
419
  const collab = loadCollab(root);
402
420
  if (!collab) return json(res, 200, { connected: false });
403
421
  try {
@@ -426,7 +444,7 @@ function backgroundPush(root) {
426
444
  pushTimer = setTimeout(async () => {
427
445
  pushTimer = null;
428
446
  try {
429
- const { loadCollab, syncOnce } = await import("./sync-WYxDN9IT.mjs").then((n) => n.o);
447
+ const { loadCollab, syncOnce } = await import("./sync-BZaCWqK-.mjs").then((n) => n.o);
430
448
  const collab = loadCollab(root);
431
449
  if (collab) await syncOnce(root, collab);
432
450
  } catch {}
@@ -799,7 +817,7 @@ function marverPlugin(ctx) {
799
817
  });
800
818
  return;
801
819
  }
802
- const { shootFrame } = await import("./shot-Cyv3GN79.mjs");
820
+ const { shootFrame } = await import("./shot-kbR_xzJH.mjs");
803
821
  write(await shootFrame({
804
822
  root,
805
823
  viewports: config.viewports,