@1agh/maude 0.40.0 → 0.42.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/apps/studio/ai-banner.tsx +2 -2
- package/apps/studio/annotations-layer.tsx +53 -2
- package/apps/studio/api.ts +504 -44
- package/apps/studio/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
- package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
- package/apps/studio/bin/_fetch-asset.mjs +556 -0
- package/apps/studio/bin/_html-playwright.mjs +26 -4
- package/apps/studio/bin/_pdf-playwright.mjs +13 -2
- package/apps/studio/bin/_png-playwright.mjs +15 -2
- package/apps/studio/bin/_pptx-playwright.mjs +17 -4
- package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
- package/apps/studio/bin/_svg-playwright.mjs +26 -4
- package/apps/studio/bin/annotate.mjs +576 -34
- package/apps/studio/bin/canvas-rects.sh +152 -0
- package/apps/studio/bin/fetch-asset.sh +34 -0
- package/apps/studio/bin/read-annotations.mjs +138 -7
- package/apps/studio/bin/screenshot.sh +53 -4
- package/apps/studio/bin/smoke.sh +42 -6
- package/apps/studio/build.ts +21 -0
- package/apps/studio/canvas-comment-mount.tsx +138 -4
- package/apps/studio/canvas-edit.ts +744 -11
- package/apps/studio/canvas-lib.tsx +219 -2
- package/apps/studio/canvas-shell.tsx +487 -20
- package/apps/studio/client/app.jsx +1476 -76
- package/apps/studio/client/comments-overlay.css +130 -126
- package/apps/studio/client/export-center.jsx +426 -0
- package/apps/studio/client/github.js +8 -0
- package/apps/studio/client/styles/3-shell-maude.css +65 -0
- package/apps/studio/client/styles/4-components.css +150 -0
- package/apps/studio/comments-overlay.tsx +148 -41
- package/apps/studio/config.schema.json +2 -2
- package/apps/studio/context-menu.tsx +15 -5
- package/apps/studio/contextual-toolbar.tsx +262 -4
- package/apps/studio/cursors-overlay.tsx +4 -4
- package/apps/studio/dist/client.bundle.js +20 -20
- package/apps/studio/dist/comment-mount.js +59 -1
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +127 -1
- package/apps/studio/drag-state.ts +24 -0
- package/apps/studio/equal-spacing-detector.ts +205 -0
- package/apps/studio/export-dialog.tsx +19 -26
- package/apps/studio/exporters/_runtime.ts +104 -0
- package/apps/studio/exporters/html.ts +12 -20
- package/apps/studio/exporters/index.ts +14 -2
- package/apps/studio/exporters/jobs.ts +334 -0
- package/apps/studio/exporters/pdf.ts +16 -20
- package/apps/studio/exporters/png.ts +12 -20
- package/apps/studio/exporters/pptx.ts +22 -23
- package/apps/studio/exporters/scope.ts +1 -0
- package/apps/studio/exporters/svg.ts +14 -22
- package/apps/studio/exporters/video.ts +15 -17
- package/apps/studio/git/service.ts +3 -1
- package/apps/studio/history.ts +47 -1
- package/apps/studio/http.ts +368 -50
- package/apps/studio/input-router.tsx +12 -0
- package/apps/studio/marquee-overlay.tsx +1 -1
- package/apps/studio/measure-overlay.tsx +241 -0
- package/apps/studio/participants-chrome.tsx +3 -3
- package/apps/studio/server.ts +3 -1
- package/apps/studio/sizing-mode.ts +117 -0
- package/apps/studio/spacing-handles.ts +166 -0
- package/apps/studio/test/annotate-write.test.ts +890 -0
- package/apps/studio/test/camera-reveal.test.tsx +173 -0
- package/apps/studio/test/canvas-edit.test.ts +50 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +24 -0
- package/apps/studio/test/canvas-rects.test.ts +198 -0
- package/apps/studio/test/comments-overlay.test.ts +117 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
- package/apps/studio/test/dom-selection.test.ts +130 -0
- package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
- package/apps/studio/test/edit-scope-api.test.ts +115 -0
- package/apps/studio/test/element-resize.test.ts +136 -0
- package/apps/studio/test/element-structural-api.test.ts +360 -0
- package/apps/studio/test/element-structural-edit.test.ts +233 -0
- package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
- package/apps/studio/test/export-center.test.tsx +287 -0
- package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
- package/apps/studio/test/exporters/history.test.ts +32 -3
- package/apps/studio/test/exporters/jobs.test.ts +263 -0
- package/apps/studio/test/history-rollback.test.ts +26 -0
- package/apps/studio/test/knob-props-authored.test.ts +87 -0
- package/apps/studio/test/read-annotations.test.ts +154 -0
- package/apps/studio/test/sizing-mode.test.ts +102 -0
- package/apps/studio/test/spacing-handles.test.ts +138 -0
- package/apps/studio/test/specimen-select.test.ts +88 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
- package/apps/studio/tool-palette.tsx +122 -2
- package/apps/studio/undo-hud.tsx +2 -2
- package/apps/studio/use-element-resize.tsx +732 -0
- package/apps/studio/use-keyboard-discipline.tsx +205 -15
- package/apps/studio/use-selection-set.tsx +14 -0
- package/apps/studio/use-spacing-handles.tsx +388 -0
- package/apps/studio/whats-new.json +45 -0
- package/apps/studio/ws.ts +6 -0
- package/cli/commands/design.mjs +6 -1
- package/cli/commands/design.test.mjs +49 -1
- package/cli/lib/fetch-asset.test.mjs +213 -0
- package/cli/lib/gitignore-block.mjs +1 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +10 -2
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./whats-new.schema.json",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "background-export-notification-center",
|
|
6
|
+
"version": "0.42.0",
|
|
7
|
+
"date": "2026-07-09",
|
|
8
|
+
"kind": "feature",
|
|
9
|
+
"title": "Exports now run in the background",
|
|
10
|
+
"summary": "Hitting Export no longer locks up the dialog until the render finishes — it closes right away and a new Exports button in the menubar tracks progress, with a toast when it's ready and a Download/Save button. Multiple exports can run at once, so a quick PNG doesn't have to wait behind a slow PDF or video. Also fixes a bug where exporting every artboard on a canvas separately could scramble some of the pages.",
|
|
11
|
+
"surface": "design-ui",
|
|
12
|
+
"tour": [
|
|
13
|
+
{
|
|
14
|
+
"target": "[data-tour=\"exports\"]",
|
|
15
|
+
"title": "Exports run in the background",
|
|
16
|
+
"body": "Kick off an export and keep working — this button shows progress and lights up when it's ready to download or save.",
|
|
17
|
+
"placement": "bottom"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "desktop-intel-mac-support",
|
|
23
|
+
"version": "0.41.0",
|
|
24
|
+
"date": "2026-07-08",
|
|
25
|
+
"kind": "improvement",
|
|
26
|
+
"title": "Desktop app now runs on Intel Macs",
|
|
27
|
+
"summary": "The macOS desktop app was Apple-Silicon-only — it now ships as a universal binary, so Intel Mac users can download and run it natively too, no Rosetta workaround needed.",
|
|
28
|
+
"learnMore": "https://maude.sh/desktop",
|
|
29
|
+
"surface": "design-ui"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "element-editing-robustness",
|
|
33
|
+
"version": "0.41.0",
|
|
34
|
+
"date": "2026-07-08",
|
|
35
|
+
"kind": "feature",
|
|
36
|
+
"title": "Edit elements like Figma or Webflow",
|
|
37
|
+
"summary": "Selecting an element now opens the Inspector automatically, and drag handles let you resize it directly on the canvas (corner + edge, Shift to lock aspect, Alt to resize from center) — artboards get free-hand resize too. Delete an element with the Del key or the right-click menu, or insert a new div, text, or image straight into the artboard; add a whole new empty artboard from a Desktop/Laptop/Tablet/Mobile preset. The Inspector gained Position (with a constraints-style inset box), Transform, and Media sections (object-fit, aspect ratio), plus on-canvas dragging for padding and gaps. Swap a photo or video (or a background image) from a built-in asset picker. Editing now works on design-system specimens too, not just your UI canvases. A shared component always tells you plainly whether an edit is local to this artboard or shared across every place it's used, and moving one instance never drags the others along. Rounding it out: keyboard nudge + tree navigation, Cmd+D duplicate, copy/paste style, align and distribute a multi-selection, deep-select + \"Select layer\" for nested/overlapping elements, Alt-hover distance measurement, a free-hand rotate handle, and a proper Fixed/Hug/Fill sizing control for flex layouts. Every one of these is undoable with Cmd+Z.",
|
|
38
|
+
"surface": "design-ui"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "whiteboard-ai-toolkit",
|
|
42
|
+
"version": "0.41.0",
|
|
43
|
+
"date": "2026-07-08",
|
|
44
|
+
"kind": "feature",
|
|
45
|
+
"title": "The Assistant now reads and draws on your whiteboard",
|
|
46
|
+
"summary": "Sketch stickies, shapes, and arrows on a canvas and the Assistant understands not just which artboard a note sits on, but which button or element it's pointing at. Ask it to pin a note next to something specific, and it'll drop it right there with an arrow pointing at it — no guessing coordinates. It can also lay out a whole tidy board for you: a sprint retro, a kanban, a week of social posts, a roadmap, a brainstorm, a user-flow diagram — just ask.",
|
|
47
|
+
"surface": "design-ui"
|
|
48
|
+
},
|
|
4
49
|
{
|
|
5
50
|
"id": "timeline-clip-editing",
|
|
6
51
|
"version": "0.40.0",
|
package/apps/studio/ws.ts
CHANGED
|
@@ -216,6 +216,12 @@ export function createWs(
|
|
|
216
216
|
// Assistant panel. Inspector clients only — same-origin shell, like the rest.
|
|
217
217
|
ctx.bus.on('acp-focus', () => broadcast({ type: 'acp-focus' }));
|
|
218
218
|
|
|
219
|
+
// feature-background-export-notification-center — export job queue state
|
|
220
|
+
// changes (queued → running → progress ticks → done/failed). Full-snapshot
|
|
221
|
+
// payload per change, mirroring git-status/sync:status above. Inspector
|
|
222
|
+
// clients only — same privileged-data class as the rest of this feed.
|
|
223
|
+
ctx.bus.on('export:job', (job: unknown) => broadcast({ type: 'export:job', payload: job }));
|
|
224
|
+
|
|
219
225
|
// HMR broadcaster — turns fs:any change events into `canvas-hmr` messages.
|
|
220
226
|
// The iframe-side client (in _shell.html) decides reload strategy from `mode`.
|
|
221
227
|
// Uses broadcastHmr so the segregated canvas origin's HMR-only sockets get it.
|
package/cli/commands/design.mjs
CHANGED
|
@@ -43,8 +43,10 @@ const BIN_VERBS = new Set([
|
|
|
43
43
|
'to-lottie',
|
|
44
44
|
'read-annotations',
|
|
45
45
|
'annotate',
|
|
46
|
+
'canvas-rects',
|
|
46
47
|
'chat-open',
|
|
47
48
|
'ensure-browser',
|
|
49
|
+
'fetch-asset',
|
|
48
50
|
]);
|
|
49
51
|
|
|
50
52
|
// Bin verbs that boot the dev-server (directly, or by shelling into server-up.sh).
|
|
@@ -136,8 +138,9 @@ Lifecycle:
|
|
|
136
138
|
|
|
137
139
|
Dev-tooling (dispatch to the dev-server bash helpers — DDR-062):
|
|
138
140
|
screenshot · server-up · prep · slug · bootstrap-check · runtime-health
|
|
139
|
-
smoke · canvas-edit · handoff · asset-sweep · visual-sanity
|
|
141
|
+
smoke · canvas-edit · handoff · asset-sweep · visual-sanity · fetch-asset
|
|
140
142
|
draw-build · draw-proof · svg-optimize · to-lottie · read-annotations · annotate
|
|
143
|
+
canvas-rects
|
|
141
144
|
Invoke the bundled helper of the same name. maude resolves it from its
|
|
142
145
|
own package root and sets CLAUDE_PLUGIN_ROOT for the child; stdout,
|
|
143
146
|
stderr, and exit code pass straight through (so command-substitution
|
|
@@ -147,6 +150,8 @@ Dev-tooling (dispatch to the dev-server bash helpers — DDR-062):
|
|
|
147
150
|
PORT=$(maude design server-up --root "$REPO")
|
|
148
151
|
eval "$(maude design prep --shell-export --shape edit --root "$REPO")"
|
|
149
152
|
maude design smoke --changed-only
|
|
153
|
+
REF=$(maude design fetch-asset "$URL" --root "$REPO") # download-first image
|
|
154
|
+
maude design canvas-rects "ui/Foo.tsx" # geometry manifest (whiteboard toolkit)
|
|
150
155
|
|
|
151
156
|
serve [--port N] [--root PATH]
|
|
152
157
|
Start the design plugin's dev server in the current repo. Equivalent
|
|
@@ -51,11 +51,59 @@ test('unknown verb exits 2', () => {
|
|
|
51
51
|
test('design help lists the dev-tooling verbs', () => {
|
|
52
52
|
const res = runDesign(['help']);
|
|
53
53
|
assert.equal(res.status, 0, res.stderr);
|
|
54
|
-
for (const verb of [
|
|
54
|
+
for (const verb of [
|
|
55
|
+
'screenshot',
|
|
56
|
+
'server-up',
|
|
57
|
+
'prep',
|
|
58
|
+
'slug',
|
|
59
|
+
'smoke',
|
|
60
|
+
'visual-sanity',
|
|
61
|
+
'canvas-rects',
|
|
62
|
+
]) {
|
|
55
63
|
assert.match(res.stdout, new RegExp(verb), `usage should mention ${verb}`);
|
|
56
64
|
}
|
|
57
65
|
});
|
|
58
66
|
|
|
67
|
+
// canvas-rects needs an explicit --root pinned to an isolated temp dir (not
|
|
68
|
+
// runDesign's ambient cwd) — its server-detection reads $CLAUDE_PROJECT_DIR /
|
|
69
|
+
// `git rev-parse` when --root is absent, which would escape the temp dir in
|
|
70
|
+
// an environment where either is set.
|
|
71
|
+
function runCanvasRects(args, root) {
|
|
72
|
+
const env = { ...process.env };
|
|
73
|
+
env.CLAUDE_PLUGIN_ROOT = undefined;
|
|
74
|
+
return spawnSync(process.execPath, [BIN, 'design', 'canvas-rects', ...args, '--root', root], {
|
|
75
|
+
cwd: root,
|
|
76
|
+
encoding: 'utf8',
|
|
77
|
+
env,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test('canvas-rects dispatches to the bundled helper (--help)', () => {
|
|
82
|
+
const root = mkdtempSync(join(tmpdir(), 'maude-canvas-rects-'));
|
|
83
|
+
try {
|
|
84
|
+
const res = runCanvasRects(['--help'], root);
|
|
85
|
+
assert.equal(res.status, 0, res.stderr);
|
|
86
|
+
assert.match(res.stdout, /canvas-rects\.sh/);
|
|
87
|
+
} finally {
|
|
88
|
+
rmSync(root, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('canvas-rects with no live server falls back to the static lane', () => {
|
|
93
|
+
// No .design/_server.json under --root → static fallback, no browser
|
|
94
|
+
// needed. A missing canvas still emits a valid empty manifest (exit 0).
|
|
95
|
+
const root = mkdtempSync(join(tmpdir(), 'maude-canvas-rects-'));
|
|
96
|
+
try {
|
|
97
|
+
const res = runCanvasRects(['ui/Nonexistent.tsx'], root);
|
|
98
|
+
assert.equal(res.status, 0, res.stderr);
|
|
99
|
+
assert.match(res.stderr, /static/);
|
|
100
|
+
const manifest = JSON.parse(res.stdout.trim());
|
|
101
|
+
assert.deepEqual(manifest, { artboards: [], elements: [], elementsTruncated: false });
|
|
102
|
+
} finally {
|
|
103
|
+
rmSync(root, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
59
107
|
// resolveServerBinary — the dev-server boot-runtime resolver (DDR-084). server-up
|
|
60
108
|
// must boot the compiled platform binary on a production install (it embeds yjs +
|
|
61
109
|
// every dep; `bun server.ts` from source can't resolve them — the historic
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// DDR-147 § Security follow-up item 1 — the hardened `maude design fetch-asset`
|
|
2
|
+
// helper (apps/studio/bin/_fetch-asset.mjs). This suite locks the SECURITY CORE
|
|
3
|
+
// that the untrusted-fetch rails promise: the resolved-IP SSRF classifier
|
|
4
|
+
// (reject loopback / link-local / RFC-1918 / CGNAT / multicast / reserved, v4 +
|
|
5
|
+
// v6 incl. IPv4-mapped + NAT64), https-only URL parsing, the png/jpg/gif/webp
|
|
6
|
+
// magic sniff (SVG/HTML rejected), the content-addressed name contract, and the
|
|
7
|
+
// realpath containment assertion. Runs under `pnpm test` (node --test cli/**).
|
|
8
|
+
//
|
|
9
|
+
// The network path (curl) is exercised by the manual smoke in the RCA/PR; these
|
|
10
|
+
// are the offline, deterministic guards a regression would trip.
|
|
11
|
+
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, resolve } from 'node:path';
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const REPO = resolve(HERE, '..', '..');
|
|
20
|
+
const MOD = resolve(REPO, 'apps', 'studio', 'bin', '_fetch-asset.mjs');
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
classifyAddress,
|
|
24
|
+
parseIPv4,
|
|
25
|
+
parseIPv6,
|
|
26
|
+
parseHttpsTarget,
|
|
27
|
+
sniffImageExt,
|
|
28
|
+
assetName,
|
|
29
|
+
containedAssetPath,
|
|
30
|
+
FetchAssetError,
|
|
31
|
+
} = await import(MOD);
|
|
32
|
+
|
|
33
|
+
// ── SSRF classifier: BLOCKED addresses ───────────────────────────────────────
|
|
34
|
+
test('classifyAddress blocks private / loopback / link-local / reserved IPv4', () => {
|
|
35
|
+
const blocked = [
|
|
36
|
+
'127.0.0.1',
|
|
37
|
+
'127.9.9.9',
|
|
38
|
+
'10.0.0.1',
|
|
39
|
+
'10.255.255.255',
|
|
40
|
+
'172.16.0.1',
|
|
41
|
+
'172.31.255.255',
|
|
42
|
+
'192.168.1.1',
|
|
43
|
+
'169.254.169.254', // AWS/GCP IMDS
|
|
44
|
+
'169.254.0.1',
|
|
45
|
+
'100.64.0.1', // CGNAT
|
|
46
|
+
'100.127.255.255',
|
|
47
|
+
'0.0.0.0',
|
|
48
|
+
'224.0.0.1', // multicast
|
|
49
|
+
'239.255.255.255',
|
|
50
|
+
'240.0.0.1', // reserved
|
|
51
|
+
'255.255.255.255',
|
|
52
|
+
'198.18.0.1', // benchmark
|
|
53
|
+
'192.0.2.5', // TEST-NET-1
|
|
54
|
+
'192.0.0.1', // IETF
|
|
55
|
+
];
|
|
56
|
+
for (const ip of blocked) {
|
|
57
|
+
assert.ok(classifyAddress(ip), `expected ${ip} to be BLOCKED but it was allowed`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('classifyAddress allows genuine public IPv4 (incl. just-outside-range boundaries)', () => {
|
|
62
|
+
const allowed = [
|
|
63
|
+
'8.8.8.8',
|
|
64
|
+
'1.1.1.1',
|
|
65
|
+
'93.184.216.34', // example.com
|
|
66
|
+
'172.15.255.255', // just below 172.16/12
|
|
67
|
+
'172.32.0.1', // just above 172.31
|
|
68
|
+
'100.63.255.255', // just below CGNAT
|
|
69
|
+
'100.128.0.1', // just above CGNAT
|
|
70
|
+
'169.253.255.255', // just below link-local
|
|
71
|
+
'11.0.0.1',
|
|
72
|
+
'192.167.255.255',
|
|
73
|
+
'192.169.0.1',
|
|
74
|
+
'223.255.255.255', // just below multicast
|
|
75
|
+
];
|
|
76
|
+
for (const ip of allowed) {
|
|
77
|
+
assert.equal(classifyAddress(ip), null, `expected ${ip} to be ALLOWED but it was blocked`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('classifyAddress blocks loopback / link-local / ULA / multicast + embedded-v4 IPv6', () => {
|
|
82
|
+
const blocked = [
|
|
83
|
+
'::1', // loopback
|
|
84
|
+
'::', // unspecified
|
|
85
|
+
'fe80::1', // link-local
|
|
86
|
+
'fe80::abcd:1234',
|
|
87
|
+
'fc00::1', // ULA
|
|
88
|
+
'fd12:3456::1', // ULA
|
|
89
|
+
'ff02::1', // multicast
|
|
90
|
+
'::ffff:127.0.0.1', // IPv4-mapped loopback
|
|
91
|
+
'::ffff:10.0.0.1', // IPv4-mapped private
|
|
92
|
+
'::ffff:169.254.169.254', // IPv4-mapped IMDS
|
|
93
|
+
'64:ff9b::127.0.0.1', // NAT64 → loopback
|
|
94
|
+
'64:ff9b::192.168.0.1', // NAT64 → private
|
|
95
|
+
];
|
|
96
|
+
for (const ip of blocked) {
|
|
97
|
+
assert.ok(classifyAddress(ip), `expected ${ip} to be BLOCKED but it was allowed`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('classifyAddress allows genuine public IPv6 (incl. IPv4-mapped public)', () => {
|
|
102
|
+
const allowed = [
|
|
103
|
+
'2001:4860:4860::8888', // Google DNS
|
|
104
|
+
'2606:4700:4700::1111', // Cloudflare DNS
|
|
105
|
+
'::ffff:8.8.8.8', // IPv4-mapped PUBLIC → allowed via embedded classify
|
|
106
|
+
'2a00:1450:4001:81b::200e',
|
|
107
|
+
];
|
|
108
|
+
for (const ip of allowed) {
|
|
109
|
+
assert.equal(classifyAddress(ip), null, `expected ${ip} to be ALLOWED but it was blocked`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('classifyAddress fail-closes on non-IP / garbage input', () => {
|
|
114
|
+
for (const bad of ['not-an-ip', 'example.com', '', '999.999.999.999', 'gggg::1']) {
|
|
115
|
+
assert.ok(classifyAddress(bad), `expected ${JSON.stringify(bad)} to be treated as blocked`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── IP parsers ───────────────────────────────────────────────────────────────
|
|
120
|
+
test('parseIPv4 rejects malformed dotted-quads', () => {
|
|
121
|
+
for (const bad of ['1.2.3', '1.2.3.4.5', '256.1.1.1', '01.2.3.4', '1.2.3.a', '', '-1.2.3.4']) {
|
|
122
|
+
assert.equal(parseIPv4(bad), null, `expected ${JSON.stringify(bad)} to fail parse`);
|
|
123
|
+
}
|
|
124
|
+
assert.deepEqual([...parseIPv4('1.2.3.4')], [1, 2, 3, 4]);
|
|
125
|
+
assert.deepEqual([...parseIPv4('255.255.255.255')], [255, 255, 255, 255]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('parseIPv6 handles ::, embedded IPv4, zone id; rejects malformed', () => {
|
|
129
|
+
assert.ok(parseIPv6('::1'));
|
|
130
|
+
assert.ok(parseIPv6('::'));
|
|
131
|
+
assert.ok(parseIPv6('fe80::1%en0')); // zone id stripped
|
|
132
|
+
assert.ok(parseIPv6('2001:db8::1'));
|
|
133
|
+
assert.ok(parseIPv6('::ffff:1.2.3.4'));
|
|
134
|
+
for (const bad of ['gggg::', '1::2::3', '12345::', '1.2.3.4', '']) {
|
|
135
|
+
assert.equal(parseIPv6(bad), null, `expected ${JSON.stringify(bad)} to fail parse`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── URL target ───────────────────────────────────────────────────────────────
|
|
140
|
+
test('parseHttpsTarget accepts https and extracts host/port', () => {
|
|
141
|
+
assert.deepEqual(parseHttpsTarget('https://example.com/a.png'), {
|
|
142
|
+
host: 'example.com',
|
|
143
|
+
port: 443,
|
|
144
|
+
});
|
|
145
|
+
assert.deepEqual(parseHttpsTarget('https://cdn.example.com:8443/x'), {
|
|
146
|
+
host: 'cdn.example.com',
|
|
147
|
+
port: 8443,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('parseHttpsTarget rejects non-https, credentials, and hostless URLs', () => {
|
|
152
|
+
const bad = [
|
|
153
|
+
'http://example.com/a.png',
|
|
154
|
+
'ftp://example.com/a.png',
|
|
155
|
+
'file:///etc/passwd',
|
|
156
|
+
'https://user:pass@example.com/a.png',
|
|
157
|
+
'not a url',
|
|
158
|
+
];
|
|
159
|
+
for (const u of bad) {
|
|
160
|
+
assert.throws(() => parseHttpsTarget(u), FetchAssetError, `expected ${u} to throw`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── image sniff ──────────────────────────────────────────────────────────────
|
|
165
|
+
test('sniffImageExt recognises png/jpg/gif/webp and rejects SVG/HTML/empty', () => {
|
|
166
|
+
const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0]);
|
|
167
|
+
const jpg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0, 0]);
|
|
168
|
+
const gif = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0, 0]);
|
|
169
|
+
const webp = Buffer.from([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50]);
|
|
170
|
+
assert.equal(sniffImageExt(png), 'png');
|
|
171
|
+
assert.equal(sniffImageExt(jpg), 'jpg');
|
|
172
|
+
assert.equal(sniffImageExt(gif), 'gif');
|
|
173
|
+
assert.equal(sniffImageExt(webp), 'webp');
|
|
174
|
+
assert.equal(sniffImageExt(Buffer.from('<svg xmlns="...')), null);
|
|
175
|
+
assert.equal(sniffImageExt(Buffer.from('<!DOCTYPE html>')), null);
|
|
176
|
+
assert.equal(sniffImageExt(Buffer.from([])), null);
|
|
177
|
+
// RIFF that is NOT webp (e.g. wav) must not sniff as an image.
|
|
178
|
+
assert.equal(
|
|
179
|
+
sniffImageExt(Buffer.from([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x41, 0x56, 0x45])),
|
|
180
|
+
null
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── naming + containment ─────────────────────────────────────────────────────
|
|
185
|
+
test('assetName is content-addressed <sha8>.<ext> and deterministic', () => {
|
|
186
|
+
const bytes = Buffer.from('hello world');
|
|
187
|
+
const n1 = assetName(bytes, 'png');
|
|
188
|
+
const n2 = assetName(bytes, 'png');
|
|
189
|
+
assert.equal(n1, n2);
|
|
190
|
+
assert.match(n1, /^[a-f0-9]{8}\.png$/);
|
|
191
|
+
assert.notEqual(assetName(Buffer.from('other'), 'jpg'), n1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('containedAssetPath resolves under <root>/<designRoot>/assets and rejects escape', () => {
|
|
195
|
+
const { assetsDir, fileAbs } = containedAssetPath('/repo', '.design', 'a1b2c3d4.png');
|
|
196
|
+
assert.equal(assetsDir, resolve('/repo/.design/assets'));
|
|
197
|
+
assert.equal(fileAbs, resolve('/repo/.design/assets/a1b2c3d4.png'));
|
|
198
|
+
// A traversal name escapes → throws.
|
|
199
|
+
assert.throws(() => containedAssetPath('/repo', '.design', '../../etc/passwd'), FetchAssetError);
|
|
200
|
+
// A design-root that climbs out of the repo → throws.
|
|
201
|
+
assert.throws(() => containedAssetPath('/repo', '../../etc', 'a1b2c3d4.png'), FetchAssetError);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ── wiring: verb registered + helper files ship ──────────────────────────────
|
|
205
|
+
test('fetch-asset is a registered `maude design` bin verb with both helper files present', () => {
|
|
206
|
+
assert.ok(existsSync(resolve(REPO, 'apps/studio/bin/fetch-asset.sh')), 'fetch-asset.sh missing');
|
|
207
|
+
assert.ok(
|
|
208
|
+
existsSync(resolve(REPO, 'apps/studio/bin/_fetch-asset.mjs')),
|
|
209
|
+
'_fetch-asset.mjs missing'
|
|
210
|
+
);
|
|
211
|
+
const design = readFileSync(resolve(REPO, 'cli/commands/design.mjs'), 'utf8');
|
|
212
|
+
assert.match(design, /'fetch-asset'/, "'fetch-asset' not in design.mjs BIN_VERBS");
|
|
213
|
+
});
|
|
@@ -39,6 +39,7 @@ export function buildBlock(designRel = '.design') {
|
|
|
39
39
|
`${root}/_locator.json`, // regenerable slug→path index
|
|
40
40
|
`${root}/_export-history.json`,
|
|
41
41
|
// Per-machine / per-user dirs.
|
|
42
|
+
`${root}/_export-jobs/`, // background-export job byte store (regenerable — feature-background-export-notification-center)
|
|
42
43
|
`${root}/_state/`, // binary CRDT logs (regenerable from hub)
|
|
43
44
|
`${root}/_history/`,
|
|
44
45
|
`${root}/_trash/`, // soft-deleted canvases (recoverable locally — Phase 22 delete)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
"test:e2e:desktop:onboarding": "pnpm --filter @maude/desktop-e2e e2e:onboarding"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
54
|
-
"@1agh/maude-darwin-x64": "0.
|
|
55
|
-
"@1agh/maude-linux-arm64": "0.
|
|
56
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
57
|
-
"@1agh/maude-linux-x64": "0.
|
|
58
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
59
|
-
"@1agh/maude-win32-x64": "0.
|
|
53
|
+
"@1agh/maude-darwin-arm64": "0.42.0",
|
|
54
|
+
"@1agh/maude-darwin-x64": "0.42.0",
|
|
55
|
+
"@1agh/maude-linux-arm64": "0.42.0",
|
|
56
|
+
"@1agh/maude-linux-arm64-musl": "0.42.0",
|
|
57
|
+
"@1agh/maude-linux-x64": "0.42.0",
|
|
58
|
+
"@1agh/maude-linux-x64-musl": "0.42.0",
|
|
59
|
+
"@1agh/maude-win32-x64": "0.42.0"
|
|
60
60
|
},
|
|
61
61
|
"files": [
|
|
62
62
|
"cli",
|
|
@@ -74,8 +74,10 @@
|
|
|
74
74
|
"commands/new.md",
|
|
75
75
|
"commands/edit.md",
|
|
76
76
|
"commands/smoke.md",
|
|
77
|
+
"commands/board.md",
|
|
77
78
|
"dev-server/bin/screenshot.sh",
|
|
78
79
|
"dev-server/bin/smoke.sh",
|
|
80
|
+
"dev-server/bin/canvas-rects.sh",
|
|
79
81
|
"agents/design-critic.md",
|
|
80
82
|
"agents/graphic-design-critic.md",
|
|
81
83
|
"agents/a11y-critic.md"
|
|
@@ -104,7 +106,9 @@
|
|
|
104
106
|
"dev-server/bin/_html-playwright.mjs",
|
|
105
107
|
"dev-server/bin/_pptx-playwright.mjs",
|
|
106
108
|
"dev-server/bin/_enumerate-artboards-playwright.mjs",
|
|
107
|
-
"dev-server/bin/screenshot.sh"
|
|
109
|
+
"dev-server/bin/screenshot.sh",
|
|
110
|
+
"dev-server/bin/_canvas-rects-playwright.mjs",
|
|
111
|
+
"dev-server/bin/canvas-rects.sh"
|
|
108
112
|
],
|
|
109
113
|
"docsUrl": "https://playwright.dev"
|
|
110
114
|
},
|
|
@@ -157,7 +161,11 @@
|
|
|
157
161
|
},
|
|
158
162
|
"autoInstall": false,
|
|
159
163
|
"fallbackBehavior": "Slash commands' bash blocks that read .ai/workflows.config.json via jq degrade to inline node one-liners; harder to read but functional.",
|
|
160
|
-
"usedBy": [
|
|
164
|
+
"usedBy": [
|
|
165
|
+
"dev-server/bin/bootstrap-check.sh",
|
|
166
|
+
"dev-server/bin/server-up.sh",
|
|
167
|
+
"dev-server/bin/canvas-rects.sh"
|
|
168
|
+
]
|
|
161
169
|
},
|
|
162
170
|
{
|
|
163
171
|
"id": "askuserquestion-mcp",
|