@1agh/maude 0.23.0 → 0.25.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/README.md +34 -1
- package/cli/bin/maude.mjs +3 -0
- package/cli/commands/cache.mjs +181 -0
- package/cli/commands/cache.test.mjs +166 -0
- package/cli/commands/design-link.test.mjs +32 -2
- package/cli/commands/design.mjs +95 -4
- package/cli/commands/design.test.mjs +56 -0
- package/cli/commands/help.mjs +34 -0
- package/cli/commands/hub.mjs +255 -30
- package/cli/commands/hub.test.mjs +126 -2
- package/cli/commands/init.mjs +3 -0
- package/cli/commands/preflight.mjs +11 -0
- package/cli/commands/preflight.test.mjs +46 -0
- package/cli/commands/scenario-report.mjs +45 -0
- package/cli/lib/cache.mjs +407 -0
- package/cli/lib/cache.test.mjs +303 -0
- package/cli/lib/design-link.mjs +74 -10
- package/cli/lib/flow-design-integration.test.mjs +165 -0
- package/cli/lib/gitignore-block.mjs +90 -0
- package/cli/lib/gitignore-block.test.mjs +123 -0
- package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
- package/cli/lib/preflight.mjs +41 -10
- package/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/ai-banner.tsx +2 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +763 -123
- package/plugins/design/dev-server/annotations-layer.tsx +1624 -208
- package/plugins/design/dev-server/api.ts +123 -4
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- package/plugins/design/dev-server/bin/preflight.sh +21 -10
- package/plugins/design/dev-server/bin/prep.sh +211 -0
- package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
- package/plugins/design/dev-server/bin/screenshot.sh +18 -1
- package/plugins/design/dev-server/bin/smoke.sh +208 -23
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +173 -0
- package/plugins/design/dev-server/canvas-icons.tsx +299 -0
- package/plugins/design/dev-server/canvas-lib.tsx +25 -21
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +404 -66
- package/plugins/design/dev-server/client/app.jsx +1231 -416
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
- package/plugins/design/dev-server/collab/index.ts +87 -9
- package/plugins/design/dev-server/collab/persistence.ts +34 -3
- package/plugins/design/dev-server/collab/registry.ts +80 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +24 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +364 -29
- package/plugins/design/dev-server/dist/comment-mount.js +78 -33
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/export-dialog.tsx +208 -18
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/fs-watch.ts +1 -0
- package/plugins/design/dev-server/http.ts +299 -28
- package/plugins/design/dev-server/input-router.tsx +35 -5
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +123 -1
- package/plugins/design/dev-server/sync/agent.ts +95 -0
- package/plugins/design/dev-server/sync/codec.ts +155 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/index.ts +479 -35
- package/plugins/design/dev-server/sync/materialize.ts +62 -0
- package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
- package/plugins/design/dev-server/sync/origins.ts +57 -0
- package/plugins/design/dev-server/sync/projection.ts +368 -0
- package/plugins/design/dev-server/sync/status.ts +115 -0
- package/plugins/design/dev-server/sync/untrusted.ts +153 -0
- package/plugins/design/dev-server/test/_helpers.ts +6 -2
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +311 -0
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +519 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +162 -0
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
- package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +32 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +132 -0
- package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
- package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
- package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
- package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +531 -4
- package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
- package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +39 -13
- package/plugins/design/dev-server/tool-palette.tsx +155 -24
- package/plugins/design/dev-server/undo-hud.tsx +4 -4
- package/plugins/design/dev-server/undo-stack.ts +20 -4
- package/plugins/design/dev-server/use-annotation-resize.tsx +219 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +68 -19
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +85 -10
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +14 -7
- package/plugins/flow/.claude-plugin/config.schema.json +5 -0
- package/plugins/flow/templates/ai-skeleton/README.md +1 -1
- package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
- package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
|
@@ -33,6 +33,17 @@ export type Target =
|
|
|
33
33
|
file: string;
|
|
34
34
|
/** True when `cssPath` is expected to match many elements; the adapter iterates. */
|
|
35
35
|
multi?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* True when the adapter should widen `cssPath` to its closest
|
|
38
|
+
* `[data-dc-screen]` ancestor before capture — i.e. "export the artboard
|
|
39
|
+
* containing this element". `selection` scope sets this `false` so the
|
|
40
|
+
* raster/vector captures the element EXACTLY (the item-3 bug was every
|
|
41
|
+
* single-target export hard-widening). `artboard` scope sets it `true`
|
|
42
|
+
* only in the fallback where we have a descendant selector but no
|
|
43
|
+
* artboard id; when the id IS known we target `[data-dc-screen="<id>"]`
|
|
44
|
+
* directly and leave this `false`.
|
|
45
|
+
*/
|
|
46
|
+
widen?: boolean;
|
|
36
47
|
}
|
|
37
48
|
| {
|
|
38
49
|
kind: 'file-tree';
|
|
@@ -49,6 +60,21 @@ export interface ActiveJsonShape {
|
|
|
49
60
|
| null;
|
|
50
61
|
}
|
|
51
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Submit-time hints captured from the LIVE iframe (the export dialog snapshots
|
|
65
|
+
* the canvas selection + the artboard under the viewport centre and rides them
|
|
66
|
+
* on the export `options` bag). The resolver prefers these over `activeJson`
|
|
67
|
+
* because opening the dialog / clicking Export can drop the persisted
|
|
68
|
+
* `_active.json.selected`, and they survive the cross-origin bridge unchanged.
|
|
69
|
+
* See the export-pipeline-fixes plan Task 1 + the selection-passthrough DDR.
|
|
70
|
+
*/
|
|
71
|
+
export interface ExportScopeHints {
|
|
72
|
+
/** Live selection at submit time. `selector` wins over `activeJson.selected`. */
|
|
73
|
+
selection?: { selector?: string; file?: string } | null;
|
|
74
|
+
/** `data-dc-screen` id of the artboard to export for `scope=artboard`. */
|
|
75
|
+
artboardId?: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
export interface ResolveScopeArgs {
|
|
53
79
|
scope: Scope;
|
|
54
80
|
activeJson: ActiveJsonShape;
|
|
@@ -56,6 +82,33 @@ export interface ResolveScopeArgs {
|
|
|
56
82
|
designRoot: string;
|
|
57
83
|
/** Absolute path to repo root. Required for `project-raw` to bound the walk. */
|
|
58
84
|
repoRoot?: string;
|
|
85
|
+
/**
|
|
86
|
+
* The export `options` bag, threaded from `runExport`. Read additively so
|
|
87
|
+
* existing callers (and the pure unit tests) that omit it stay green.
|
|
88
|
+
*/
|
|
89
|
+
options?: Record<string, unknown>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Narrow the free-form options bag to the selection/artboard hints we read. */
|
|
93
|
+
function readHints(options: Record<string, unknown> | undefined): ExportScopeHints {
|
|
94
|
+
if (!options || typeof options !== 'object') return {};
|
|
95
|
+
const sel = options.selection;
|
|
96
|
+
const selection =
|
|
97
|
+
sel && typeof sel === 'object'
|
|
98
|
+
? {
|
|
99
|
+
selector:
|
|
100
|
+
typeof (sel as Record<string, unknown>).selector === 'string'
|
|
101
|
+
? ((sel as Record<string, unknown>).selector as string)
|
|
102
|
+
: undefined,
|
|
103
|
+
file:
|
|
104
|
+
typeof (sel as Record<string, unknown>).file === 'string'
|
|
105
|
+
? ((sel as Record<string, unknown>).file as string)
|
|
106
|
+
: undefined,
|
|
107
|
+
}
|
|
108
|
+
: null;
|
|
109
|
+
const artboardId =
|
|
110
|
+
typeof options.artboardId === 'string' && options.artboardId ? options.artboardId : null;
|
|
111
|
+
return { selection, artboardId };
|
|
59
112
|
}
|
|
60
113
|
|
|
61
114
|
const RAW_EXCLUDES = new Set([
|
|
@@ -88,6 +141,16 @@ function slugify(file: string, designRel: string): string {
|
|
|
88
141
|
.toLowerCase();
|
|
89
142
|
}
|
|
90
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Escape a value for use inside a double-quoted CSS attribute selector
|
|
146
|
+
* (`[data-dc-screen="<here>"]`). Artboard ids are normally plain slugs, but a
|
|
147
|
+
* stray `"` or `\` would break out of the selector — escape defensively so the
|
|
148
|
+
* id is matched literally.
|
|
149
|
+
*/
|
|
150
|
+
function cssAttrEscape(value: string): string {
|
|
151
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
152
|
+
}
|
|
153
|
+
|
|
91
154
|
interface SelectionShape {
|
|
92
155
|
file?: string;
|
|
93
156
|
selector?: string;
|
|
@@ -147,37 +210,70 @@ export async function resolveScope(args: ResolveScopeArgs): Promise<Target[]> {
|
|
|
147
210
|
if (!activeFile) return [];
|
|
148
211
|
const slug = slugify(activeFile, designRel);
|
|
149
212
|
const sel = firstSelection(activeJson.selected);
|
|
213
|
+
const hints = readHints(args.options);
|
|
150
214
|
|
|
151
215
|
if (scope === 'selection') {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
216
|
+
// Prefer the live submit-time snapshot over the persisted `_active.json`
|
|
217
|
+
// selection — the dialog can clear the canvas selection before the bridged
|
|
218
|
+
// export runs, leaving `activeJson.selected` null (item 3 root cause #1).
|
|
219
|
+
const selector = hints.selection?.selector ?? sel?.selector ?? sel?.cssPath;
|
|
220
|
+
if (!selector) {
|
|
221
|
+
// No selection anywhere → fall back to artboard scope so the export
|
|
222
|
+
// still produces something useful. Single fallback site.
|
|
156
223
|
return resolveScope({ ...args, scope: 'artboard' });
|
|
157
224
|
}
|
|
158
|
-
const file =
|
|
225
|
+
const file = hints.selection?.file ?? sel?.file ?? activeFile;
|
|
159
226
|
return [
|
|
160
227
|
{
|
|
161
228
|
kind: 'element',
|
|
162
229
|
cssPath: selector,
|
|
163
230
|
canvasSlug: slugify(file, designRel),
|
|
164
231
|
file,
|
|
232
|
+
// Capture the element EXACTLY — do NOT widen to the enclosing artboard
|
|
233
|
+
// (item 3 root cause #2: every single-target export hard-widened).
|
|
234
|
+
widen: false,
|
|
165
235
|
},
|
|
166
236
|
];
|
|
167
237
|
}
|
|
168
238
|
|
|
169
239
|
if (scope === 'artboard') {
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
240
|
+
// Preferred path: the dialog captured which artboard is active (the
|
|
241
|
+
// selection's host, or the artboard under the viewport centre). Target it
|
|
242
|
+
// by id so EVERY format (PDF included — its shim doesn't widen) renders
|
|
243
|
+
// the right artboard, not `:first-of-type` (item 5 root cause).
|
|
244
|
+
if (hints.artboardId) {
|
|
245
|
+
return [
|
|
246
|
+
{
|
|
247
|
+
kind: 'element',
|
|
248
|
+
cssPath: `[data-dc-screen="${cssAttrEscape(hints.artboardId)}"]`,
|
|
249
|
+
canvasSlug: slug,
|
|
250
|
+
file: activeFile,
|
|
251
|
+
widen: false,
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
// Fallback: we only have a descendant selector → pass it through and let
|
|
256
|
+
// the adapter widen to the closest `[data-dc-screen]` ancestor.
|
|
257
|
+
const descendant = hints.selection?.selector ?? sel?.selector ?? sel?.cssPath;
|
|
258
|
+
if (descendant) {
|
|
259
|
+
return [
|
|
260
|
+
{
|
|
261
|
+
kind: 'element',
|
|
262
|
+
cssPath: descendant,
|
|
263
|
+
canvasSlug: slug,
|
|
264
|
+
file: activeFile,
|
|
265
|
+
widen: true,
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
// Last resort — first artboard on the active canvas.
|
|
175
270
|
return [
|
|
176
271
|
{
|
|
177
272
|
kind: 'element',
|
|
178
|
-
cssPath:
|
|
273
|
+
cssPath: '[data-dc-screen]:first-of-type',
|
|
179
274
|
canvasSlug: slug,
|
|
180
275
|
file: activeFile,
|
|
276
|
+
widen: false,
|
|
181
277
|
},
|
|
182
278
|
];
|
|
183
279
|
}
|
|
@@ -44,7 +44,10 @@ async function captureSvg(
|
|
|
44
44
|
if (target.multi) {
|
|
45
45
|
args.push('--multi', '1', '--out-dir', outDir);
|
|
46
46
|
} else {
|
|
47
|
-
|
|
47
|
+
// Widen to the enclosing artboard only when scope.ts requested it; a
|
|
48
|
+
// `selection` target captures the element exactly.
|
|
49
|
+
if (target.widen) args.push('--widen-to-artboard', '1');
|
|
50
|
+
args.push('--out', path.join(outDir, `${target.canvasSlug}.svg`));
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
// Run via `node` so the shim's `import 'playwright'` resolves against
|
|
@@ -40,6 +40,7 @@ export function createFsWatch(ctx: Context): FsWatch {
|
|
|
40
40
|
// Skip our own runtime artifacts.
|
|
41
41
|
if (filename.startsWith('_server.json')) return;
|
|
42
42
|
if (filename.startsWith('_active.json')) return;
|
|
43
|
+
if (filename.startsWith('_sync.json')) return;
|
|
43
44
|
if (filename.startsWith(`_history${path.sep}`) || filename.startsWith('_history/')) return;
|
|
44
45
|
emit(filename);
|
|
45
46
|
});
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// the route table without rewriting this module. The `fetch` export is the
|
|
5
5
|
// top-level fall-through for paths Bun's `routes` field doesn't cover.
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
9
|
+
import { dirname, join, posix, resolve, sep } from 'node:path';
|
|
9
10
|
|
|
10
11
|
import type { Api } from './api.ts';
|
|
11
12
|
import { buildCanvasModule } from './canvas-build.ts';
|
|
@@ -52,6 +53,67 @@ function ext(p: string): string {
|
|
|
52
53
|
return i === -1 ? '' : p.slice(i).toLowerCase();
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* T2 (9.1-A) — build the strict CSP for the canvas-content shell. Every inline
|
|
58
|
+
* `<script>` (importmap, module bootstrap, inspector) is allowlisted by sha256
|
|
59
|
+
* hash so we never resort to `'unsafe-inline'`. `connect-src 'self'` is locked
|
|
60
|
+
* to the document's own origin so hub-pushed JSX can't beacon out / hit IMDS /
|
|
61
|
+
* LAN — and `'self'` covers same-origin `ws:`/`wss:` (CSP3), so the HMR + collab
|
|
62
|
+
* sockets (which connect to the canvas origin the iframe loads from) still work
|
|
63
|
+
* while `ws://attacker` / `wss://attacker` exfil is refused. `style-src
|
|
64
|
+
* 'unsafe-inline'` is intentional —
|
|
65
|
+
* specimens use `style={{…}}` attributes + injected `<style>`; style injection
|
|
66
|
+
* is not the F1 RCE vector (script + connect are). No `'unsafe-eval'` — the
|
|
67
|
+
* POC verifies the runtime (motion/pixi/Bun.build output) doesn't need it.
|
|
68
|
+
*
|
|
69
|
+
* `webrtc 'block'` (A6, DDR-060 F1 re-audit) — `connect-src` governs only
|
|
70
|
+
* fetch/XHR/WebSocket/sendBeacon; WebRTC does NOT flow through Fetch, so an
|
|
71
|
+
* `RTCPeerConnection` with an attacker STUN/TURN hostname smuggles bytes out via
|
|
72
|
+
* ICE DNS/STUN even under `connect-src 'self'`. ⚠️ This directive is specified
|
|
73
|
+
* (CSP3) but UNIMPLEMENTED in shipping Chrome/Firefox as of 2026 (Chromium
|
|
74
|
+
* #40188662, Firefox bug 1783489) — currently a NO-OP. The enforceable control
|
|
75
|
+
* today is the RTC-constructor lockout in `templates/_shell.html`; this directive
|
|
76
|
+
* is kept for when browsers honor it. WebRTC + self-navigation exfil remain
|
|
77
|
+
* DOCUMENTED residuals of opt-in linked mode (the reachable data is collab
|
|
78
|
+
* metadata, not repo files — traversal is closed). Do NOT treat this as a closed
|
|
79
|
+
* exfil lane. The canvas runtime itself uses zero WebRTC.
|
|
80
|
+
*
|
|
81
|
+
* `frame-ancestors` (A6) — restricts who may embed the canvas document. The
|
|
82
|
+
* legit embedder is the main dev-server origin, so we allowlist exactly that
|
|
83
|
+
* (`mainOrigin`) plus `'self'`; an arbitrary external page can no longer reframe
|
|
84
|
+
* the canvas. When `mainOrigin` is unknown (tests / pre-boot) the directive is
|
|
85
|
+
* OMITTED rather than set to `'self'` — `'self'` alone would forbid the legit
|
|
86
|
+
* cross-origin embed and blank the canvas.
|
|
87
|
+
*/
|
|
88
|
+
export function cspForCanvasShell(html: string, mainOrigin?: string): string {
|
|
89
|
+
const hashes: string[] = [];
|
|
90
|
+
// Match inline <script> blocks only (no src=). `[^>]*` excludes any with src.
|
|
91
|
+
const re = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
|
|
92
|
+
let m: RegExpExecArray | null;
|
|
93
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex-exec loop.
|
|
94
|
+
while ((m = re.exec(html)) !== null) {
|
|
95
|
+
const body = m[1] ?? '';
|
|
96
|
+
const digest = createHash('sha256').update(body, 'utf8').digest('base64');
|
|
97
|
+
hashes.push(`'sha256-${digest}'`);
|
|
98
|
+
}
|
|
99
|
+
const scriptSrc = ["'self'", ...hashes].join(' ');
|
|
100
|
+
const directives = [
|
|
101
|
+
"default-src 'none'",
|
|
102
|
+
`script-src ${scriptSrc}`,
|
|
103
|
+
"connect-src 'self'",
|
|
104
|
+
"img-src 'self' data: blob:",
|
|
105
|
+
"style-src 'self' 'unsafe-inline'",
|
|
106
|
+
"font-src 'self' data:",
|
|
107
|
+
"frame-src 'self'",
|
|
108
|
+
"base-uri 'none'",
|
|
109
|
+
"object-src 'none'",
|
|
110
|
+
"form-action 'none'",
|
|
111
|
+
"webrtc 'block'",
|
|
112
|
+
];
|
|
113
|
+
if (mainOrigin) directives.push(`frame-ancestors 'self' ${mainOrigin}`);
|
|
114
|
+
return directives.join('; ');
|
|
115
|
+
}
|
|
116
|
+
|
|
55
117
|
function safePathUnderRoot(reqUrl: string, repoRoot: string): string | null {
|
|
56
118
|
let pathname: string;
|
|
57
119
|
try {
|
|
@@ -71,16 +133,80 @@ const DIST_DIR = join(HERE, 'dist');
|
|
|
71
133
|
const CLIENT_DIR = join(HERE, 'client');
|
|
72
134
|
const TEMPLATES_DIR = join(HERE, '..', 'templates');
|
|
73
135
|
|
|
74
|
-
// In-memory transpile cache. Key = absolute canvas path
|
|
75
|
-
//
|
|
76
|
-
//
|
|
136
|
+
// In-memory transpile cache. Key = absolute canvas path. Repeat GETs against an
|
|
137
|
+
// unchanged source skip the parse + ID-injection + Bun.Transpiler + Bun.build
|
|
138
|
+
// entirely.
|
|
77
139
|
interface CanvasCacheEntry {
|
|
78
|
-
|
|
140
|
+
/** Freshness signature over the .tsx AND every directly-imported sibling/
|
|
141
|
+
* relative `.css` it inlines (Bun.build extracts `import "./x.css"` into a
|
|
142
|
+
* `<style>` tag — there is no `<link>`). Rebuild when ANY of them changes.
|
|
143
|
+
* A prior version keyed only on the .tsx mtime, so editing a sibling
|
|
144
|
+
* `<canvas>.css` was a cache HIT: the HMR reload (mode:'module') re-served the
|
|
145
|
+
* stale inlined CSS and the edit only showed once the .tsx itself changed.
|
|
146
|
+
* (DDR-064 collab dogfooding finding, 2026-05-29.) */
|
|
147
|
+
sig: string;
|
|
79
148
|
etag: string;
|
|
80
149
|
js: string;
|
|
150
|
+
/** Absolute paths of the relative `.css` imports inlined into this build —
|
|
151
|
+
* re-statted each request to recompute `sig` without re-reading the source. */
|
|
152
|
+
cssDeps: string[];
|
|
81
153
|
}
|
|
82
154
|
const canvasCache = new Map<string, CanvasCacheEntry>();
|
|
83
155
|
|
|
156
|
+
// Per-process boot id, folded into every canvas ETag. The canvas transpile
|
|
157
|
+
// BUNDLES the shared chrome (canvas-lib → tool-palette / annotations-layer /
|
|
158
|
+
// use-tool-mode / canvas-cursors / …), but the freshness sig + base etag only
|
|
159
|
+
// track the canvas file + its inlined CSS — NOT those chrome sources. So a
|
|
160
|
+
// chrome edit left the etag unchanged → the browser's `If-None-Match` matched →
|
|
161
|
+
// 304 → the user kept getting the STALE transpile even after a server restart
|
|
162
|
+
// (the recurring "my cursor / chrome change didn't show up" bug). Folding the
|
|
163
|
+
// boot id in means a restart — the expected workflow after a dev-server source
|
|
164
|
+
// edit (see CLAUDE.md) — changes every canvas etag, so a normal reload re-fetches
|
|
165
|
+
// fresh chrome. (DDR-067.)
|
|
166
|
+
const RUNTIME_BOOT_ID = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
167
|
+
|
|
168
|
+
// Bumped by the chrome source watchers (below) on every canvas-lib / dev-server
|
|
169
|
+
// `.tsx`/`.ts` edit. Folded into the canvas ETag so a LIVE chrome edit (no
|
|
170
|
+
// restart) also changes the etag — otherwise clearing `canvasCache` re-transpiles
|
|
171
|
+
// fresh on the server, but the unchanged canvas SOURCE yields the same
|
|
172
|
+
// `result.etag`, the browser's `If-None-Match` matches → 304 → the user still
|
|
173
|
+
// gets the stale bundle even after the HMR hard-reload. (DDR-067.)
|
|
174
|
+
let CHROME_EPOCH = 0;
|
|
175
|
+
|
|
176
|
+
/** Relative `.css` import specifiers in a canvas source → absolute paths (the
|
|
177
|
+
* files Bun.build inlines). Bare / virtual specifiers (`@maude/…`, npm) are
|
|
178
|
+
* skipped — only on-disk relative CSS affects the inlined output. Resolved
|
|
179
|
+
* paths are clamped to `designRoot`: a canvas source is attacker-influenced
|
|
180
|
+
* under linked mode (DDR-054), and these paths are `stat`-ed for mtime, so a
|
|
181
|
+
* `../../../etc/…` specifier must not let the freshness probe reach outside the
|
|
182
|
+
* design tree. Legit DS imports (`../../system/<ds>/…`) stay inside designRoot. */
|
|
183
|
+
function cssDepsFromSource(source: string, canvasAbsPath: string, designRoot: string): string[] {
|
|
184
|
+
const dir = dirname(canvasAbsPath);
|
|
185
|
+
const root = resolve(designRoot);
|
|
186
|
+
const deps: string[] = [];
|
|
187
|
+
const re = /import\s+["']([^"']+\.css)["']/g;
|
|
188
|
+
let m: RegExpExecArray | null = re.exec(source);
|
|
189
|
+
while (m !== null) {
|
|
190
|
+
if (m[1].startsWith('.')) {
|
|
191
|
+
const abs = resolve(dir, m[1]);
|
|
192
|
+
if (abs === root || abs.startsWith(root + sep)) deps.push(abs);
|
|
193
|
+
}
|
|
194
|
+
m = re.exec(source);
|
|
195
|
+
}
|
|
196
|
+
return deps;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** mtime signature over the .tsx + its inlined CSS deps. A missing/unreadable
|
|
200
|
+
* file contributes 0 — a delete is itself a change, so the signature differs. */
|
|
201
|
+
function canvasFreshnessSig(tsxAbsPath: string, cssDeps: string[]): string {
|
|
202
|
+
const parts: string[] = [];
|
|
203
|
+
for (const p of [tsxAbsPath, ...cssDeps]) {
|
|
204
|
+
const mt = Bun.file(p).lastModified;
|
|
205
|
+
parts.push(`${p}@${Number.isFinite(mt) ? mt : 0}`);
|
|
206
|
+
}
|
|
207
|
+
return parts.join('|');
|
|
208
|
+
}
|
|
209
|
+
|
|
84
210
|
async function serveCanvasTsx(
|
|
85
211
|
absPath: string,
|
|
86
212
|
req: Request,
|
|
@@ -90,12 +216,16 @@ async function serveCanvasTsx(
|
|
|
90
216
|
const file = Bun.file(absPath);
|
|
91
217
|
if (!(await file.exists())) return new Response('Not found', { status: 404 });
|
|
92
218
|
|
|
93
|
-
//
|
|
94
|
-
|
|
219
|
+
// Freshness = the .tsx mtime AND every inlined sibling `.css` mtime. Keying on
|
|
220
|
+
// the .tsx alone meant a direct edit to `<canvas>.css` was a cache HIT, so the
|
|
221
|
+
// HMR reload (mode:'module') re-served the stale inlined CSS — the edit only
|
|
222
|
+
// surfaced once the .tsx itself changed (DDR-064 dogfooding finding).
|
|
95
223
|
let cached = canvasCache.get(absPath);
|
|
224
|
+
const sig = canvasFreshnessSig(absPath, cached?.cssDeps ?? []);
|
|
96
225
|
|
|
97
|
-
if (!cached || cached.
|
|
226
|
+
if (!cached || cached.sig !== sig) {
|
|
98
227
|
const source = await file.text();
|
|
228
|
+
const cssDeps = cssDepsFromSource(source, absPath, ctx.paths.designRoot);
|
|
99
229
|
let result: Awaited<ReturnType<typeof buildCanvasModule>>;
|
|
100
230
|
try {
|
|
101
231
|
result = await buildCanvasModule(absPath, source, {
|
|
@@ -114,7 +244,17 @@ async function serveCanvasTsx(
|
|
|
114
244
|
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
115
245
|
});
|
|
116
246
|
}
|
|
117
|
-
|
|
247
|
+
// Recompute the signature against the freshly-parsed deps — this very edit
|
|
248
|
+
// may have added or removed a `.css` import.
|
|
249
|
+
cached = {
|
|
250
|
+
sig: canvasFreshnessSig(absPath, cssDeps),
|
|
251
|
+
// Fold in the boot id (restart) + chrome epoch (live edit) so a chrome
|
|
252
|
+
// change busts the browser's cached transpile even when the canvas source
|
|
253
|
+
// (hence result.etag) is unchanged. See RUNTIME_BOOT_ID / CHROME_EPOCH.
|
|
254
|
+
etag: `${result.etag}-${RUNTIME_BOOT_ID}-${CHROME_EPOCH}`,
|
|
255
|
+
js: result.js,
|
|
256
|
+
cssDeps,
|
|
257
|
+
};
|
|
118
258
|
canvasCache.set(absPath, cached);
|
|
119
259
|
// Persist the locator map. Awaited so the inspector / Phase-12 layers
|
|
120
260
|
// panel sees a consistent (cdId -> source) view by the time the canvas
|
|
@@ -156,6 +296,22 @@ async function serveFile(absPath: string, headers: Record<string, string> = {}):
|
|
|
156
296
|
export interface Http {
|
|
157
297
|
routes: Record<string, (req: Request) => Response | Promise<Response>>;
|
|
158
298
|
fetch(req: Request): Promise<Response>;
|
|
299
|
+
/**
|
|
300
|
+
* T2 (9.1-A) — build the canvas mount-harness response. `applyCsp` adds the
|
|
301
|
+
* strict CSP (always on for the segregated canvas origin; the legacy main
|
|
302
|
+
* origin keeps it env-gated for the POC). Shared so both listeners produce
|
|
303
|
+
* byte-identical HTML.
|
|
304
|
+
*/
|
|
305
|
+
serveCanvasShell(applyCsp: boolean): Promise<Response>;
|
|
306
|
+
/**
|
|
307
|
+
* T2 (9.1-A) — allowlist gate for the segregated canvas origin. Returns true
|
|
308
|
+
* only for the routes the canvas runtime legitimately needs (shell, runtime
|
|
309
|
+
* bundles, comment-mount, transpiled .tsx + CSS/assets under designRoot,
|
|
310
|
+
* git-user, canvas-meta, health). Everything else is 403'd at the door so
|
|
311
|
+
* hub-pushed JSX can't reach /_api/export, /_config, /_sync-status, comments,
|
|
312
|
+
* the app shell, or arbitrary repo files. WS upgrades are gated in server.ts.
|
|
313
|
+
*/
|
|
314
|
+
isCanvasSafeRoute(pathname: string): boolean;
|
|
159
315
|
}
|
|
160
316
|
|
|
161
317
|
export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActivity): Http {
|
|
@@ -176,6 +332,7 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
176
332
|
ctx.bus.on('fs:any', (rel: string) => {
|
|
177
333
|
if (rel.startsWith('_lib/')) {
|
|
178
334
|
canvasCache.clear();
|
|
335
|
+
CHROME_EPOCH++;
|
|
179
336
|
}
|
|
180
337
|
});
|
|
181
338
|
|
|
@@ -183,6 +340,7 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
183
340
|
try {
|
|
184
341
|
libWatcher = watch(canvasLibPath(), () => {
|
|
185
342
|
canvasCache.clear();
|
|
343
|
+
CHROME_EPOCH++;
|
|
186
344
|
ctx.bus.emit('fs:any', '_lib/canvas-lib.tsx');
|
|
187
345
|
});
|
|
188
346
|
} catch (err) {
|
|
@@ -194,21 +352,27 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
194
352
|
void libWatcher;
|
|
195
353
|
|
|
196
354
|
// G7v2 — canvas-lib.tsx transitively imports many dev-server siblings
|
|
197
|
-
// (canvas-shell, contextual-toolbar, equal-spacing-handles,
|
|
198
|
-
// any of them invalidates the bundled canvas
|
|
199
|
-
// the mtime-keyed `canvasCache` keeps serving
|
|
200
|
-
// user sees pre-edit behaviour even after a hard
|
|
355
|
+
// (canvas-shell, contextual-toolbar, equal-spacing-handles, tool-palette,
|
|
356
|
+
// use-tool-mode, ...). Editing any of them invalidates the bundled canvas
|
|
357
|
+
// output. Without watching them the mtime-keyed `canvasCache` keeps serving
|
|
358
|
+
// the stale bundle and the user sees pre-edit behaviour even after a hard
|
|
359
|
+
// iframe reload.
|
|
201
360
|
//
|
|
202
|
-
// Recursive watch over DEV_SERVER_ROOT, filtered to .tsx
|
|
203
|
-
//
|
|
204
|
-
// (`
|
|
361
|
+
// Recursive watch over DEV_SERVER_ROOT, filtered to .tsx AND .ts: the chrome
|
|
362
|
+
// graph includes plain `.ts` modules that DO reach the canvas
|
|
363
|
+
// (`canvas-cursors.ts`, `canvas-arrowheads.ts`) — the prior `.tsx`-only filter
|
|
364
|
+
// skipped them, so cursor edits never busted the cache (the recurring stale-
|
|
365
|
+
// cursor bug, DDR-067). Server-only `.ts` (api / http / context) also clears
|
|
366
|
+
// here, which is harmless (a needless rebuild, not a stale serve). Test files
|
|
367
|
+
// (`test/`), built output (`dist/`, `client/`), and node_modules are skipped.
|
|
205
368
|
let devSrcWatcher: ReturnType<typeof watch> | null = null;
|
|
206
369
|
try {
|
|
207
370
|
devSrcWatcher = watch(DEV_SERVER_ROOT, { recursive: true }, (_evt, filename) => {
|
|
208
371
|
if (!filename) return;
|
|
209
|
-
if (!filename.endsWith('.tsx')) return;
|
|
372
|
+
if (!filename.endsWith('.tsx') && !filename.endsWith('.ts')) return;
|
|
210
373
|
if (filename.startsWith('test/') || filename.startsWith('test\\')) return;
|
|
211
374
|
if (filename.startsWith('dist/') || filename.startsWith('client/')) return;
|
|
375
|
+
if (filename.includes('node_modules')) return;
|
|
212
376
|
canvasCache.clear();
|
|
213
377
|
ctx.bus.emit('fs:any', `_lib/${filename}`);
|
|
214
378
|
});
|
|
@@ -241,7 +405,24 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
241
405
|
|
|
242
406
|
'/_active': () => Response.json(inspect.state),
|
|
243
407
|
|
|
244
|
-
|
|
408
|
+
// Phase 9 Task 8 — offline-mode banner poll fallback. The linked-mode sync
|
|
409
|
+
// runtime writes `_sync.json`; browser tabs also get live pushes over the
|
|
410
|
+
// WS ('sync:status'). Returns `{ linked: false }` in solo mode.
|
|
411
|
+
'/_sync-status': () => {
|
|
412
|
+
const file = join(ctx.paths.designRoot, '_sync.json');
|
|
413
|
+
if (!existsSync(file)) {
|
|
414
|
+
return Response.json({ linked: false }, { headers: { 'Cache-Control': 'no-store' } });
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
return Response.json(JSON.parse(readFileSync(file, 'utf8')), {
|
|
418
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
419
|
+
});
|
|
420
|
+
} catch {
|
|
421
|
+
return Response.json({ linked: false }, { headers: { 'Cache-Control': 'no-store' } });
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
'/_config': () => Response.json({ ...ctx.cfg, canvasOrigin: ctx.canvasOrigin }),
|
|
245
426
|
|
|
246
427
|
'/_index-data': async () =>
|
|
247
428
|
Response.json(await api.buildIndexData(), { headers: { 'Cache-Control': 'no-store' } }),
|
|
@@ -585,14 +766,10 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
585
766
|
// Query parameter ?canvas=<path-relative-to-designRoot> tells the shell
|
|
586
767
|
// which canvas to import + mount. See plugins/design/templates/_shell.html.
|
|
587
768
|
if (pathname === '/_canvas-shell.html' || pathname === '/_canvas-shell') {
|
|
588
|
-
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
const injected = inspect.injectInspector(shellHtml);
|
|
593
|
-
return new Response(injected, {
|
|
594
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' },
|
|
595
|
-
});
|
|
769
|
+
// The segregated canvas origin (server.ts) calls serveCanvasShell(true)
|
|
770
|
+
// directly with CSP always on; on the legacy main origin the CSP stays
|
|
771
|
+
// env-gated (MAUDE_CSP_POC) for the POC / backwards-compat.
|
|
772
|
+
return serveCanvasShell(process.env.MAUDE_CSP_POC === '1');
|
|
596
773
|
}
|
|
597
774
|
|
|
598
775
|
// Fall-through: serve user repo files (designRoot + everything under repoRoot).
|
|
@@ -622,5 +799,99 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
622
799
|
}
|
|
623
800
|
}
|
|
624
801
|
|
|
625
|
-
|
|
802
|
+
async function serveCanvasShell(applyCsp: boolean): Promise<Response> {
|
|
803
|
+
const shellHtml = await Bun.file(join(TEMPLATES_DIR, '_shell.html')).text();
|
|
804
|
+
// Inject inspector overlay — Cmd+Click selection + add-comment flow.
|
|
805
|
+
const injected = inspect.injectInspector(shellHtml);
|
|
806
|
+
const headers: Record<string, string> = {
|
|
807
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
808
|
+
'Cache-Control': 'no-store',
|
|
809
|
+
};
|
|
810
|
+
if (applyCsp) headers['Content-Security-Policy'] = cspForCanvasShell(injected, ctx.mainOrigin);
|
|
811
|
+
return new Response(injected, { headers });
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Canvas assets the segregated origin may serve out of designRoot. Excludes
|
|
815
|
+
// `.json` so no `*.meta.json` / `config.json` / `_comments/*.json` leaks via
|
|
816
|
+
// the static lane (canvas-meta goes through the gated /_api route instead).
|
|
817
|
+
const CANVAS_ASSET_EXTS = new Set([
|
|
818
|
+
'.tsx',
|
|
819
|
+
'.css',
|
|
820
|
+
'.svg',
|
|
821
|
+
'.png',
|
|
822
|
+
'.jpg',
|
|
823
|
+
'.jpeg',
|
|
824
|
+
'.gif',
|
|
825
|
+
'.webp',
|
|
826
|
+
'.ico',
|
|
827
|
+
'.woff',
|
|
828
|
+
'.woff2',
|
|
829
|
+
'.ttf',
|
|
830
|
+
'.otf',
|
|
831
|
+
]);
|
|
832
|
+
|
|
833
|
+
// Exact API paths the canvas iframe needs (collab + display data). See
|
|
834
|
+
// isCanvasSafeRoute for the trust rationale. Mutations are limited to inert
|
|
835
|
+
// collab data (annotations SVG, comment replies via the dynamic route).
|
|
836
|
+
const CANVAS_SAFE_API = new Set([
|
|
837
|
+
'/_api/git-user', // presence display name
|
|
838
|
+
'/_api/canvas-meta', // layout/viewport sidecar (GET + PATCH)
|
|
839
|
+
'/_api/annotations', // annotation SVG (GET + PUT) — drives the collab bridge
|
|
840
|
+
'/_api/git-committers', // @mention autocomplete
|
|
841
|
+
'/_api/ai', // AI-activity banner
|
|
842
|
+
'/_comments', // per-file comment list (renders pins)
|
|
843
|
+
]);
|
|
844
|
+
|
|
845
|
+
function isCanvasSafeRoute(pathname: string): boolean {
|
|
846
|
+
// A1/A2 (DDR-060 F1 re-audit, phase-9.1-t2-f1-cross-origin-reaudit.md) —
|
|
847
|
+
// DECODE + NORMALIZE before gating. `URL.pathname` preserves `%2f` (it does
|
|
848
|
+
// NOT decode it to `/`), so a raw allowlist check on the encoded path is
|
|
849
|
+
// fooled: `/.design/..%2fsite%2fx.css` reads as ONE opaque segment under the
|
|
850
|
+
// designRoot with an asset ext (the `_`-segment + ext checks see no literal
|
|
851
|
+
// slash to split on), yet `safePathUnderRoot` later DECODES the same `%2f`,
|
|
852
|
+
// turns `..%2f` into a real `../`, and climbs out of the designRoot — re-
|
|
853
|
+
// confined only to repoRoot. That decode mismatch let a hub-pushed canvas
|
|
854
|
+
// read any repo `.tsx`/`.css`/`.svg`/font + `_history` snapshots. Decoding
|
|
855
|
+
// here makes the gate agree with the resolver: `..%2f` → `../`, normalize
|
|
856
|
+
// collapses it, and the path no longer matches designPrefix → 403. A
|
|
857
|
+
// malformed escape (`%ZZ`) throws → reject. This gate runs ONLY on the
|
|
858
|
+
// segregated canvas origin (server.ts), so the main origin is untouched.
|
|
859
|
+
let safe: string;
|
|
860
|
+
try {
|
|
861
|
+
safe = posix.normalize(decodeURIComponent(pathname));
|
|
862
|
+
} catch {
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
if (safe === '/_canvas-shell.html' || safe === '/_canvas-shell') return true;
|
|
866
|
+
if (safe === '/_health') return true;
|
|
867
|
+
if (safe === '/_client/comment-mount.js') return true;
|
|
868
|
+
// Canvas-chrome stylesheets (composer / thread / pin / cursor CSS). Inert
|
|
869
|
+
// static assets from the dev-server distribution — no secrets, no code
|
|
870
|
+
// exec, no repo content. Without this the cross-origin canvas 403s e.g.
|
|
871
|
+
// `/_client/comments-overlay.css`, so the in-iframe comment composer renders
|
|
872
|
+
// unstyled and, missing `position: fixed`, collapses to the top-left (0,0).
|
|
873
|
+
// Allowed by pattern (not per-file) so future chrome CSS can't silently
|
|
874
|
+
// regress the same way.
|
|
875
|
+
if (safe.startsWith('/_client/') && ext(safe) === '.css') return true;
|
|
876
|
+
if (safe.startsWith('/_canvas-runtime/')) return true;
|
|
877
|
+
// Collab + display-data endpoints the canvas runtime legitimately calls from
|
|
878
|
+
// inside the iframe. All are reads or inert collab writes (annotations SVG,
|
|
879
|
+
// comment replies) — the "safe to sync" set per DDR-054. None expose code
|
|
880
|
+
// execution, secrets, export, /_config, /_sync-status, or files outside
|
|
881
|
+
// designRoot/annotations; the canvas origin's CSP `connect-src 'self'` still
|
|
882
|
+
// confines the iframe so hub-pushed JSX can't reach IMDS/LAN/main-origin.
|
|
883
|
+
if (CANVAS_SAFE_API.has(safe)) return true;
|
|
884
|
+
// POST /_api/comments/<id>/reply — dynamic path (fetch-handled).
|
|
885
|
+
if (/^\/_api\/comments\/[A-Za-z0-9_]+\/reply$/.test(safe)) return true;
|
|
886
|
+
const designPrefix = `/${ctx.paths.designRel.replace(/^\/+|\/+$/g, '')}/`;
|
|
887
|
+
if (safe.startsWith(designPrefix)) {
|
|
888
|
+
const rest = safe.slice(designPrefix.length);
|
|
889
|
+
// Reject runtime/state dirs+files (_comments, _sync.json, _history, …).
|
|
890
|
+
if (rest.split('/').some((seg) => seg.startsWith('_'))) return false;
|
|
891
|
+
return CANVAS_ASSET_EXTS.has(ext(safe));
|
|
892
|
+
}
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return { routes, fetch, serveCanvasShell, isCanvasSafeRoute };
|
|
626
897
|
}
|