@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.
- package/CHANGELOG.md +124 -0
- package/README.md +41 -19
- package/dist/{build-DmfxJc6L.mjs → build-ByYafIhj.mjs} +37 -5
- package/dist/cli.mjs +21 -7
- package/dist/{collab-BmaOjwP1.mjs → collab-mAiNZlDj.mjs} +232 -43
- package/dist/{comments-pNxjTrGN.mjs → comments-DHB_8BRa.mjs} +3 -3
- package/dist/{daemon-WjonKlPz.mjs → daemon-Bfucyf1o.mjs} +3 -3
- package/dist/{dev-BXq7yE1p.mjs → dev-yZMyQeUj.mjs} +5 -5
- package/dist/{events-C9k-ceOj.mjs → events-B3LBn74P.mjs} +3 -1
- package/dist/{init-BpitOqRQ.mjs → init-Dvaso7YO.mjs} +66 -2
- package/dist/{manifest-CS6krOTe.mjs → manifest-DvOmglFp.mjs} +7 -0
- package/dist/{marver-id-gate-DOjnZakC.mjs → marver-id-gate-D6By7XHj.mjs} +8 -5
- package/dist/{plugin-Boy7I_JH.mjs → plugin-BsmG5i2X.mjs} +25 -7
- package/dist/{profile-ChpTPd-X.mjs → profile-BjAPAJSb.mjs} +1 -1
- package/dist/{serve-BeLLizNg.mjs → serve-Bcwfpvhl.mjs} +4 -3
- package/dist/{share-C64b1tdN.mjs → share-Gqo_Ygqw.mjs} +14 -15
- package/dist/{shot-Cyv3GN79.mjs → shot-kbR_xzJH.mjs} +7 -1
- package/dist/{summary-Bf9tiGrE.mjs → summary-C7CAypbB.mjs} +1 -1
- package/dist/{sync-WYxDN9IT.mjs → sync-BZaCWqK-.mjs} +1 -1
- package/docs/live-jam.md +173 -0
- package/docs/publish.md +270 -0
- package/docs/sharing.md +333 -0
- package/docs/slides.md +134 -0
- package/package.json +3 -1
- package/src/client/const.ts +13 -0
- package/src/client/content/chart-engine.ts +33 -0
- package/src/client/content/chart.tsx +100 -0
- package/src/client/content/index.tsx +25 -4
- package/src/client/content/slide.tsx +238 -0
- package/src/client/content/video.tsx +126 -0
- package/src/client/shell/App.tsx +49 -15
- package/src/client/shell/Comments.tsx +94 -17
- package/src/client/shell/LockedApp.tsx +7 -2
- package/src/client/shell/Play.tsx +138 -24
- package/src/client/shell/Toolbar.tsx +12 -3
- package/src/client/shell/canvas/FrameNode.tsx +5 -3
- package/src/client/shell/comments-store.ts +59 -4
- package/src/client/shell/hash.ts +3 -1
- package/src/client/shell/icons.tsx +2 -0
- package/src/client/shell/mentions.ts +108 -1
- package/src/client/shell/play-order.ts +22 -0
- package/src/client/shell/sound.ts +32 -0
- package/src/client/shell/store.ts +41 -12
- package/src/client/shell/styles.css +58 -27
- package/src/client/stage/main.tsx +54 -3
- package/src/shared/events.ts +12 -3
- package/src/shared/utm.ts +3 -2
- package/templates/AGENTS-embedded.md +1 -0
- package/templates/AGENTS-studio.md +1 -0
- package/templates/instructions/publish.md +7 -0
- package/templates/instructions/reference/deck-layouts.md +230 -0
- package/templates/instructions/reference/deck-story.md +110 -0
- package/templates/instructions/slides.md +398 -0
|
@@ -1,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-
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* (
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* sends
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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-
|
|
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-
|
|
5
|
-
import { n as localProfile } from "./profile-
|
|
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-
|
|
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-
|
|
4
|
-
import { i as toFrameId } from "./manifest-
|
|
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-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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-
|
|
4
|
-
import { a as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
820
|
+
const { shootFrame } = await import("./shot-kbR_xzJH.mjs");
|
|
803
821
|
write(await shootFrame({
|
|
804
822
|
root,
|
|
805
823
|
viewports: config.viewports,
|