@marver-design/marver 0.11.0 → 0.12.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 +83 -5
- package/README.md +17 -4
- package/dist/auth-DdUKeVKt.mjs +1150 -0
- package/dist/{build-Cr_ZLMdq.mjs → build-DmfxJc6L.mjs} +188 -33
- package/dist/cli.mjs +16 -7
- package/dist/collab-BmaOjwP1.mjs +781 -0
- package/dist/{comments-CRqaO2MH.mjs → comments-pNxjTrGN.mjs} +3 -2
- package/dist/{daemon-DSmaS453.mjs → daemon-WjonKlPz.mjs} +3 -3
- package/dist/{dev-CLBjRs0J.mjs → dev-BXq7yE1p.mjs} +5 -5
- package/dist/events-C9k-ceOj.mjs +101 -0
- package/dist/{init-BSclDg4I.mjs → init-BpitOqRQ.mjs} +2 -3
- package/dist/{config-t9coJ-Pq.mjs → manifest-CS6krOTe.mjs} +236 -4
- package/dist/{marver-id-B8-3WiHk.mjs → marver-id-CVCoaMGI.mjs} +106 -1
- package/dist/{marver-id-gate-BDzW6ahN.mjs → marver-id-gate-DOjnZakC.mjs} +94 -6
- package/dist/{plugin-vE7sxEeT.mjs → plugin-Boy7I_JH.mjs} +82 -9
- package/dist/{profile-DcsJyppw.mjs → profile-ChpTPd-X.mjs} +1 -1
- package/dist/secure-cookie-_K1Hsx8H.mjs +26 -0
- package/dist/{serve-CZ1KEQxd.mjs → serve-BeLLizNg.mjs} +72 -18
- package/dist/share-C64b1tdN.mjs +159 -0
- package/dist/summary-Bf9tiGrE.mjs +211 -0
- package/dist/{sync-DELGomPk.mjs → sync-WYxDN9IT.mjs} +3 -102
- package/package.json +1 -1
- package/src/client/shell/App.tsx +43 -8
- package/src/client/shell/Comments.tsx +9 -5
- package/src/client/shell/LockedApp.tsx +113 -0
- package/src/client/shell/Play.tsx +97 -18
- package/src/client/shell/Toolbar.tsx +26 -2
- package/src/client/shell/canvas/Canvas.tsx +5 -15
- package/src/client/shell/canvas/ctl.ts +26 -0
- package/src/client/shell/comments-store.ts +39 -9
- package/src/client/shell/hash.ts +19 -0
- package/src/client/shell/icons.tsx +1 -0
- package/src/client/shell/store.ts +54 -3
- package/src/client/shell/styles.css +27 -0
- package/src/shared/events.ts +8 -3
- package/templates/instructions/publish.md +15 -1
- package/dist/auth-B5yuwnOq.mjs +0 -494
- package/dist/collab-C-n-uQdm.mjs +0 -364
- package/dist/manifest-C2tzkNaC.mjs +0 -233
- package/dist/update-DuWDj5nR.mjs +0 -77
- package/dist/utm-CxC3QN5X.mjs +0 -20
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { n as replay } from "./events-C9k-ceOj.mjs";
|
|
3
3
|
import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
|
|
4
|
-
import { n as
|
|
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";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { createInterface } from "node:readline";
|
|
7
8
|
import { randomUUID } from "node:crypto";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as replay } from "./events-C9k-ceOj.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-ChpTPd-X.mjs";
|
|
4
|
+
import { i as toFrameId } from "./manifest-CS6krOTe.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 {
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
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";
|
|
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("./
|
|
250
|
+
const { installedVersion } = await import("./plugin-Boy7I_JH.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-WYxDN9IT.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-WjonKlPz.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);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//#region src/shared/events.ts
|
|
2
|
+
/** Events the other side lacks, by id - the sync payload in either direction. */
|
|
3
|
+
function diffEvents(mine, theirIds) {
|
|
4
|
+
const have = new Set(theirIds);
|
|
5
|
+
return mine.filter((e) => !have.has(e.id));
|
|
6
|
+
}
|
|
7
|
+
/** Replay a board's events into current thread state. Deterministic: replay order is
|
|
8
|
+
* (ts, id) so two stores holding the same event SET always derive the same state.
|
|
9
|
+
* TWO passes - creates first, then everything else - so a reply whose author's clock
|
|
10
|
+
* ran ahead of the creator's still lands instead of being dropped forever. */
|
|
11
|
+
function replay(events) {
|
|
12
|
+
const ordered = [...events].sort((a, b) => a.ts - b.ts || (a.id < b.id ? -1 : 1));
|
|
13
|
+
const threads = /* @__PURE__ */ new Map();
|
|
14
|
+
for (const ev of ordered) {
|
|
15
|
+
if (ev.type !== "create") continue;
|
|
16
|
+
if (!ev.commentId || threads.has(ev.commentId)) continue;
|
|
17
|
+
threads.set(ev.commentId, {
|
|
18
|
+
id: ev.commentId,
|
|
19
|
+
board: ev.board,
|
|
20
|
+
nodeKey: ev.nodeKey,
|
|
21
|
+
frame: ev.frame,
|
|
22
|
+
anchor: ev.anchor,
|
|
23
|
+
author: ev.author,
|
|
24
|
+
body: ev.body,
|
|
25
|
+
ts: ev.ts,
|
|
26
|
+
resolved: false,
|
|
27
|
+
agent: ev.agent,
|
|
28
|
+
agentMeta: ev.agentMeta,
|
|
29
|
+
replies: [],
|
|
30
|
+
reactions: {}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
for (const ev of ordered) switch (ev.type) {
|
|
34
|
+
case "reply": {
|
|
35
|
+
const t = ev.parentId ? threads.get(ev.parentId) : void 0;
|
|
36
|
+
if (!t || !ev.commentId || t.replies.some((r) => r.id === ev.commentId)) break;
|
|
37
|
+
t.replies.push({
|
|
38
|
+
id: ev.commentId,
|
|
39
|
+
author: ev.author,
|
|
40
|
+
body: ev.body,
|
|
41
|
+
ts: ev.ts,
|
|
42
|
+
agent: ev.agent,
|
|
43
|
+
agentMeta: ev.agentMeta
|
|
44
|
+
});
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case "edit": {
|
|
48
|
+
if (!ev.commentId || ev.body === void 0) break;
|
|
49
|
+
const t = threads.get(ev.commentId);
|
|
50
|
+
if (t) {
|
|
51
|
+
t.body = ev.body;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
for (const th of threads.values()) {
|
|
55
|
+
const r = th.replies.find((r) => r.id === ev.commentId);
|
|
56
|
+
if (r) {
|
|
57
|
+
r.body = ev.body;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "resolve": {
|
|
64
|
+
const t = ev.commentId ? threads.get(ev.commentId) : void 0;
|
|
65
|
+
if (t) {
|
|
66
|
+
t.resolved = true;
|
|
67
|
+
if (ev.addressedIn) t.addressedIn = ev.addressedIn;
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "reopen": {
|
|
72
|
+
const t = ev.commentId ? threads.get(ev.commentId) : void 0;
|
|
73
|
+
if (t) {
|
|
74
|
+
t.resolved = false;
|
|
75
|
+
t.addressedIn = void 0;
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "reanchor": {
|
|
80
|
+
const t = ev.commentId ? threads.get(ev.commentId) : void 0;
|
|
81
|
+
if (t && ev.anchor != null) t.anchor = ev.anchor;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "react": {
|
|
85
|
+
const t = ev.commentId ? threads.get(ev.commentId) : void 0;
|
|
86
|
+
const who = ev.author?.id ?? ev.author?.email;
|
|
87
|
+
if (!t || !who || !ev.emoji) break;
|
|
88
|
+
const users = t.reactions[ev.emoji] ??= [];
|
|
89
|
+
const at = users.indexOf(who);
|
|
90
|
+
if (at === -1) users.push(who);
|
|
91
|
+
else {
|
|
92
|
+
users.splice(at, 1);
|
|
93
|
+
if (!users.length) delete t.reactions[ev.emoji];
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return [...threads.values()];
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { replay as n, diffEvents as t };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as
|
|
3
|
-
import { a as writeManifest, r as scanFrames } from "./manifest-C2tzkNaC.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";
|
|
4
3
|
import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, managedFile, pkgDir } from "./managed-HwHVNI3h.mjs";
|
|
5
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
6
5
|
import { dirname, join, relative } from "node:path";
|
|
@@ -144,7 +143,7 @@ function init(root, opts) {
|
|
|
144
143
|
refresh("providers.tsx", [providersTemplate(null, null), providersTemplate(null, host.toaster, host.routerPkg)], providersTemplate(host.router, host.toaster, host.routerPkg));
|
|
145
144
|
refresh("tsconfig.json", [STANDALONE_TSCONFIG], tsconfigNow());
|
|
146
145
|
}
|
|
147
|
-
write(".gitignore", ".local/\n.dist/\n");
|
|
146
|
+
write(".gitignore", ".local/\n.dist/\n.dist-seeds/\n");
|
|
148
147
|
write(".gitattributes", "comments/*.jsonl merge=union\n");
|
|
149
148
|
write("scenes/_layout.tsx", readFileSync(join(templates, "root-layout.tsx"), "utf8"));
|
|
150
149
|
if (!existsSync(join(design, "boards"))) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { delimiter, isAbsolute, join, relative, sep } from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
4
6
|
//#region src/server/detect.ts
|
|
5
7
|
function readJson(file) {
|
|
6
8
|
try {
|
|
@@ -262,7 +264,9 @@ async function loadConfig(root) {
|
|
|
262
264
|
share: {
|
|
263
265
|
branding: user.share?.branding !== false,
|
|
264
266
|
name: typeof user.share?.name === "string" ? user.share.name : void 0,
|
|
265
|
-
logo: typeof user.share?.logo === "string" ? user.share.logo : void 0
|
|
267
|
+
logo: typeof user.share?.logo === "string" ? user.share.logo : void 0,
|
|
268
|
+
frontDoor: user.share?.frontDoor !== false && user.share?.front_door !== false,
|
|
269
|
+
notify: user.share?.notify !== false
|
|
266
270
|
},
|
|
267
271
|
...jamFields(user.jam)
|
|
268
272
|
};
|
|
@@ -366,4 +370,232 @@ function validViewports(v) {
|
|
|
366
370
|
return Object.keys(out).length ? out : null;
|
|
367
371
|
}
|
|
368
372
|
//#endregion
|
|
369
|
-
|
|
373
|
+
//#region src/server/manifest.ts
|
|
374
|
+
const FRAME_EXT = /\.(tsx|jsx|html)$/;
|
|
375
|
+
const RESERVED_SCENES = /* @__PURE__ */ new Set(["components", "screens"]);
|
|
376
|
+
/** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted. */
|
|
377
|
+
function extractMeta(src) {
|
|
378
|
+
const m = /export\s+const\s+meta\s*=\s*\{([\s\S]*?)\}/.exec(src);
|
|
379
|
+
if (!m) return {};
|
|
380
|
+
const body = m[1];
|
|
381
|
+
const pick = (key) => {
|
|
382
|
+
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(['"\`])([^'"\`]*)\\1`).exec(body);
|
|
383
|
+
return r ? r[2] : void 0;
|
|
384
|
+
};
|
|
385
|
+
const out = {};
|
|
386
|
+
const title = pick("title");
|
|
387
|
+
if (title) out.title = title;
|
|
388
|
+
const viewport = pick("viewport");
|
|
389
|
+
if (viewport) out.viewport = viewport;
|
|
390
|
+
const theme = pick("theme");
|
|
391
|
+
if (theme) out.theme = theme;
|
|
392
|
+
const of = pick("of");
|
|
393
|
+
if (of) out.of = of;
|
|
394
|
+
const variant = pick("variant");
|
|
395
|
+
if (variant) out.variant = variant;
|
|
396
|
+
const intent = pick("intent");
|
|
397
|
+
if (intent) out.intent = intent;
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
/** Content-frame detection: LEXICAL by convention - the frame file
|
|
401
|
+
* itself imports PKG/content (an import specifier scan, so "<Diagram" inside a
|
|
402
|
+
* string in a UI frame can never misbadge it; barrels/re-exports are not
|
|
403
|
+
* detected - meta.intent is the taught path and always works). Returns the
|
|
404
|
+
* inferred intent + natural width, or null for UI frames. */
|
|
405
|
+
const CONTENT_IMPORT = new RegExp(`from\\s+['"]${PKG}/content['"]`);
|
|
406
|
+
const WIDE_DOC = /<Doc\b[^>]*\blayout\s*=\s*["']wide["']/;
|
|
407
|
+
const contentWidthOf = (src) => WIDE_DOC.test(src) ? CONTENT_WIDTH.wide : CONTENT_WIDTH.document;
|
|
408
|
+
function contentScan(src) {
|
|
409
|
+
if (!CONTENT_IMPORT.test(src)) return null;
|
|
410
|
+
const count = (re) => (src.match(re) ?? []).length;
|
|
411
|
+
const diagrams = count(/<Diagram[\s>/]/g);
|
|
412
|
+
const imgs = count(/<Img[\s>/]/g);
|
|
413
|
+
const mds = count(/<Md[\s>/]/g);
|
|
414
|
+
return {
|
|
415
|
+
intent: diagrams > 0 ? "diagram" : imgs > mds ? "moodboard" : "spec",
|
|
416
|
+
width: contentWidthOf(src)
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/** id = path relative to design/, extension dropped, `scenes/` prefix dropped. Always `/`-separated. */
|
|
420
|
+
function toFrameId(designRelPath) {
|
|
421
|
+
const noExt = designRelPath.split(sep).join("/").replace(FRAME_EXT, "");
|
|
422
|
+
return noExt.startsWith("scenes/") ? noExt.slice(7) : noExt;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* A7: which frames does a changed `design/**` file affect? Manifest + directory conventions,
|
|
426
|
+
* NOT a module-graph walk (deterministic, cheap, may over-reload but never misses a
|
|
427
|
+
* conventionally-affected frame). Returns:
|
|
428
|
+
* null -> uncontrolled: leave to default Vite HMR (src/** deps, theme.css, config, assets)
|
|
429
|
+
* [] -> controlled but affects no current frame (e.g. a layout in an empty dir)
|
|
430
|
+
* [ids] -> controlled: the shell drives a rev-stamped reload of these frames
|
|
431
|
+
*/
|
|
432
|
+
function affectedFrameIds(absFile, root, manifest) {
|
|
433
|
+
const design = join(root, "design");
|
|
434
|
+
if (absFile !== design && !absFile.startsWith(design + sep)) return null;
|
|
435
|
+
const rel = relative(design, absFile).split(sep).join("/");
|
|
436
|
+
const name = rel.split("/").pop() ?? rel;
|
|
437
|
+
const designRel = `design/${rel}`;
|
|
438
|
+
const tsxFrames = manifest.frames.filter((f) => f.kind === "tsx");
|
|
439
|
+
if (name === "theme.css") return null;
|
|
440
|
+
if (rel === "providers.tsx" || rel === "providers.jsx") return tsxFrames.map((f) => f.id);
|
|
441
|
+
if (/(^|\/)(_layout\.(tsx|jsx)|_fixtures\.(ts|tsx|js|jsx|json))$/.test(rel)) {
|
|
442
|
+
const prefix = `design/${rel.slice(0, rel.length - name.length)}`;
|
|
443
|
+
return tsxFrames.filter((f) => f.file.startsWith(prefix)).map((f) => f.id);
|
|
444
|
+
}
|
|
445
|
+
const direct = manifest.frames.find((f) => f.file === designRel);
|
|
446
|
+
if (direct) return [direct.id];
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
function walk(dir, out = []) {
|
|
450
|
+
if (!existsSync(dir)) return out;
|
|
451
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
452
|
+
if (e.name.startsWith(".")) continue;
|
|
453
|
+
const p = join(dir, e.name);
|
|
454
|
+
if (e.isDirectory()) walk(p, out);
|
|
455
|
+
else out.push(p);
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
function scanFrames(root) {
|
|
460
|
+
const design = join(root, "design");
|
|
461
|
+
const frames = [];
|
|
462
|
+
for (const base of ["scenes", "components"]) for (const abs of walk(join(design, base))) {
|
|
463
|
+
const name = abs.split(sep).pop();
|
|
464
|
+
if (name.startsWith("_") || !FRAME_EXT.test(name)) continue;
|
|
465
|
+
const rel = relative(design, abs).split(sep).join("/");
|
|
466
|
+
const id = toFrameId(rel);
|
|
467
|
+
const scene = id.includes("/") ? id.split("/")[0] : "";
|
|
468
|
+
if (base === "scenes" && RESERVED_SCENES.has(scene)) {
|
|
469
|
+
console.error(`[marver] scene "${scene}" is a reserved name - "${rel}" skipped. Rename the scene.`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const kind = name.endsWith(".html") ? "html" : "tsx";
|
|
473
|
+
const entry = {
|
|
474
|
+
id,
|
|
475
|
+
file: `design/${rel}`,
|
|
476
|
+
kind,
|
|
477
|
+
scene
|
|
478
|
+
};
|
|
479
|
+
if (kind === "tsx") {
|
|
480
|
+
const src = readFileSync(abs, "utf8");
|
|
481
|
+
const meta = extractMeta(src);
|
|
482
|
+
if (meta.title) entry.title = meta.title;
|
|
483
|
+
if (meta.viewport) entry.viewport = meta.viewport;
|
|
484
|
+
if (meta.theme) entry.theme = meta.theme;
|
|
485
|
+
if (meta.of) entry.variantGroup = meta.of;
|
|
486
|
+
if (meta.variant) entry.variant = meta.variant;
|
|
487
|
+
const content = contentScan(src);
|
|
488
|
+
if (content || meta.intent) {
|
|
489
|
+
entry.intent = meta.intent ?? content.intent;
|
|
490
|
+
entry.contentWidth = content?.width ?? contentWidthOf(src);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
frames.push(entry);
|
|
494
|
+
}
|
|
495
|
+
const extRank = (file) => file.endsWith(".tsx") ? 0 : file.endsWith(".jsx") ? 1 : 2;
|
|
496
|
+
frames.sort((a, b) => a.id.localeCompare(b.id) || extRank(a.file) - extRank(b.file) || a.file.localeCompare(b.file));
|
|
497
|
+
const seen = /* @__PURE__ */ new Map();
|
|
498
|
+
const deduped = frames.filter((f) => {
|
|
499
|
+
const prev = seen.get(f.id);
|
|
500
|
+
if (prev) {
|
|
501
|
+
console.error(`[marver] duplicate frame id "${f.id}" (${prev} vs ${f.file}) - keeping ${prev}.`);
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
seen.set(f.id, f.file);
|
|
505
|
+
return true;
|
|
506
|
+
});
|
|
507
|
+
frames.length = 0;
|
|
508
|
+
frames.push(...deduped);
|
|
509
|
+
inferVariantGroups(frames);
|
|
510
|
+
const sceneCounts = /* @__PURE__ */ new Map();
|
|
511
|
+
for (const f of frames) sceneCounts.set(f.scene, (sceneCounts.get(f.scene) ?? 0) + 1);
|
|
512
|
+
return {
|
|
513
|
+
frames,
|
|
514
|
+
scenes: [...sceneCounts.entries()].map(([name, n]) => ({
|
|
515
|
+
name,
|
|
516
|
+
frames: n
|
|
517
|
+
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/** Variant groups. A group = 2+ frames in one DIRECTORY whose basenames
|
|
521
|
+
* are letter-prefixed (`a-terminal`), or frames declaring `meta.of`. Group id = the
|
|
522
|
+
* directory's id prefix (or meta.of); variant key = the letter (or meta.variant).
|
|
523
|
+
* Nested directories scope alternatives inside a busy scene (checkout/payment/a-card).
|
|
524
|
+
* States (empty.tsx, error.tsx) never letter-prefix, so they never misgroup.
|
|
525
|
+
* Mutates entries in place: only frames whose group materializes keep the fields. */
|
|
526
|
+
function inferVariantGroups(frames) {
|
|
527
|
+
const dirOf = (id) => {
|
|
528
|
+
const i = id.lastIndexOf("/");
|
|
529
|
+
return i >= 0 ? id.slice(0, i) : "";
|
|
530
|
+
};
|
|
531
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
532
|
+
for (const f of frames) {
|
|
533
|
+
if (f.kind !== "tsx") {
|
|
534
|
+
delete f.variantGroup;
|
|
535
|
+
delete f.variant;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
let group = f.variantGroup;
|
|
539
|
+
let key = f.variant;
|
|
540
|
+
if (!group) {
|
|
541
|
+
const base = f.id.slice(f.id.lastIndexOf("/") + 1);
|
|
542
|
+
const m = /^([a-z])-.+$/.exec(base);
|
|
543
|
+
if (!m) continue;
|
|
544
|
+
group = dirOf(f.id);
|
|
545
|
+
if (!group) continue;
|
|
546
|
+
key = key ?? m[1];
|
|
547
|
+
} else if (!key) {
|
|
548
|
+
const m = /^([a-z])-.+$/.exec(f.id.slice(f.id.lastIndexOf("/") + 1));
|
|
549
|
+
if (!m) {
|
|
550
|
+
console.warn(`[marver] ${f.file} declares of:"${group}" but no variant key (add meta.variant or a letter prefix) - not grouped.`);
|
|
551
|
+
delete f.variantGroup;
|
|
552
|
+
delete f.variant;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
key = m[1];
|
|
556
|
+
}
|
|
557
|
+
f.variantGroup = group;
|
|
558
|
+
f.variant = key;
|
|
559
|
+
candidates.set(group, [...candidates.get(group) ?? [], f]);
|
|
560
|
+
}
|
|
561
|
+
for (const [group, members] of candidates) {
|
|
562
|
+
if (new Set(members.map((m) => dirOf(m.id))).size > 1) {
|
|
563
|
+
console.warn(`[marver] group "${group}" spans directories - variants must be siblings; not grouped.`);
|
|
564
|
+
for (const m of members) {
|
|
565
|
+
delete m.variantGroup;
|
|
566
|
+
delete m.variant;
|
|
567
|
+
}
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const seen = /* @__PURE__ */ new Set();
|
|
571
|
+
const kept = [];
|
|
572
|
+
for (const m of members.sort((a, b) => (a.variant ?? "").localeCompare(b.variant ?? ""))) {
|
|
573
|
+
if (seen.has(m.variant)) {
|
|
574
|
+
console.warn(`[marver] duplicate variant "${m.variant}" in group "${group}" (${m.file}) - not grouped.`);
|
|
575
|
+
delete m.variantGroup;
|
|
576
|
+
delete m.variant;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
seen.add(m.variant);
|
|
580
|
+
kept.push(m);
|
|
581
|
+
}
|
|
582
|
+
if (kept.length < 2) for (const m of kept) {
|
|
583
|
+
delete m.variantGroup;
|
|
584
|
+
delete m.variant;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
for (const f of frames) if (f.variant && !f.variantGroup) delete f.variant;
|
|
588
|
+
}
|
|
589
|
+
/** Write design/manifest.json only when content changed. Returns the manifest either way. */
|
|
590
|
+
function writeManifest(root, manifest) {
|
|
591
|
+
const file = join(root, "design", "manifest.json");
|
|
592
|
+
const next = JSON.stringify(manifest, null, 2) + "\n";
|
|
593
|
+
const prev = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
594
|
+
if (hash(prev) === hash(next)) return false;
|
|
595
|
+
mkdirSync(join(root, "design"), { recursive: true });
|
|
596
|
+
writeFileSync(file, next);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
600
|
+
//#endregion
|
|
601
|
+
export { writeManifest as a, detectAgent as c, toFrameId as i, detectHost as l, hash as n, DEFAULTS as o, scanFrames as r, loadConfig as s, affectedFrameIds as t, readJson as u };
|
|
@@ -365,6 +365,111 @@ async function verifySignature(parts, kid, issuer) {
|
|
|
365
365
|
}, signature);
|
|
366
366
|
}
|
|
367
367
|
/**
|
|
368
|
+
* Verify a short-lived bearer JWT the identity service minted for a NON-gate
|
|
369
|
+
* purpose (the front door's summary token, 04-solution §9.1). Same trust root
|
|
370
|
+
* and JWKS as the assertion path, but a distinct `typ` - so neither token can
|
|
371
|
+
* ever be replayed at the other door - and no nonce: the token is not a
|
|
372
|
+
* sign-in, it is a capability that expires in seconds.
|
|
373
|
+
*/
|
|
374
|
+
async function verifyBearerJwt(opts) {
|
|
375
|
+
const { token, origin, issuer, typ } = opts;
|
|
376
|
+
if (!token || token.length > MAX_TOKEN_BYTES) return {
|
|
377
|
+
ok: false,
|
|
378
|
+
reason: "token size"
|
|
379
|
+
};
|
|
380
|
+
const parts = token.split(".");
|
|
381
|
+
if (parts.length !== 3) return {
|
|
382
|
+
ok: false,
|
|
383
|
+
reason: "not a jws"
|
|
384
|
+
};
|
|
385
|
+
let header;
|
|
386
|
+
let claims;
|
|
387
|
+
try {
|
|
388
|
+
const h = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
|
|
389
|
+
const c = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
390
|
+
if (!isPlainObject(h) || !isPlainObject(c)) return {
|
|
391
|
+
ok: false,
|
|
392
|
+
reason: "malformed json"
|
|
393
|
+
};
|
|
394
|
+
header = h;
|
|
395
|
+
claims = c;
|
|
396
|
+
} catch {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
reason: "malformed json"
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (header.alg !== "ES256") return {
|
|
403
|
+
ok: false,
|
|
404
|
+
reason: "alg"
|
|
405
|
+
};
|
|
406
|
+
if (header.typ !== typ) return {
|
|
407
|
+
ok: false,
|
|
408
|
+
reason: "typ"
|
|
409
|
+
};
|
|
410
|
+
if ("jku" in header || "x5u" in header || "jwk" in header) return {
|
|
411
|
+
ok: false,
|
|
412
|
+
reason: "header injection"
|
|
413
|
+
};
|
|
414
|
+
const kid = typeof header.kid === "string" ? header.kid : null;
|
|
415
|
+
if (!kid || kid.length > 128) return {
|
|
416
|
+
ok: false,
|
|
417
|
+
reason: "kid"
|
|
418
|
+
};
|
|
419
|
+
if (claims.iss !== issuer) return {
|
|
420
|
+
ok: false,
|
|
421
|
+
reason: "iss"
|
|
422
|
+
};
|
|
423
|
+
if (claims.aud !== origin) return {
|
|
424
|
+
ok: false,
|
|
425
|
+
reason: "aud"
|
|
426
|
+
};
|
|
427
|
+
if (opts.azp && claims.azp !== opts.azp) return {
|
|
428
|
+
ok: false,
|
|
429
|
+
reason: "azp"
|
|
430
|
+
};
|
|
431
|
+
const sub = typeof claims.sub === "string" ? claims.sub : "";
|
|
432
|
+
const email = typeof claims.email === "string" ? claims.email : "";
|
|
433
|
+
if (!sub || !email) return {
|
|
434
|
+
ok: false,
|
|
435
|
+
reason: "missing claims"
|
|
436
|
+
};
|
|
437
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
438
|
+
const exp = typeof claims.exp === "number" ? claims.exp : 0;
|
|
439
|
+
const iat = typeof claims.iat === "number" ? claims.iat : 0;
|
|
440
|
+
if (!exp || exp + CLOCK_TOLERANCE_S < now) return {
|
|
441
|
+
ok: false,
|
|
442
|
+
reason: "expired"
|
|
443
|
+
};
|
|
444
|
+
if (!iat || iat - CLOCK_TOLERANCE_S > now) return {
|
|
445
|
+
ok: false,
|
|
446
|
+
reason: "issued in the future"
|
|
447
|
+
};
|
|
448
|
+
if (opts.maxAgeS && exp - iat > opts.maxAgeS) return {
|
|
449
|
+
ok: false,
|
|
450
|
+
reason: "lifetime"
|
|
451
|
+
};
|
|
452
|
+
let verified = false;
|
|
453
|
+
try {
|
|
454
|
+
verified = await verifySignature(parts, kid, issuer);
|
|
455
|
+
} catch {
|
|
456
|
+
return {
|
|
457
|
+
ok: false,
|
|
458
|
+
reason: "key fetch failed"
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (!verified) return {
|
|
462
|
+
ok: false,
|
|
463
|
+
reason: "signature"
|
|
464
|
+
};
|
|
465
|
+
return {
|
|
466
|
+
ok: true,
|
|
467
|
+
sub,
|
|
468
|
+
email,
|
|
469
|
+
claims
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
368
473
|
* The optional display claims, taken only if they are the right shape.
|
|
369
474
|
*
|
|
370
475
|
* Deliberately forgiving: a malformed name or picture drops that ONE field and
|
|
@@ -408,4 +513,4 @@ function browserBinding(cookieValue) {
|
|
|
408
513
|
return createHash("sha256").update(cookieValue).digest("base64url");
|
|
409
514
|
}
|
|
410
515
|
//#endregion
|
|
411
|
-
export { TransactionStore, browserBinding, normalizeIssuer, verifyAssertion };
|
|
516
|
+
export { TransactionStore, browserBinding, normalizeIssuer, verifyAssertion, verifyBearerJwt };
|