@1agh/maude 0.41.0 → 0.43.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/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/annotations-layer.tsx +1055 -278
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +173 -63
- package/apps/studio/bin/_html-playwright.mjs +26 -4
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- 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/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
- package/apps/studio/bin/_svg-playwright.mjs +26 -4
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +160 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/screenshot.sh +53 -4
- package/apps/studio/canvas-edit.ts +459 -10
- package/apps/studio/canvas-lib.tsx +523 -5
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +476 -88
- package/apps/studio/client/export-center.jsx +460 -0
- package/apps/studio/client/github.js +4 -0
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +437 -0
- package/apps/studio/client/styles/3-shell-maude.css +59 -0
- package/apps/studio/client/styles/4-components.css +150 -0
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +2 -2
- package/apps/studio/context-menu.tsx +31 -6
- package/apps/studio/dist/client.bundle.js +5561 -19
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/export-dialog.tsx +18 -25
- 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 +376 -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 +37 -19
- package/apps/studio/footage/schema.test.ts +195 -0
- package/apps/studio/footage/schema.ts +484 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +198 -20
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +318 -64
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/server.ts +8 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +19 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +46 -0
- 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/footage-store.test.ts +100 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +43 -20
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +38 -1
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +70 -0
- package/apps/studio/ws.ts +6 -0
- package/cli/commands/design.mjs +34 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +1 -0
package/apps/studio/http.ts
CHANGED
|
@@ -10,20 +10,23 @@ import { dirname, join, posix, relative, resolve, sep } from 'node:path';
|
|
|
10
10
|
|
|
11
11
|
import { probeAcpAvailability } from './acp/probe.ts';
|
|
12
12
|
import { deleteChat, listChats, readChatMessages } from './acp/transcript.ts';
|
|
13
|
-
import { type Api, ASSET_MAX_VIDEO_BYTES } from './api.ts';
|
|
13
|
+
import { type Api, ASSET_MAX_BYTES, ASSET_MAX_VIDEO_BYTES } from './api.ts';
|
|
14
14
|
import { buildCanvasModule } from './canvas-build.ts';
|
|
15
15
|
import { canvasLibPath } from './canvas-lib-resolver.ts';
|
|
16
16
|
import { TranspileError } from './canvas-pipeline.ts';
|
|
17
17
|
import type { AiActivity } from './collab/ai-activity.ts';
|
|
18
18
|
import type { Context } from './context.ts';
|
|
19
|
-
import { isFormat, isScope,
|
|
19
|
+
import { type Format, isFormat, isScope, type Scope } from './exporters/index.ts';
|
|
20
|
+
import { type ExportJobQueue, ExportQueueFullError } from './exporters/jobs.ts';
|
|
20
21
|
import type { ActiveJsonShape } from './exporters/scope.ts';
|
|
22
|
+
import { createFootageStore, FOOTAGE_MAX_BYTES } from './footage-store.ts';
|
|
21
23
|
import { createGitEndpoints } from './git/endpoints.ts';
|
|
22
24
|
import { gitShowFile } from './git/service.ts';
|
|
23
25
|
import { createGitHubEndpoints } from './github/endpoints.ts';
|
|
24
26
|
import type { Inspect } from './inspect.ts';
|
|
25
27
|
import { canvasSlug, writeLocator } from './locator.ts';
|
|
26
|
-
import { DEV_SERVER_ROOT } from './paths.ts';
|
|
28
|
+
import { DEV_SERVER_ROOT, STICKERS_DIR } from './paths.ts';
|
|
29
|
+
import { createPhotoStore, PHOTO_EDIT_MAX_BYTES } from './photo-store.ts';
|
|
27
30
|
import { probeReadiness } from './readiness.ts';
|
|
28
31
|
import { getRuntimeBundle, packageForSlug } from './runtime-bundle.ts';
|
|
29
32
|
import { linkHub } from './sync/hub-link.ts';
|
|
@@ -594,7 +597,13 @@ export interface Http {
|
|
|
594
597
|
isCanvasSafeRoute(pathname: string): boolean;
|
|
595
598
|
}
|
|
596
599
|
|
|
597
|
-
export function createHttp(
|
|
600
|
+
export function createHttp(
|
|
601
|
+
ctx: Context,
|
|
602
|
+
api: Api,
|
|
603
|
+
inspect: Inspect,
|
|
604
|
+
ai: AiActivity,
|
|
605
|
+
exportJobs: ExportJobQueue
|
|
606
|
+
): Http {
|
|
598
607
|
// Cache invalidation — when canvas-lib changes, every cached canvas bundle
|
|
599
608
|
// is stale because canvas-lib is inlined into each one via the resolver
|
|
600
609
|
// plugin. Drop the whole cache so the next request rebuilds with the fresh
|
|
@@ -714,6 +723,12 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
714
723
|
// `routes` map (the dual-allowlist rule), so the untrusted canvas iframe origin
|
|
715
724
|
// can never reach status/commit/publish/get-latest. http.ts owns the gating;
|
|
716
725
|
// git/endpoints.ts owns the orchestration.
|
|
726
|
+
const photoStore = createPhotoStore(ctx);
|
|
727
|
+
// feature-footage-analysis-director — footage-analysis + EDL sidecars.
|
|
728
|
+
// MAIN-ORIGIN ONLY (privileged): the `/_api/footage` route is intentionally
|
|
729
|
+
// absent from CANVAS_SAFE_API + startCanvasServer's `routes` map, so the
|
|
730
|
+
// untrusted canvas iframe origin can never read/write the director's analysis.
|
|
731
|
+
const footageStore = createFootageStore(ctx);
|
|
717
732
|
const gitApi = createGitEndpoints(ctx);
|
|
718
733
|
// Phase 28 (E3) — `/_api/github/*`. Same dual-allowlist rule: main-origin only,
|
|
719
734
|
// and every route is token-bearing (server-held keychain token via the loopback
|
|
@@ -722,6 +737,38 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
722
737
|
const gitJson = (r: { status: number; json: unknown }) =>
|
|
723
738
|
Response.json(r.json, { status: r.status, headers: { 'Cache-Control': 'no-store' } });
|
|
724
739
|
|
|
740
|
+
// Shared by /_api/export and /_api/export-jobs — build the exportJobs.enqueue()
|
|
741
|
+
// args from a validated request body. `inspect.state` is the live `_active.json`;
|
|
742
|
+
// readers narrow to the resolver's subset locally so the export pipeline doesn't
|
|
743
|
+
// pin the wider ActiveState interface.
|
|
744
|
+
function buildExportArgs(
|
|
745
|
+
req: Request,
|
|
746
|
+
body: { format: Format; scope: Scope; options?: Record<string, unknown> }
|
|
747
|
+
) {
|
|
748
|
+
const activeJson = inspect.state as unknown as ActiveJsonShape;
|
|
749
|
+
return {
|
|
750
|
+
format: body.format,
|
|
751
|
+
scope: body.scope,
|
|
752
|
+
options: body.options ?? {},
|
|
753
|
+
resolve: { activeJson, designRoot: ctx.paths.designRoot, repoRoot: ctx.paths.repoRoot },
|
|
754
|
+
ctx: {
|
|
755
|
+
designRoot: ctx.paths.designRoot,
|
|
756
|
+
repoRoot: ctx.paths.repoRoot,
|
|
757
|
+
// Adapters reach back into the server via this origin only when they
|
|
758
|
+
// need Playwright rendering (PNG / PDF / SVG / HTML). The host that
|
|
759
|
+
// received this request is, by definition, the one serving the canvas.
|
|
760
|
+
serverOrigin: new URL(req.url).origin,
|
|
761
|
+
// Mirror `client/app.jsx:85` — the per-DS tokensCssRel wins over the
|
|
762
|
+
// legacy top-level default (which still points at the pre-multi-DS
|
|
763
|
+
// layout `system/colors_and_type.css`). Without the per-DS path, the
|
|
764
|
+
// standalone `_canvas-shell.html` 404s on the tokens link and the
|
|
765
|
+
// rendered DOM uses `var(--bg-0)` unresolved → screenshots come out
|
|
766
|
+
// blank. See canvasShellUrl().
|
|
767
|
+
tokensCssRel: ctx.cfg.designSystems?.[0]?.tokensCssRel ?? ctx.cfg.tokensCssRel,
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
725
772
|
const routes = {
|
|
726
773
|
'/_health': () =>
|
|
727
774
|
Response.json({
|
|
@@ -797,8 +844,8 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
797
844
|
// expands to so Claude can Read it. MAIN-ORIGIN ONLY: sameOriginWrite CSRF gate
|
|
798
845
|
// on POST + deliberately absent from CANVAS_SAFE_API + startCanvasServer routes,
|
|
799
846
|
// so the untrusted canvas iframe is 403'd. The disk caps live in
|
|
800
|
-
// api.saveChatAttachment (magic-byte sniff /
|
|
801
|
-
// session write budget).
|
|
847
|
+
// api.saveChatAttachment (magic-byte sniff / ASSET_MAX_BYTES / content-
|
|
848
|
+
// addressed name / session write budget).
|
|
802
849
|
//
|
|
803
850
|
// GET ?name=<sha8>.<ext> serves the pasted image back to the chat feed
|
|
804
851
|
// (thumbnail + lightbox). The name is regex-allowlisted in
|
|
@@ -825,9 +872,12 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
825
872
|
if (!isLoopbackHost(req.headers.get('host')))
|
|
826
873
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
827
874
|
const declared = Number(req.headers.get('content-length') || '0');
|
|
828
|
-
if (Number.isFinite(declared) && declared >
|
|
875
|
+
if (Number.isFinite(declared) && declared > ASSET_MAX_BYTES) {
|
|
829
876
|
return Response.json(
|
|
830
|
-
{
|
|
877
|
+
{
|
|
878
|
+
ok: false,
|
|
879
|
+
error: `attachment exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`,
|
|
880
|
+
},
|
|
831
881
|
{ status: 413, headers: { 'Cache-Control': 'no-store' } }
|
|
832
882
|
);
|
|
833
883
|
}
|
|
@@ -930,6 +980,87 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
930
980
|
return new Response('Method not allowed', { status: 405 });
|
|
931
981
|
},
|
|
932
982
|
|
|
983
|
+
'/_api/photo-edit': async (req: Request) => {
|
|
984
|
+
// feature-photo-editor (Stage C, Task 8) — non-destructive PhotoEdit sidecar.
|
|
985
|
+
// GET ?asset=<sha8 | assets/<sha8>.<ext>> → stored PhotoEdit or {}
|
|
986
|
+
// PUT/POST ?asset=<...> body { …PhotoEdit } → validated + persisted
|
|
987
|
+
// CANVAS-SAFE (listed in BOTH CANVAS_SAFE_API below AND startCanvasServer's
|
|
988
|
+
// `routes` map — Bun matches `routes` before `fetch`, so a one-list entry
|
|
989
|
+
// 404s from the canvas iframe, the DDR-088 rollout bug). Reached from the
|
|
990
|
+
// canvas origin by <PhotoLayer> (GET) and the headless bg-remove harness
|
|
991
|
+
// (PUT), so NO sameOriginWrite (it would block the legit canvas origin);
|
|
992
|
+
// the photo-store cap stack (sha8 validate + containment + validatePhotoEdit
|
|
993
|
+
// + size cap) is the load-bearing mitigation. loopback-Host gates DNS-
|
|
994
|
+
// rebinding on every method.
|
|
995
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
996
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
997
|
+
const asset = new URL(req.url).searchParams.get('asset');
|
|
998
|
+
if (req.method === 'GET') {
|
|
999
|
+
const edit = await photoStore.getPhotoEdit(asset);
|
|
1000
|
+
return Response.json(edit ?? {}, { headers: { 'Cache-Control': 'no-store' } });
|
|
1001
|
+
}
|
|
1002
|
+
if (req.method === 'PUT' || req.method === 'POST') {
|
|
1003
|
+
const body = await readJson<unknown>(req, PHOTO_EDIT_MAX_BYTES + 1024);
|
|
1004
|
+
if (body == null) return new Response('body required', { status: 400 });
|
|
1005
|
+
const result = await photoStore.savePhotoEdit(asset, body);
|
|
1006
|
+
if (!result.ok) {
|
|
1007
|
+
return Response.json(
|
|
1008
|
+
{ ok: false, error: result.error },
|
|
1009
|
+
{ status: result.status, headers: { 'Cache-Control': 'no-store' } }
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
return Response.json(
|
|
1013
|
+
{ ok: true, path: result.path, edit: result.edit },
|
|
1014
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
return new Response('Method not allowed', { status: 405 });
|
|
1018
|
+
},
|
|
1019
|
+
|
|
1020
|
+
'/_api/footage': async (req: Request) => {
|
|
1021
|
+
// feature-footage-analysis-director (Task 2) — FootageAnalysis + EDL sidecars.
|
|
1022
|
+
// GET ?asset=<sha8|assets/…> → stored FootageAnalysis or {}
|
|
1023
|
+
// GET ?slug=<cut-slug> → stored Edl or {}
|
|
1024
|
+
// PUT/POST ?asset=<…> body {FootageAnalysis} → validated + persisted
|
|
1025
|
+
// PUT/POST ?slug=<…> body {Edl} → validated + persisted
|
|
1026
|
+
// MAIN-ORIGIN ONLY (privileged — NOT in CANVAS_SAFE_API / startCanvasServer
|
|
1027
|
+
// routes): written by the footage-analyst / footage-director agents over
|
|
1028
|
+
// loopback, never the untrusted canvas iframe. A GET from the canvas origin
|
|
1029
|
+
// 403s at the gate (canvas-origin-gate.test.ts). loopback-Host gates
|
|
1030
|
+
// DNS-rebinding on every method; the footage-store cap stack (sha8/slug
|
|
1031
|
+
// validate + containment + validate{FootageAnalysis,Edl} + size cap) is the
|
|
1032
|
+
// load-bearing mitigation for the caller-derived write path.
|
|
1033
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
1034
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1035
|
+
const params = new URL(req.url).searchParams;
|
|
1036
|
+
const asset = params.get('asset');
|
|
1037
|
+
const slug = params.get('slug');
|
|
1038
|
+
if (!asset && !slug) return new Response('asset or slug required', { status: 400 });
|
|
1039
|
+
const isEdl = !asset && !!slug;
|
|
1040
|
+
|
|
1041
|
+
if (req.method === 'GET') {
|
|
1042
|
+
const data = isEdl
|
|
1043
|
+
? await footageStore.getEdl(slug)
|
|
1044
|
+
: await footageStore.getAnalysis(asset);
|
|
1045
|
+
return Response.json(data ?? {}, { headers: { 'Cache-Control': 'no-store' } });
|
|
1046
|
+
}
|
|
1047
|
+
if (req.method === 'PUT' || req.method === 'POST') {
|
|
1048
|
+
const body = await readJson<unknown>(req, FOOTAGE_MAX_BYTES + 1024);
|
|
1049
|
+
if (body == null) return new Response('body required', { status: 400 });
|
|
1050
|
+
const result = isEdl
|
|
1051
|
+
? await footageStore.saveEdl(slug, body)
|
|
1052
|
+
: await footageStore.saveAnalysis(asset, body);
|
|
1053
|
+
if (!result.ok) {
|
|
1054
|
+
return Response.json(
|
|
1055
|
+
{ ok: false, error: result.error },
|
|
1056
|
+
{ status: result.status, headers: { 'Cache-Control': 'no-store' } }
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
return Response.json({ ok: true, ...result }, { headers: { 'Cache-Control': 'no-store' } });
|
|
1060
|
+
}
|
|
1061
|
+
return new Response('Method not allowed', { status: 405 });
|
|
1062
|
+
},
|
|
1063
|
+
|
|
933
1064
|
'/_api/canvas-source': async (req: Request) => {
|
|
934
1065
|
// DDR-148 — read-only raw .tsx source for the Timeline panel's sequence/
|
|
935
1066
|
// keyframe parser. GET ?file=<canvas rel or slug> → { source }.
|
|
@@ -1436,10 +1567,13 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
1436
1567
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1437
1568
|
if (!isLoopbackHost(req.headers.get('host')))
|
|
1438
1569
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1439
|
-
const body = await readJson<{
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1570
|
+
const body = await readJson<{
|
|
1571
|
+
canvas?: unknown;
|
|
1572
|
+
id?: unknown;
|
|
1573
|
+
text?: unknown;
|
|
1574
|
+
occurrence?: unknown;
|
|
1575
|
+
before?: unknown;
|
|
1576
|
+
}>(req, 16 * 1024);
|
|
1443
1577
|
if (!body) return new Response('body required', { status: 400 });
|
|
1444
1578
|
const result = await api.editText(body);
|
|
1445
1579
|
if (!result.ok) {
|
|
@@ -1813,9 +1947,11 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
1813
1947
|
},
|
|
1814
1948
|
|
|
1815
1949
|
'/_api/insert-element': async (req: Request) => {
|
|
1816
|
-
// Stage I — insert a synthesized div/text/image relative to `refId
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1950
|
+
// Stage I — insert a synthesized div/text/image relative to `refId`, OR —
|
|
1951
|
+
// empty-artboard fallback — as a direct child of `artboardId` (exactly one
|
|
1952
|
+
// of the two). POST { canvas, refId|artboardId, position, kind, src?,
|
|
1953
|
+
// refIndex? } → api.insertElementOp. Returns the new element's
|
|
1954
|
+
// post-transpile id so the shell can select it.
|
|
1819
1955
|
// Whole-file undo seq. MAIN-ORIGIN ONLY (absent from both allowlists);
|
|
1820
1956
|
// sameOriginWrite + loopback-Host gated. An `image` src is contained to
|
|
1821
1957
|
// assets/ in the engine (no remote hotlink / ../ / scheme).
|
|
@@ -1827,6 +1963,7 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
1827
1963
|
const body = await readJson<{
|
|
1828
1964
|
canvas?: unknown;
|
|
1829
1965
|
refId?: unknown;
|
|
1966
|
+
artboardId?: unknown;
|
|
1830
1967
|
position?: unknown;
|
|
1831
1968
|
kind?: unknown;
|
|
1832
1969
|
src?: unknown;
|
|
@@ -1922,6 +2059,24 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
1922
2059
|
);
|
|
1923
2060
|
},
|
|
1924
2061
|
|
|
2062
|
+
'/_api/stickers': async (req: Request) => {
|
|
2063
|
+
// Phase 4 (feature-whiteboard-annotation-improvements) — the bundled
|
|
2064
|
+
// sticker catalogue for the StickerPicker. GET → { packs:[{slug, name,
|
|
2065
|
+
// author, attributionUrl, license, stickers:[{file,keywords,url}]}] }.
|
|
2066
|
+
// MAIN-ORIGIN ONLY: the picker is a shell dialog, same posture as
|
|
2067
|
+
// /_api/assets above — absent from CANVAS_SAFE_API + startCanvasServer
|
|
2068
|
+
// routes (dual-allowlist, DDR-054). Loopback-Host gated (DNS-rebinding);
|
|
2069
|
+
// read-only, so no sameOriginWrite (GET).
|
|
2070
|
+
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2071
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2072
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2073
|
+
const r = await api.listStickers();
|
|
2074
|
+
return Response.json(
|
|
2075
|
+
{ ok: true, packs: r.packs },
|
|
2076
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
2077
|
+
);
|
|
2078
|
+
},
|
|
2079
|
+
|
|
1925
2080
|
'/_api/edit-scope': async (req: Request) => {
|
|
1926
2081
|
// Stage H (feature-element-editing-robustness) — the INV-3 predictability
|
|
1927
2082
|
// verdict. GET ?canvas&id&rendered → { ok, scope:'local'|'shared',
|
|
@@ -2041,17 +2196,28 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
2041
2196
|
|
|
2042
2197
|
'/_api/export-history': async (req: Request) => {
|
|
2043
2198
|
// Phase 6.5 T10 — read-only recent-exports feed for the dialog's
|
|
2044
|
-
// Recent tab. Writes happen as a side-effect of
|
|
2199
|
+
// Recent tab. Writes happen as a side-effect of job completion
|
|
2200
|
+
// (exporters/jobs.ts persistAndEvict) rather than this handler.
|
|
2045
2201
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2046
|
-
const history =
|
|
2202
|
+
const history = exportJobs.loadHistory();
|
|
2047
2203
|
return Response.json({ history }, { headers: { 'Cache-Control': 'no-store' } });
|
|
2048
2204
|
},
|
|
2049
2205
|
|
|
2050
2206
|
'/_api/export': async (req: Request) => {
|
|
2051
|
-
//
|
|
2052
|
-
//
|
|
2053
|
-
//
|
|
2207
|
+
// feature-background-export-notification-center — thin wrapper over the
|
|
2208
|
+
// job queue: enqueue() then await the SAME job's result. Byte-for-byte
|
|
2209
|
+
// identical external contract to the old synchronous handler, so
|
|
2210
|
+
// `/design:export` (CLI) and any other blocking caller need zero changes.
|
|
2211
|
+
// sameOriginWrite CSRF + loopback-Host (DNS-rebinding) gated, matching
|
|
2212
|
+
// the write-route convention elsewhere in this file (e.g.
|
|
2213
|
+
// /_api/delete-element) — closed as part of the /flow:done security
|
|
2214
|
+
// fan-out (defender finding: this POST now has a disk-write + queue-
|
|
2215
|
+
// growth consequence a bare cross-origin form-POST could trigger).
|
|
2054
2216
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2217
|
+
if (!sameOriginWrite(req))
|
|
2218
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2219
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2220
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2055
2221
|
const body = await readJson<{
|
|
2056
2222
|
format?: unknown;
|
|
2057
2223
|
scope?: unknown;
|
|
@@ -2060,63 +2226,108 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
2060
2226
|
if (!body) return new Response('body required', { status: 400 });
|
|
2061
2227
|
if (!isFormat(body.format)) return new Response('unknown or missing format', { status: 400 });
|
|
2062
2228
|
if (!isScope(body.scope)) return new Response('unknown or missing scope', { status: 400 });
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
// wider ActiveState interface.
|
|
2066
|
-
const activeJson = inspect.state as unknown as ActiveJsonShape;
|
|
2229
|
+
const format = body.format;
|
|
2230
|
+
const scope = body.scope;
|
|
2067
2231
|
try {
|
|
2068
|
-
const result =
|
|
2069
|
-
format: body.
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
resolve: { activeJson, designRoot: ctx.paths.designRoot, repoRoot: ctx.paths.repoRoot },
|
|
2073
|
-
ctx: {
|
|
2074
|
-
designRoot: ctx.paths.designRoot,
|
|
2075
|
-
repoRoot: ctx.paths.repoRoot,
|
|
2076
|
-
// Adapters reach back into the server via this origin only when
|
|
2077
|
-
// they need Playwright rendering (PNG / PDF / SVG / HTML). The
|
|
2078
|
-
// host that received this request is, by definition, the one
|
|
2079
|
-
// serving the canvas.
|
|
2080
|
-
serverOrigin: new URL(req.url).origin,
|
|
2081
|
-
// Mirror `client/app.jsx:85` — the per-DS tokensCssRel wins over
|
|
2082
|
-
// the legacy top-level default (which still points at the pre-
|
|
2083
|
-
// multi-DS layout `system/colors_and_type.css`). Without the
|
|
2084
|
-
// per-DS path, the standalone `_canvas-shell.html` 404s on the
|
|
2085
|
-
// tokens link and the rendered DOM uses `var(--bg-0)` unresolved
|
|
2086
|
-
// → screenshots come out blank. See canvasShellUrl().
|
|
2087
|
-
tokensCssRel: ctx.cfg.designSystems?.[0]?.tokensCssRel ?? ctx.cfg.tokensCssRel,
|
|
2088
|
-
},
|
|
2089
|
-
});
|
|
2090
|
-
// Fire-and-forget history append — failure here doesn't block the
|
|
2091
|
-
// download. Synchronous await keeps the order: history reflects the
|
|
2092
|
-
// export the moment the client sees a 200.
|
|
2093
|
-
try {
|
|
2094
|
-
await api.appendExportHistory({
|
|
2095
|
-
format: body.format,
|
|
2096
|
-
scope: body.scope,
|
|
2097
|
-
options: body.options ?? {},
|
|
2098
|
-
filename: result.filename,
|
|
2099
|
-
at: new Date().toISOString(),
|
|
2100
|
-
});
|
|
2101
|
-
} catch {
|
|
2102
|
-
/* ignore — history is best-effort */
|
|
2103
|
-
}
|
|
2232
|
+
const { result } = exportJobs.enqueue(
|
|
2233
|
+
buildExportArgs(req, { format, scope, options: body.options })
|
|
2234
|
+
);
|
|
2235
|
+
const finished = await result;
|
|
2104
2236
|
// Bun.serve accepts Uint8Array directly; the cast satisfies the
|
|
2105
2237
|
// SharedArrayBuffer-strict BodyInit narrowing on @types/bun.
|
|
2106
|
-
return new Response(
|
|
2238
|
+
return new Response(finished.body as unknown as BodyInit, {
|
|
2107
2239
|
status: 200,
|
|
2108
2240
|
headers: {
|
|
2109
|
-
'Content-Type':
|
|
2110
|
-
'Content-Disposition': `attachment; filename="${
|
|
2241
|
+
'Content-Type': finished.contentType,
|
|
2242
|
+
'Content-Disposition': `attachment; filename="${finished.filename}"`,
|
|
2111
2243
|
'Cache-Control': 'no-store',
|
|
2112
2244
|
},
|
|
2113
2245
|
});
|
|
2114
2246
|
} catch (err) {
|
|
2247
|
+
if (err instanceof ExportQueueFullError) {
|
|
2248
|
+
return new Response(err.message, { status: 429 });
|
|
2249
|
+
}
|
|
2115
2250
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2116
2251
|
return new Response(`export failed: ${msg}`, { status: 500 });
|
|
2117
2252
|
}
|
|
2118
2253
|
},
|
|
2119
2254
|
|
|
2255
|
+
'/_api/export-jobs': async (req: Request) => {
|
|
2256
|
+
// feature-background-export-notification-center — the non-blocking
|
|
2257
|
+
// sibling of /_api/export: same body, returns 202 { jobId } immediately
|
|
2258
|
+
// without awaiting the render. MAIN-ORIGIN ONLY (absent from
|
|
2259
|
+
// CANVAS_SAFE_API + startCanvasServer routes, same trust boundary as
|
|
2260
|
+
// /_api/export today — DDR-060). loopback-Host gated on every method;
|
|
2261
|
+
// the mutating POST is additionally sameOriginWrite CSRF gated (same
|
|
2262
|
+
// /flow:done security fan-out fix as /_api/export above).
|
|
2263
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2264
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2265
|
+
if (req.method === 'GET') {
|
|
2266
|
+
return Response.json(
|
|
2267
|
+
{ jobs: exportJobs.list() },
|
|
2268
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2272
|
+
if (!sameOriginWrite(req))
|
|
2273
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2274
|
+
const body = await readJson<{
|
|
2275
|
+
format?: unknown;
|
|
2276
|
+
scope?: unknown;
|
|
2277
|
+
options?: Record<string, unknown>;
|
|
2278
|
+
}>(req, 64 * 1024);
|
|
2279
|
+
if (!body) return new Response('body required', { status: 400 });
|
|
2280
|
+
if (!isFormat(body.format)) return new Response('unknown or missing format', { status: 400 });
|
|
2281
|
+
if (!isScope(body.scope)) return new Response('unknown or missing scope', { status: 400 });
|
|
2282
|
+
try {
|
|
2283
|
+
const { id, result } = exportJobs.enqueue(
|
|
2284
|
+
buildExportArgs(req, { format: body.format, scope: body.scope, options: body.options })
|
|
2285
|
+
);
|
|
2286
|
+
// This route deliberately doesn't await the render — the job's own
|
|
2287
|
+
// status (surfaced via GET /_api/export-jobs + the export:job WS
|
|
2288
|
+
// push) is the completion signal. A render failure still needs a
|
|
2289
|
+
// handler here or it's an unhandled rejection on every failed/timed-
|
|
2290
|
+
// out background job (security fan-out finding, /flow:done) — the
|
|
2291
|
+
// failure is already recorded on the job record, so this is a no-op.
|
|
2292
|
+
result.catch(() => {});
|
|
2293
|
+
return Response.json(
|
|
2294
|
+
{ jobId: id },
|
|
2295
|
+
{ status: 202, headers: { 'Cache-Control': 'no-store' } }
|
|
2296
|
+
);
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
if (err instanceof ExportQueueFullError) {
|
|
2299
|
+
return new Response(err.message, { status: 429 });
|
|
2300
|
+
}
|
|
2301
|
+
throw err;
|
|
2302
|
+
}
|
|
2303
|
+
},
|
|
2304
|
+
|
|
2305
|
+
'/_api/export-jobs/download': async (req: Request) => {
|
|
2306
|
+
// MAIN-ORIGIN ONLY, same boundary as /_api/export-jobs above.
|
|
2307
|
+
// loopback-Host gated (read-only — no sameOriginWrite needed — but an
|
|
2308
|
+
// unguessable job-scoped UUID plus this guard closes the DNS-rebinding
|
|
2309
|
+
// angle the /flow:done security fan-out checked for).
|
|
2310
|
+
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2311
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2312
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2313
|
+
const id = new URL(req.url).searchParams.get('id');
|
|
2314
|
+
if (!id) return new Response('id query param required', { status: 400 });
|
|
2315
|
+
const dl = await exportJobs.getBytes(id);
|
|
2316
|
+
if (!dl.ok) {
|
|
2317
|
+
return new Response(dl.reason === 'not-done' ? 'job not finished' : 'Not found', {
|
|
2318
|
+
status: dl.reason === 'not-done' ? 409 : 404,
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
return new Response(dl.bytes as unknown as BodyInit, {
|
|
2322
|
+
status: 200,
|
|
2323
|
+
headers: {
|
|
2324
|
+
'Content-Type': dl.contentType,
|
|
2325
|
+
'Content-Disposition': `attachment; filename="${dl.filename}"`,
|
|
2326
|
+
'Cache-Control': 'no-store',
|
|
2327
|
+
},
|
|
2328
|
+
});
|
|
2329
|
+
},
|
|
2330
|
+
|
|
2120
2331
|
'/_canvas-state': async (req: Request) => {
|
|
2121
2332
|
const url = new URL(req.url);
|
|
2122
2333
|
if (req.method === 'GET') {
|
|
@@ -2272,6 +2483,48 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
2272
2483
|
}
|
|
2273
2484
|
}
|
|
2274
2485
|
|
|
2486
|
+
// Phase 4 (feature-whiteboard-annotation-improvements) — bundled sticker
|
|
2487
|
+
// PNGs, served from MAUDE's OWN STICKERS_DIR (paths.ts, DDR-045), never
|
|
2488
|
+
// the served project's designRoot (unlike /assets/ above). MAIN-ORIGIN
|
|
2489
|
+
// ONLY: absent from CANVAS_SAFE_API + startCanvasServer's own routes
|
|
2490
|
+
// object (server.ts) — a canvas-origin request 404s via isCanvasSafeRoute
|
|
2491
|
+
// before ever reaching here (dual-allowlist, DDR-054/DDR-088).
|
|
2492
|
+
if (pathname.startsWith('/_stickers/')) {
|
|
2493
|
+
const rest = pathname.slice('/_stickers/'.length);
|
|
2494
|
+
let decoded = '';
|
|
2495
|
+
try {
|
|
2496
|
+
decoded = decodeURIComponent(rest);
|
|
2497
|
+
} catch {
|
|
2498
|
+
return new Response('Bad request', { status: 400 });
|
|
2499
|
+
}
|
|
2500
|
+
const parts = decoded.split('/');
|
|
2501
|
+
const [pack, name] = parts;
|
|
2502
|
+
if (
|
|
2503
|
+
parts.length === 2 &&
|
|
2504
|
+
pack &&
|
|
2505
|
+
name &&
|
|
2506
|
+
/^[a-z0-9-]+$/.test(pack) &&
|
|
2507
|
+
!name.includes('..') &&
|
|
2508
|
+
!name.includes('/') &&
|
|
2509
|
+
!name.includes('\\') &&
|
|
2510
|
+
ext(name) === '.png'
|
|
2511
|
+
) {
|
|
2512
|
+
const abs = join(STICKERS_DIR, pack, name);
|
|
2513
|
+
const f = Bun.file(abs);
|
|
2514
|
+
if (await f.exists()) {
|
|
2515
|
+
return new Response(f, {
|
|
2516
|
+
headers: {
|
|
2517
|
+
'Content-Type': 'image/png',
|
|
2518
|
+
'Cache-Control': 'public, max-age=31536000, immutable', // bundled with this maude version, never changes at this URL
|
|
2519
|
+
'X-Content-Type-Options': 'nosniff',
|
|
2520
|
+
},
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
return new Response('Not found', { status: 404 });
|
|
2524
|
+
}
|
|
2525
|
+
return new Response('Not found', { status: 404 });
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2275
2528
|
// Fall-through: serve user repo files (designRoot + everything under repoRoot).
|
|
2276
2529
|
const fp = safePathUnderRoot(req.url, ctx.paths.repoRoot);
|
|
2277
2530
|
if (!fp) return new Response('Forbidden', { status: 403 });
|
|
@@ -2363,7 +2616,8 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
2363
2616
|
'/_api/git-user', // presence display name
|
|
2364
2617
|
'/_api/canvas-meta', // layout/viewport sidecar (GET + PATCH)
|
|
2365
2618
|
'/_api/annotations', // annotation SVG (GET + PUT) — drives the collab bridge
|
|
2366
|
-
'/_api/asset', // Phase 23 — capped binary image upload (sniff+
|
|
2619
|
+
'/_api/asset', // Phase 23 — capped binary image upload (sniff+category cap+sha8 name+no-SVG)
|
|
2620
|
+
'/_api/photo-edit', // feature-photo-editor — PhotoEdit sidecar GET/PUT (cap-stack gated). MIRROR in server.ts routes.
|
|
2367
2621
|
'/_api/git-committers', // @mention autocomplete
|
|
2368
2622
|
'/_api/ai', // AI-activity banner
|
|
2369
2623
|
'/_comments', // per-file comment list (renders pins)
|
|
@@ -330,6 +330,16 @@ export function isEditableTarget(t: EventTarget | null): boolean {
|
|
|
330
330
|
const tag = el.tagName;
|
|
331
331
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
332
332
|
if (el.isContentEditable) return true;
|
|
333
|
+
// Dogfood fix — `.isContentEditable` is a computed/inherited property whose
|
|
334
|
+
// handling of the `contenteditable="plaintext-only"` token (the value the
|
|
335
|
+
// element-text-edit system uses, canvas-shell.tsx) has had cross-engine/
|
|
336
|
+
// version inconsistencies. Check the raw attribute too so a tool-letter
|
|
337
|
+
// shortcut (R, T, N, …) can never fire while that editor has focus,
|
|
338
|
+
// regardless of whether isContentEditable correctly reflects it in a given
|
|
339
|
+
// runtime — this was the reported bug (typing "R" while editing in-canvas
|
|
340
|
+
// text switched to the Rectangle tool).
|
|
341
|
+
const raw = el.getAttribute?.('contenteditable');
|
|
342
|
+
if (raw === 'true' || raw === 'plaintext-only' || raw === '') return true;
|
|
333
343
|
return false;
|
|
334
344
|
}
|
|
335
345
|
|
package/apps/studio/inspect.ts
CHANGED
|
@@ -47,6 +47,19 @@ export interface SelectedElement {
|
|
|
47
47
|
* degrade to canvas-wide — never trust the positional id when stale.
|
|
48
48
|
*/
|
|
49
49
|
stale?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* feature-photo-editor (Task 14) — which photo-editing context this
|
|
52
|
+
* selection is (an artboard `<img>` vs. an annotation `ImageStroke`), and
|
|
53
|
+
* the resolved `assets/<sha8>.<ext>` source. Client-derived (dom-selection.ts
|
|
54
|
+
* re-reads the live DOM, including a `data-photo-asset` tag stamped after an
|
|
55
|
+
* edit bakes) — round-tripped here (not dropped like the OTHER client-only
|
|
56
|
+
* fields such as `authored`/`computed`/`attrs`) because, unlike those, there
|
|
57
|
+
* is no cheap way to re-derive it from a plain server-side restore: losing it
|
|
58
|
+
* silently drops the Inspector's Photo tab on every canvas switch / reconnect
|
|
59
|
+
* until a fresh click re-selects the element.
|
|
60
|
+
*/
|
|
61
|
+
photoKind?: 'artboard-img' | 'annotation-image';
|
|
62
|
+
photoAsset?: string;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
/**
|
|
@@ -240,6 +253,19 @@ export function createInspect(
|
|
|
240
253
|
v,
|
|
241
254
|
canvas_mtime: mtimeFor(file),
|
|
242
255
|
...(id ? { id, canvas: deriveCanvasSlug(file) } : {}),
|
|
256
|
+
...(() => {
|
|
257
|
+
if (sel.photoKind !== 'artboard-img' && sel.photoKind !== 'annotation-image') return {};
|
|
258
|
+
// `photoAsset` traces back to client-derived DOM state (a
|
|
259
|
+
// `data-photo-asset` attribute inside the untrusted canvas iframe,
|
|
260
|
+
// DDR-054) — unlike the sibling `text` field it had no shape/length
|
|
261
|
+
// constraint before persisting to `_active.json` and broadcasting to
|
|
262
|
+
// every connected WS peer (security review finding). It's always a
|
|
263
|
+
// fixed-shape `assets/<sha8>.<ext>` reference, so an unshaped value
|
|
264
|
+
// is dropped outright rather than merely truncated.
|
|
265
|
+
const asset = String(sel.photoAsset || '');
|
|
266
|
+
if (!/^assets\/[0-9a-f]{8}\.[a-z0-9]+$/i.test(asset)) return {};
|
|
267
|
+
return { photoKind: sel.photoKind, photoAsset: asset };
|
|
268
|
+
})(),
|
|
243
269
|
};
|
|
244
270
|
}
|
|
245
271
|
|
|
@@ -366,6 +392,26 @@ const INSPECTOR_SCRIPT = `
|
|
|
366
392
|
'@keyframes dc-activity-glow { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.85; } }',
|
|
367
393
|
'@keyframes dc-activity-wave { from { transform: translateY(-100%); } to { transform: translateY(100%); } }',
|
|
368
394
|
'@media (prefers-reduced-motion: reduce) { html .dc-activity-rim { transition: none; } html .dc-activity-rim::after { animation: none; opacity: 0.55; } html .dc-activity-scan { display: none; } }',
|
|
395
|
+
/* feature-photo-editor — background-removal busy reveal. A data-photo-busy
|
|
396
|
+
attribute toggle on the real img/image element (set by canvas-lib.tsx's
|
|
397
|
+
PhotoPreviewBridge on a photo-busy postMessage from the shell), styled
|
|
398
|
+
here at the same single injection point as the .dc-activity-* agent-edit
|
|
399
|
+
chrome above — same "something is actively happening" rim + pulse
|
|
400
|
+
language. Deliberately NOT a floating tracked overlay: that's the exact
|
|
401
|
+
architecture DDR-161's addendum removed from the live-edit preview
|
|
402
|
+
(z-index/resize/hit-testing bugs) — an attribute on the element itself
|
|
403
|
+
moves for free with pan/zoom/resize, no separate rect sync needed.
|
|
404
|
+
Deliberately opacity-only (not a moving mask-position sweep, tried
|
|
405
|
+
first): the ML pass runs single-threaded WASM on the main thread
|
|
406
|
+
(confirmed live — env.wasm.numThreads falls back without
|
|
407
|
+
crossOriginIsolated), which blocks per-frame style/paint work for the
|
|
408
|
+
ENTIRE inference; a mask-position animation froze mid-sweep for that
|
|
409
|
+
whole window. opacity is one of the few properties browsers composite
|
|
410
|
+
off the main thread unconditionally, so the pulse keeps animating
|
|
411
|
+
exactly when the page is busiest — the one moment it needs to. */
|
|
412
|
+
'html [data-photo-busy] { outline: 2px solid var(--mdcc-activity, hsl(210 90% 58%)); outline-offset: 2px; animation: dc-photo-busy-pulse 1100ms ease-in-out infinite; will-change: opacity; }',
|
|
413
|
+
'@keyframes dc-photo-busy-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }',
|
|
414
|
+
'@media (prefers-reduced-motion: reduce) { html [data-photo-busy] { animation: none; opacity: 0.75; } }',
|
|
369
415
|
/* Phase 13.1 / DDR-077 — "holding last good render" toast, shown when an
|
|
370
416
|
agent edit produced a broken intermediate (build/render error) and the
|
|
371
417
|
canvas is held instead of flashing white. Amber = warn, distinct from the
|
|
@@ -482,7 +528,13 @@ const INSPECTOR_SCRIPT = `
|
|
|
482
528
|
return;
|
|
483
529
|
}
|
|
484
530
|
if (e.shiftKey) {
|
|
485
|
-
|
|
531
|
+
// ⌘⇧T (timeline) + ⌘⇧G (changes) were missing here, so with focus inside
|
|
532
|
+
// the canvas iframe they never reached the shell — opening the Timeline
|
|
533
|
+
// (and its Space/arrow transport, which needs the dock focused) silently
|
|
534
|
+
// stopped working after a canvas interaction moved focus into the iframe.
|
|
535
|
+
// ⌘⇧T is also the browser "reopen closed tab" chord, so the preventDefault
|
|
536
|
+
// below is doubly load-bearing.
|
|
537
|
+
var id = k === 'i' ? 'inspector' : k === 'm' ? 'comments' : k === 'e' ? 'export' : k === 'h' ? 'handoff' : k === 't' ? 'timeline' : k === 'g' ? 'changes' : null;
|
|
486
538
|
if (id) {
|
|
487
539
|
e.preventDefault();
|
|
488
540
|
try { window.parent.postMessage({ dgn: 'shell-shortcut', id: id }, '*'); } catch (err) {}
|
package/apps/studio/paths.ts
CHANGED
|
@@ -48,6 +48,16 @@ export const CLIENT_DIR: string = join(DEV_SERVER_ROOT, 'client');
|
|
|
48
48
|
/** `<DEV_SERVER_ROOT>/dist/runtime/` — pre-built /_canvas-runtime/*.js bundles. */
|
|
49
49
|
export const RUNTIME_BUNDLES_DIR: string = join(DIST_DIR, 'runtime');
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* `<DEV_SERVER_ROOT>/stickers/` — bundled whiteboard sticker packs
|
|
53
|
+
* (feature-whiteboard-annotation-improvements, Phase 4). Ships with the
|
|
54
|
+
* `@1agh/maude` tarball (a subdir of `apps/studio`, already covered by the
|
|
55
|
+
* existing `files` entry — no separate packaging step). Resolved from
|
|
56
|
+
* DEV_SERVER_ROOT per DDR-045 — this is MAUDE's own bundled asset store, never
|
|
57
|
+
* the served project's.
|
|
58
|
+
*/
|
|
59
|
+
export const STICKERS_DIR: string = join(DEV_SERVER_ROOT, 'stickers');
|
|
60
|
+
|
|
51
61
|
/**
|
|
52
62
|
* Absolute path to a bundled plugin's loadable tree (`commands/`, `agents/`,
|
|
53
63
|
* `skills/`, `hooks/`, `.claude-plugin/plugin.json`), or `null` when this layout
|