@1agh/maude 0.42.0 → 0.44.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/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- 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 +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- 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 +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- 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/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -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/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -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/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- 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 +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -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 +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -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/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- 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/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -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/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -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/tour-overlay.test.tsx +16 -0
- 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 +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- 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 +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
package/apps/studio/http.ts
CHANGED
|
@@ -8,26 +8,75 @@ import { createHash } from 'node:crypto';
|
|
|
8
8
|
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
9
9
|
import { dirname, join, posix, relative, resolve, sep } from 'node:path';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
cancelSignin,
|
|
13
|
+
getClaudeAuthStatus,
|
|
14
|
+
getInstallState,
|
|
15
|
+
isSigninInFlight,
|
|
16
|
+
startInstall,
|
|
17
|
+
startSignin,
|
|
18
|
+
} from './acp/login-state.ts';
|
|
19
|
+
import { probeAcpAvailabilityAuthed } from './acp/probe.ts';
|
|
12
20
|
import { deleteChat, listChats, readChatMessages } from './acp/transcript.ts';
|
|
13
|
-
import { type Api, ASSET_MAX_VIDEO_BYTES } from './api.ts';
|
|
21
|
+
import { type Api, ASSET_MAX_BYTES, ASSET_MAX_VIDEO_BYTES } from './api.ts';
|
|
22
|
+
import { ImportAssetError, importSvg, SVG_MAX_BYTES } from './bin/_import-asset.mjs';
|
|
23
|
+
import { ImportBrandError, importBrand } from './bin/_import-brand.mjs';
|
|
14
24
|
import { buildCanvasModule } from './canvas-build.ts';
|
|
15
25
|
import { canvasLibPath } from './canvas-lib-resolver.ts';
|
|
16
26
|
import { TranspileError } from './canvas-pipeline.ts';
|
|
17
27
|
import type { AiActivity } from './collab/ai-activity.ts';
|
|
18
28
|
import type { Context } from './context.ts';
|
|
29
|
+
import { reloadConfig } from './context.ts';
|
|
30
|
+
import { probeSetupReadiness } from './design-setup-readiness.ts';
|
|
19
31
|
import { type Format, isFormat, isScope, type Scope } from './exporters/index.ts';
|
|
20
32
|
import { type ExportJobQueue, ExportQueueFullError } from './exporters/jobs.ts';
|
|
21
33
|
import type { ActiveJsonShape } from './exporters/scope.ts';
|
|
34
|
+
import { generatedClipAnalysis } from './footage/schema.ts';
|
|
35
|
+
import { createFootageStore, FOOTAGE_MAX_BYTES } from './footage-store.ts';
|
|
36
|
+
import {
|
|
37
|
+
type AudioMatch,
|
|
38
|
+
type Candidate,
|
|
39
|
+
rankMatches,
|
|
40
|
+
sanitizeReuseText,
|
|
41
|
+
} from './generation/audio-library.ts';
|
|
42
|
+
import { localizeGenAsset } from './generation/download.ts';
|
|
43
|
+
import { type GenerationJobQueue, GenerationQueueFullError } from './generation/jobs.ts';
|
|
44
|
+
import {
|
|
45
|
+
configuredProviders,
|
|
46
|
+
deleteProviderKey,
|
|
47
|
+
getProviderKey,
|
|
48
|
+
isConfigured,
|
|
49
|
+
setProviderKey,
|
|
50
|
+
} from './generation/keys.ts';
|
|
51
|
+
import {
|
|
52
|
+
isTranscriptionProvider,
|
|
53
|
+
readTranscriptionProvider,
|
|
54
|
+
writeTranscriptionProvider,
|
|
55
|
+
} from './generation/prefs.ts';
|
|
56
|
+
import {
|
|
57
|
+
createAdapter,
|
|
58
|
+
getProviderDescriptor,
|
|
59
|
+
hasProvider,
|
|
60
|
+
listProviders,
|
|
61
|
+
} from './generation/registry.ts';
|
|
62
|
+
import { validateGenRequest } from './generation/types.ts';
|
|
63
|
+
import {
|
|
64
|
+
downloadWhisperModel,
|
|
65
|
+
getWhisperModel,
|
|
66
|
+
listWhisperModels,
|
|
67
|
+
removeWhisperModel,
|
|
68
|
+
} from './generation/whisper-models.ts';
|
|
22
69
|
import { createGitEndpoints } from './git/endpoints.ts';
|
|
23
70
|
import { gitShowFile } from './git/service.ts';
|
|
24
71
|
import { createGitHubEndpoints } from './github/endpoints.ts';
|
|
25
72
|
import type { Inspect } from './inspect.ts';
|
|
26
73
|
import { canvasSlug, writeLocator } from './locator.ts';
|
|
27
|
-
import { DEV_SERVER_ROOT } from './paths.ts';
|
|
74
|
+
import { DEV_SERVER_ROOT, MEDIA_DIR, STICKERS_DIR } from './paths.ts';
|
|
75
|
+
import { createPhotoStore, PHOTO_EDIT_MAX_BYTES } from './photo-store.ts';
|
|
28
76
|
import { probeReadiness } from './readiness.ts';
|
|
29
77
|
import { getRuntimeBundle, packageForSlug } from './runtime-bundle.ts';
|
|
30
78
|
import { linkHub } from './sync/hub-link.ts';
|
|
79
|
+
import { readUiPrefs, type UiPrefs, writeUiPrefs } from './ui-prefs.ts';
|
|
31
80
|
import { loadWhatsNew } from './whats-new.ts';
|
|
32
81
|
import { isLoopbackHost } from './ws.ts';
|
|
33
82
|
|
|
@@ -104,6 +153,17 @@ function ext(p: string): string {
|
|
|
104
153
|
* external page can no longer reframe the canvas. When `mainOrigin` is unknown
|
|
105
154
|
* (tests / pre-boot) the directive is OMITTED rather than set to `'self'` —
|
|
106
155
|
* `'self'` alone would forbid the legit cross-origin embed and blank the canvas.
|
|
156
|
+
*
|
|
157
|
+
* `connect-src` narrow exception (fix-photo-editor-followup-debt, Task 7):
|
|
158
|
+
* `@imgly/background-removal`'s model weights (~11-44 MB) fetch from IMG.LY's
|
|
159
|
+
* own CDN, `https://staticimgly.com`, on first client-side use — there is no
|
|
160
|
+
* self-hosting mechanism today (a separate, larger follow-up; see DDR-054's
|
|
161
|
+
* dated addendum for this decision, and `.ai/state/STATE.md` for tracking).
|
|
162
|
+
* The inference itself stays 100% client-side (pixels never leave the
|
|
163
|
+
* browser) — only the weight DOWNLOAD needs this one extra origin. This is
|
|
164
|
+
* the ONLY documented exception to the `connect-src 'self'` invariant above —
|
|
165
|
+
* do not read it as precedent for adding further hosts without the same
|
|
166
|
+
* scrutiny (exact hostname, no wildcard subdomain, a DDR record).
|
|
107
167
|
*/
|
|
108
168
|
export function cspForCanvasShell(html: string, mainOrigin?: string): string {
|
|
109
169
|
const hashes: string[] = [];
|
|
@@ -120,7 +180,7 @@ export function cspForCanvasShell(html: string, mainOrigin?: string): string {
|
|
|
120
180
|
const directives = [
|
|
121
181
|
"default-src 'none'",
|
|
122
182
|
`script-src ${scriptSrc}`,
|
|
123
|
-
"connect-src 'self'",
|
|
183
|
+
"connect-src 'self' https://staticimgly.com",
|
|
124
184
|
"img-src 'self' data: blob:",
|
|
125
185
|
// DDR-148 — a video-comp's <Video>/<Audio> loads media from the canvas
|
|
126
186
|
// origin's own designRoot (assets/). Same inert-bytes trust as img-src
|
|
@@ -200,6 +260,24 @@ export function sameOriginWrite(req: Request): boolean {
|
|
|
200
260
|
}
|
|
201
261
|
}
|
|
202
262
|
|
|
263
|
+
/**
|
|
264
|
+
* CSRF guard for a key-bearing / side-effecting GET (Task 2.5 audio-search —
|
|
265
|
+
* ethical-hacker F1). A cross-site page can issue a `no-cors` GET to
|
|
266
|
+
* `127.0.0.1:<port>` that passes the `isLoopbackHost` DNS-rebind guard AND may
|
|
267
|
+
* omit `Origin` (browsers don't reliably stamp Origin on a simple cross-origin
|
|
268
|
+
* GET), so `sameOriginWrite` alone can't fend it off. Browsers DO always send
|
|
269
|
+
* the Fetch-Metadata `Sec-Fetch-Site` header, and non-browser clients (the CLI,
|
|
270
|
+
* curl, bun:test) never do — so reject any request whose `Sec-Fetch-Site` is
|
|
271
|
+
* present and is not `same-origin`/`none`. A missing header (CLI) is allowed.
|
|
272
|
+
* Prevents a cross-site page from driving the user's key-bearing provider
|
|
273
|
+
* fan-out + disk scan as a confused deputy.
|
|
274
|
+
*/
|
|
275
|
+
export function sameOriginRead(req: Request): boolean {
|
|
276
|
+
const site = req.headers.get('sec-fetch-site');
|
|
277
|
+
if (!site) return true; // non-browser client (CLI / curl) → allow
|
|
278
|
+
return site === 'same-origin' || site === 'none';
|
|
279
|
+
}
|
|
280
|
+
|
|
203
281
|
function safePathUnderRoot(reqUrl: string, repoRoot: string): string | null {
|
|
204
282
|
let pathname: string;
|
|
205
283
|
try {
|
|
@@ -600,8 +678,19 @@ export function createHttp(
|
|
|
600
678
|
api: Api,
|
|
601
679
|
inspect: Inspect,
|
|
602
680
|
ai: AiActivity,
|
|
603
|
-
exportJobs: ExportJobQueue
|
|
681
|
+
exportJobs: ExportJobQueue,
|
|
682
|
+
generateJobs: GenerationJobQueue
|
|
604
683
|
): Http {
|
|
684
|
+
// Task 2.7 (approach A) — in-flight whisper-model download state (one at a
|
|
685
|
+
// time), polled by the Settings "Download model" card via GET
|
|
686
|
+
// /_api/generate/whisper-model. Closure-scoped: one server, one download.
|
|
687
|
+
let whisperDownload: {
|
|
688
|
+
id: string;
|
|
689
|
+
received: number;
|
|
690
|
+
total: number;
|
|
691
|
+
error?: string;
|
|
692
|
+
} | null = null;
|
|
693
|
+
|
|
605
694
|
// Cache invalidation — when canvas-lib changes, every cached canvas bundle
|
|
606
695
|
// is stale because canvas-lib is inlined into each one via the resolver
|
|
607
696
|
// plugin. Drop the whole cache so the next request rebuilds with the fresh
|
|
@@ -721,6 +810,12 @@ export function createHttp(
|
|
|
721
810
|
// `routes` map (the dual-allowlist rule), so the untrusted canvas iframe origin
|
|
722
811
|
// can never reach status/commit/publish/get-latest. http.ts owns the gating;
|
|
723
812
|
// git/endpoints.ts owns the orchestration.
|
|
813
|
+
const photoStore = createPhotoStore(ctx);
|
|
814
|
+
// feature-footage-analysis-director — footage-analysis + EDL sidecars.
|
|
815
|
+
// MAIN-ORIGIN ONLY (privileged): the `/_api/footage` route is intentionally
|
|
816
|
+
// absent from CANVAS_SAFE_API + startCanvasServer's `routes` map, so the
|
|
817
|
+
// untrusted canvas iframe origin can never read/write the director's analysis.
|
|
818
|
+
const footageStore = createFootageStore(ctx);
|
|
724
819
|
const gitApi = createGitEndpoints(ctx);
|
|
725
820
|
// Phase 28 (E3) — `/_api/github/*`. Same dual-allowlist rule: main-origin only,
|
|
726
821
|
// and every route is token-bearing (server-held keychain token via the loopback
|
|
@@ -777,8 +872,19 @@ export function createHttp(
|
|
|
777
872
|
// MAIN-ORIGIN ONLY — absent from CANVAS_SAFE_API + startCanvasServer routes,
|
|
778
873
|
// so the untrusted canvas iframe is 403'd. The native shell reads this to
|
|
779
874
|
// decide between the enabled panel and the not-connected explainer.
|
|
780
|
-
|
|
781
|
-
|
|
875
|
+
// Explicit CSRF + DNS-rebind gate added alongside DDR-166: this route's
|
|
876
|
+
// `claudePath` field can now reveal a Maude-auto-installed binary's exact
|
|
877
|
+
// path, and this file's other ACP-adjacent routes all double-gate —
|
|
878
|
+
// security-review finding that this one had none at all, pre-existing
|
|
879
|
+
// but newly worth closing given what this diff makes it able to reveal.
|
|
880
|
+
'/_api/acp/status': async (req: Request) => {
|
|
881
|
+
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
882
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
883
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
884
|
+
return Response.json(await probeAcpAvailabilityAuthed(), {
|
|
885
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
886
|
+
});
|
|
887
|
+
},
|
|
782
888
|
|
|
783
889
|
// DDR-128 — first-open AI-editing readiness. Read-only probe of the AI-editing
|
|
784
890
|
// dependency chain (claude CLI · maude CLI · maude marketplace + plugins in the
|
|
@@ -796,6 +902,100 @@ export function createHttp(
|
|
|
796
902
|
return Response.json(await probeReadiness(), { headers: { 'Cache-Control': 'no-store' } });
|
|
797
903
|
},
|
|
798
904
|
|
|
905
|
+
// DDR-166 plan, Phase 2 (T6) — design-setup readiness (project ✓ / design
|
|
906
|
+
// system ✓ / first canvas ✓ / brand assets ✓), distinct from the AI-editing
|
|
907
|
+
// dependency probe above. Read-only, cheap (a few existsSync/readdir calls
|
|
908
|
+
// scoped to designRoot) — same double gate as every other privileged
|
|
909
|
+
// read here even though it can't shell out, for consistency with the rest
|
|
910
|
+
// of this file's onboarding-surface routes. The quick-setup checklist
|
|
911
|
+
// (SetupChecklist.jsx) polls this.
|
|
912
|
+
'/_api/setup-readiness': async (req: Request) => {
|
|
913
|
+
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
914
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
915
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
916
|
+
return Response.json(await probeSetupReadiness(ctx), {
|
|
917
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
918
|
+
});
|
|
919
|
+
},
|
|
920
|
+
|
|
921
|
+
// DDR-166 T0c (Addendum 2) — install Claude Code from Maude by running the
|
|
922
|
+
// exact official one-liner once, ephemerally (no persistent Maude-managed
|
|
923
|
+
// cache — that design was rejected on security review; see
|
|
924
|
+
// installClaudeCli()'s own doc comment for the fix). MAIN-ORIGIN ONLY,
|
|
925
|
+
// same CSRF + DNS-rebind double gate as every other privileged route.
|
|
926
|
+
'/_api/claude/install': (req: Request) => {
|
|
927
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
928
|
+
if (!sameOriginWrite(req))
|
|
929
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
930
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
931
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
932
|
+
// Fire-and-forget — the install can run past Bun's default 10s idle
|
|
933
|
+
// timeout on a slow network, so this returns as soon as the spawn is
|
|
934
|
+
// confirmed started; the panel polls /_api/claude/install-status for
|
|
935
|
+
// the result, the same shape /_api/claude/signin-status already uses.
|
|
936
|
+
const result = startInstall();
|
|
937
|
+
return Response.json(result, {
|
|
938
|
+
status: result.ok ? 200 : 409,
|
|
939
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
940
|
+
});
|
|
941
|
+
},
|
|
942
|
+
// GET route with a real subprocess side effect downstream (signin-status
|
|
943
|
+
// spawns `claude auth status`) — sameOriginWrite alone doesn't gate a GET
|
|
944
|
+
// (browsers don't reliably stamp Origin on a simple cross-origin GET,
|
|
945
|
+
// per this file's own sameOriginRead doc comment, written for exactly
|
|
946
|
+
// this class of route after a prior ethical-hacker finding on
|
|
947
|
+
// audio-search). Security-review finding against the first cut of these
|
|
948
|
+
// two routes, which used the wrong guard.
|
|
949
|
+
'/_api/claude/install-status': (req: Request) => {
|
|
950
|
+
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
951
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
952
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
953
|
+
return Response.json(getInstallState(), { headers: { 'Cache-Control': 'no-store' } });
|
|
954
|
+
},
|
|
955
|
+
|
|
956
|
+
// DDR-166 T0d — sign in to Claude from Maude, driving the user's OWN `claude`
|
|
957
|
+
// CLI through its own `auth login`/`auth status` subcommands (never Maude's
|
|
958
|
+
// own OAuth, never a token Maude holds). MAIN-ORIGIN ONLY — absent from
|
|
959
|
+
// CANVAS_SAFE_API + startCanvasServer routes, same CSRF + DNS-rebind double
|
|
960
|
+
// gate as every other privileged POST route (DDR-088). Zero new Tauri
|
|
961
|
+
// commands (Decision 0) — this lives in the same Bun process as bridge.ts/
|
|
962
|
+
// env.ts, so it reuses the literal scrubAgentEnv/resolveClaudePath the chat
|
|
963
|
+
// spawn already uses.
|
|
964
|
+
'/_api/claude/signin': (req: Request) => {
|
|
965
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
966
|
+
if (!sameOriginWrite(req))
|
|
967
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
968
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
969
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
970
|
+
const result = startSignin();
|
|
971
|
+
return Response.json(result, {
|
|
972
|
+
status: result.ok ? 200 : 409,
|
|
973
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
974
|
+
});
|
|
975
|
+
},
|
|
976
|
+
'/_api/claude/signin-cancel': (req: Request) => {
|
|
977
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
978
|
+
if (!sameOriginWrite(req))
|
|
979
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
980
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
981
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
982
|
+
cancelSignin();
|
|
983
|
+
return Response.json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
|
|
984
|
+
},
|
|
985
|
+
// Read-only poll target — the panel calls this (or /_api/preflight, which
|
|
986
|
+
// folds the same status in) on an interval while a sign-in is in flight.
|
|
987
|
+
// Narrowed fields only (never email/orgId/orgName — see login-state.ts).
|
|
988
|
+
'/_api/claude/signin-status': async (req: Request) => {
|
|
989
|
+
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
990
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
991
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
992
|
+
const status = await getClaudeAuthStatus();
|
|
993
|
+
return Response.json(
|
|
994
|
+
{ ...status, inFlight: isSigninInFlight() },
|
|
995
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
996
|
+
);
|
|
997
|
+
},
|
|
998
|
+
|
|
799
999
|
// Phase 31 (DDR-123) — `/design:chat` focus hook. `maude design chat-open`
|
|
800
1000
|
// POSTs here; we emit a bus event the shell turns into "open the Assistant
|
|
801
1001
|
// panel" (app.jsx, native-only). MAIN-ORIGIN ONLY (off the canvas allowlist).
|
|
@@ -836,8 +1036,8 @@ export function createHttp(
|
|
|
836
1036
|
// expands to so Claude can Read it. MAIN-ORIGIN ONLY: sameOriginWrite CSRF gate
|
|
837
1037
|
// on POST + deliberately absent from CANVAS_SAFE_API + startCanvasServer routes,
|
|
838
1038
|
// so the untrusted canvas iframe is 403'd. The disk caps live in
|
|
839
|
-
// api.saveChatAttachment (magic-byte sniff /
|
|
840
|
-
// session write budget).
|
|
1039
|
+
// api.saveChatAttachment (magic-byte sniff / ASSET_MAX_BYTES / content-
|
|
1040
|
+
// addressed name / session write budget).
|
|
841
1041
|
//
|
|
842
1042
|
// GET ?name=<sha8>.<ext> serves the pasted image back to the chat feed
|
|
843
1043
|
// (thumbnail + lightbox). The name is regex-allowlisted in
|
|
@@ -864,9 +1064,12 @@ export function createHttp(
|
|
|
864
1064
|
if (!isLoopbackHost(req.headers.get('host')))
|
|
865
1065
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
866
1066
|
const declared = Number(req.headers.get('content-length') || '0');
|
|
867
|
-
if (Number.isFinite(declared) && declared >
|
|
1067
|
+
if (Number.isFinite(declared) && declared > ASSET_MAX_BYTES) {
|
|
868
1068
|
return Response.json(
|
|
869
|
-
{
|
|
1069
|
+
{
|
|
1070
|
+
ok: false,
|
|
1071
|
+
error: `attachment exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`,
|
|
1072
|
+
},
|
|
870
1073
|
{ status: 413, headers: { 'Cache-Control': 'no-store' } }
|
|
871
1074
|
);
|
|
872
1075
|
}
|
|
@@ -969,6 +1172,87 @@ export function createHttp(
|
|
|
969
1172
|
return new Response('Method not allowed', { status: 405 });
|
|
970
1173
|
},
|
|
971
1174
|
|
|
1175
|
+
'/_api/photo-edit': async (req: Request) => {
|
|
1176
|
+
// feature-photo-editor (Stage C, Task 8) — non-destructive PhotoEdit sidecar.
|
|
1177
|
+
// GET ?asset=<sha8 | assets/<sha8>.<ext>> → stored PhotoEdit or {}
|
|
1178
|
+
// PUT/POST ?asset=<...> body { …PhotoEdit } → validated + persisted
|
|
1179
|
+
// CANVAS-SAFE (listed in BOTH CANVAS_SAFE_API below AND startCanvasServer's
|
|
1180
|
+
// `routes` map — Bun matches `routes` before `fetch`, so a one-list entry
|
|
1181
|
+
// 404s from the canvas iframe, the DDR-088 rollout bug). Reached from the
|
|
1182
|
+
// canvas origin by <PhotoLayer> (GET) and the headless bg-remove harness
|
|
1183
|
+
// (PUT), so NO sameOriginWrite (it would block the legit canvas origin);
|
|
1184
|
+
// the photo-store cap stack (sha8 validate + containment + validatePhotoEdit
|
|
1185
|
+
// + size cap) is the load-bearing mitigation. loopback-Host gates DNS-
|
|
1186
|
+
// rebinding on every method.
|
|
1187
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
1188
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1189
|
+
const asset = new URL(req.url).searchParams.get('asset');
|
|
1190
|
+
if (req.method === 'GET') {
|
|
1191
|
+
const edit = await photoStore.getPhotoEdit(asset);
|
|
1192
|
+
return Response.json(edit ?? {}, { headers: { 'Cache-Control': 'no-store' } });
|
|
1193
|
+
}
|
|
1194
|
+
if (req.method === 'PUT' || req.method === 'POST') {
|
|
1195
|
+
const body = await readJson<unknown>(req, PHOTO_EDIT_MAX_BYTES + 1024);
|
|
1196
|
+
if (body == null) return new Response('body required', { status: 400 });
|
|
1197
|
+
const result = await photoStore.savePhotoEdit(asset, body);
|
|
1198
|
+
if (!result.ok) {
|
|
1199
|
+
return Response.json(
|
|
1200
|
+
{ ok: false, error: result.error },
|
|
1201
|
+
{ status: result.status, headers: { 'Cache-Control': 'no-store' } }
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
return Response.json(
|
|
1205
|
+
{ ok: true, path: result.path, edit: result.edit },
|
|
1206
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
return new Response('Method not allowed', { status: 405 });
|
|
1210
|
+
},
|
|
1211
|
+
|
|
1212
|
+
'/_api/footage': async (req: Request) => {
|
|
1213
|
+
// feature-footage-analysis-director (Task 2) — FootageAnalysis + EDL sidecars.
|
|
1214
|
+
// GET ?asset=<sha8|assets/…> → stored FootageAnalysis or {}
|
|
1215
|
+
// GET ?slug=<cut-slug> → stored Edl or {}
|
|
1216
|
+
// PUT/POST ?asset=<…> body {FootageAnalysis} → validated + persisted
|
|
1217
|
+
// PUT/POST ?slug=<…> body {Edl} → validated + persisted
|
|
1218
|
+
// MAIN-ORIGIN ONLY (privileged — NOT in CANVAS_SAFE_API / startCanvasServer
|
|
1219
|
+
// routes): written by the footage-analyst / footage-director agents over
|
|
1220
|
+
// loopback, never the untrusted canvas iframe. A GET from the canvas origin
|
|
1221
|
+
// 403s at the gate (canvas-origin-gate.test.ts). loopback-Host gates
|
|
1222
|
+
// DNS-rebinding on every method; the footage-store cap stack (sha8/slug
|
|
1223
|
+
// validate + containment + validate{FootageAnalysis,Edl} + size cap) is the
|
|
1224
|
+
// load-bearing mitigation for the caller-derived write path.
|
|
1225
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
1226
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1227
|
+
const params = new URL(req.url).searchParams;
|
|
1228
|
+
const asset = params.get('asset');
|
|
1229
|
+
const slug = params.get('slug');
|
|
1230
|
+
if (!asset && !slug) return new Response('asset or slug required', { status: 400 });
|
|
1231
|
+
const isEdl = !asset && !!slug;
|
|
1232
|
+
|
|
1233
|
+
if (req.method === 'GET') {
|
|
1234
|
+
const data = isEdl
|
|
1235
|
+
? await footageStore.getEdl(slug)
|
|
1236
|
+
: await footageStore.getAnalysis(asset);
|
|
1237
|
+
return Response.json(data ?? {}, { headers: { 'Cache-Control': 'no-store' } });
|
|
1238
|
+
}
|
|
1239
|
+
if (req.method === 'PUT' || req.method === 'POST') {
|
|
1240
|
+
const body = await readJson<unknown>(req, FOOTAGE_MAX_BYTES + 1024);
|
|
1241
|
+
if (body == null) return new Response('body required', { status: 400 });
|
|
1242
|
+
const result = isEdl
|
|
1243
|
+
? await footageStore.saveEdl(slug, body)
|
|
1244
|
+
: await footageStore.saveAnalysis(asset, body);
|
|
1245
|
+
if (!result.ok) {
|
|
1246
|
+
return Response.json(
|
|
1247
|
+
{ ok: false, error: result.error },
|
|
1248
|
+
{ status: result.status, headers: { 'Cache-Control': 'no-store' } }
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
return Response.json({ ok: true, ...result }, { headers: { 'Cache-Control': 'no-store' } });
|
|
1252
|
+
}
|
|
1253
|
+
return new Response('Method not allowed', { status: 405 });
|
|
1254
|
+
},
|
|
1255
|
+
|
|
972
1256
|
'/_api/canvas-source': async (req: Request) => {
|
|
973
1257
|
// DDR-148 — read-only raw .tsx source for the Timeline panel's sequence/
|
|
974
1258
|
// keyframe parser. GET ?file=<canvas rel or slug> → { source }.
|
|
@@ -1475,10 +1759,13 @@ export function createHttp(
|
|
|
1475
1759
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1476
1760
|
if (!isLoopbackHost(req.headers.get('host')))
|
|
1477
1761
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1478
|
-
const body = await readJson<{
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1762
|
+
const body = await readJson<{
|
|
1763
|
+
canvas?: unknown;
|
|
1764
|
+
id?: unknown;
|
|
1765
|
+
text?: unknown;
|
|
1766
|
+
occurrence?: unknown;
|
|
1767
|
+
before?: unknown;
|
|
1768
|
+
}>(req, 16 * 1024);
|
|
1482
1769
|
if (!body) return new Response('body required', { status: 400 });
|
|
1483
1770
|
const result = await api.editText(body);
|
|
1484
1771
|
if (!result.ok) {
|
|
@@ -1852,9 +2139,11 @@ export function createHttp(
|
|
|
1852
2139
|
},
|
|
1853
2140
|
|
|
1854
2141
|
'/_api/insert-element': async (req: Request) => {
|
|
1855
|
-
// Stage I — insert a synthesized div/text/image relative to `refId
|
|
1856
|
-
//
|
|
1857
|
-
//
|
|
2142
|
+
// Stage I — insert a synthesized div/text/image relative to `refId`, OR —
|
|
2143
|
+
// empty-artboard fallback — as a direct child of `artboardId` (exactly one
|
|
2144
|
+
// of the two). POST { canvas, refId|artboardId, position, kind, src?,
|
|
2145
|
+
// refIndex? } → api.insertElementOp. Returns the new element's
|
|
2146
|
+
// post-transpile id so the shell can select it.
|
|
1858
2147
|
// Whole-file undo seq. MAIN-ORIGIN ONLY (absent from both allowlists);
|
|
1859
2148
|
// sameOriginWrite + loopback-Host gated. An `image` src is contained to
|
|
1860
2149
|
// assets/ in the engine (no remote hotlink / ../ / scheme).
|
|
@@ -1866,6 +2155,7 @@ export function createHttp(
|
|
|
1866
2155
|
const body = await readJson<{
|
|
1867
2156
|
canvas?: unknown;
|
|
1868
2157
|
refId?: unknown;
|
|
2158
|
+
artboardId?: unknown;
|
|
1869
2159
|
position?: unknown;
|
|
1870
2160
|
kind?: unknown;
|
|
1871
2161
|
src?: unknown;
|
|
@@ -1961,6 +2251,24 @@ export function createHttp(
|
|
|
1961
2251
|
);
|
|
1962
2252
|
},
|
|
1963
2253
|
|
|
2254
|
+
'/_api/stickers': async (req: Request) => {
|
|
2255
|
+
// Phase 4 (feature-whiteboard-annotation-improvements) — the bundled
|
|
2256
|
+
// sticker catalogue for the StickerPicker. GET → { packs:[{slug, name,
|
|
2257
|
+
// author, attributionUrl, license, stickers:[{file,keywords,url}]}] }.
|
|
2258
|
+
// MAIN-ORIGIN ONLY: the picker is a shell dialog, same posture as
|
|
2259
|
+
// /_api/assets above — absent from CANVAS_SAFE_API + startCanvasServer
|
|
2260
|
+
// routes (dual-allowlist, DDR-054). Loopback-Host gated (DNS-rebinding);
|
|
2261
|
+
// read-only, so no sameOriginWrite (GET).
|
|
2262
|
+
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2263
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2264
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2265
|
+
const r = await api.listStickers();
|
|
2266
|
+
return Response.json(
|
|
2267
|
+
{ ok: true, packs: r.packs },
|
|
2268
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
2269
|
+
);
|
|
2270
|
+
},
|
|
2271
|
+
|
|
1964
2272
|
'/_api/edit-scope': async (req: Request) => {
|
|
1965
2273
|
// Stage H (feature-element-editing-robustness) — the INV-3 predictability
|
|
1966
2274
|
// verdict. GET ?canvas&id&rendered → { ok, scope:'local'|'shared',
|
|
@@ -2017,6 +2325,71 @@ export function createHttp(
|
|
|
2017
2325
|
);
|
|
2018
2326
|
},
|
|
2019
2327
|
|
|
2328
|
+
'/_api/set-artboard-hug': async (req: Request) => {
|
|
2329
|
+
// Artboard "hug height" default — Hug ⇄ Fixed toggle in the CSS panel.
|
|
2330
|
+
// POST { canvas, artboardId, fixed, freezeHeight? } → api.setArtboardHugOp
|
|
2331
|
+
// (writes/removes the bare `fixed` prop on <DCArtboard id="…">; freezeHeight
|
|
2332
|
+
// optionally pins `height` at the board's current measured size so pinning
|
|
2333
|
+
// to Fixed doesn't snap the box). Whole-file undo seq. MAIN-ORIGIN ONLY;
|
|
2334
|
+
// sameOriginWrite + loopback-Host gated, mirrors /_api/resize-artboard.
|
|
2335
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2336
|
+
if (!sameOriginWrite(req))
|
|
2337
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2338
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2339
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2340
|
+
const body = await readJson<{
|
|
2341
|
+
canvas?: unknown;
|
|
2342
|
+
artboardId?: unknown;
|
|
2343
|
+
fixed?: unknown;
|
|
2344
|
+
freezeHeight?: unknown;
|
|
2345
|
+
}>(req, 8 * 1024);
|
|
2346
|
+
if (!body) return new Response('body required', { status: 400 });
|
|
2347
|
+
const result = await api.setArtboardHugOp(body);
|
|
2348
|
+
if (!result.ok) {
|
|
2349
|
+
return Response.json(
|
|
2350
|
+
{ ok: false, error: result.error },
|
|
2351
|
+
{ status: result.status, headers: { 'Cache-Control': 'no-store' } }
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
return Response.json(
|
|
2355
|
+
{ ok: true, seq: result.seq },
|
|
2356
|
+
{ status: 200, headers: { 'Cache-Control': 'no-store' } }
|
|
2357
|
+
);
|
|
2358
|
+
},
|
|
2359
|
+
|
|
2360
|
+
'/_api/set-artboard-style': async (req: Request) => {
|
|
2361
|
+
// Artboard "more settings" — background / padding / layout / gap, applied
|
|
2362
|
+
// to .dc-artboard-body. POST { canvas, artboardId, background?, padding?,
|
|
2363
|
+
// layout?, gap? } (each key: value to set, `null` to reset, absent to leave
|
|
2364
|
+
// untouched) → api.setArtboardStyleOp. Whole-file undo seq. MAIN-ORIGIN
|
|
2365
|
+
// ONLY; sameOriginWrite + loopback-Host gated, mirrors /_api/resize-artboard.
|
|
2366
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2367
|
+
if (!sameOriginWrite(req))
|
|
2368
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2369
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2370
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2371
|
+
const body = await readJson<{
|
|
2372
|
+
canvas?: unknown;
|
|
2373
|
+
artboardId?: unknown;
|
|
2374
|
+
background?: unknown;
|
|
2375
|
+
padding?: unknown;
|
|
2376
|
+
layout?: unknown;
|
|
2377
|
+
gap?: unknown;
|
|
2378
|
+
}>(req, 8 * 1024);
|
|
2379
|
+
if (!body) return new Response('body required', { status: 400 });
|
|
2380
|
+
const result = await api.setArtboardStyleOp(body);
|
|
2381
|
+
if (!result.ok) {
|
|
2382
|
+
return Response.json(
|
|
2383
|
+
{ ok: false, error: result.error },
|
|
2384
|
+
{ status: result.status, headers: { 'Cache-Control': 'no-store' } }
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
return Response.json(
|
|
2388
|
+
{ ok: true, seq: result.seq },
|
|
2389
|
+
{ status: 200, headers: { 'Cache-Control': 'no-store' } }
|
|
2390
|
+
);
|
|
2391
|
+
},
|
|
2392
|
+
|
|
2020
2393
|
'/_api/delete-artboard': async (req: Request) => {
|
|
2021
2394
|
// Delete an artboard by its `id` prop (Backspace / context-menu on a frame).
|
|
2022
2395
|
// POST { canvas, artboardId } → api.deleteArtboardOp (removes the
|
|
@@ -2078,6 +2451,140 @@ export function createHttp(
|
|
|
2078
2451
|
);
|
|
2079
2452
|
},
|
|
2080
2453
|
|
|
2454
|
+
// DDR-167 (Phase 3 / T10) — hardened LOCAL-file SVG ingestion for the
|
|
2455
|
+
// in-app Brand-upload panel (T12). Privileged + main-origin-only (Decision
|
|
2456
|
+
// 4): absent from CANVAS_SAFE_API + startCanvasServer's routes map — the
|
|
2457
|
+
// untrusted canvas iframe must never reach it. Raw-bytes body only, never
|
|
2458
|
+
// multipart (this dev server has no multipart parsing anywhere). The
|
|
2459
|
+
// `X-Import-Kind` header is a dispatch HINT ONLY — never trusted for the
|
|
2460
|
+
// security-relevant decision; the sanitize pipeline's own pre-parse
|
|
2461
|
+
// structural sniff decides. PDF import is wired but not yet available —
|
|
2462
|
+
// see the DDR's addendum on why the planned rasterization mechanism
|
|
2463
|
+
// (headless-Chromium navigating a file:// PDF) doesn't render content
|
|
2464
|
+
// under browser automation; a `kind: pdf` request 501s naming that.
|
|
2465
|
+
'/_api/import-asset': async (req: Request) => {
|
|
2466
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2467
|
+
if (!sameOriginWrite(req))
|
|
2468
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2469
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2470
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2471
|
+
const kind = (req.headers.get('x-import-kind') || 'svg').toLowerCase();
|
|
2472
|
+
if (kind !== 'svg') {
|
|
2473
|
+
return Response.json(
|
|
2474
|
+
{
|
|
2475
|
+
ok: false,
|
|
2476
|
+
error: 'PDF import is not yet available (DDR-167 addendum) — only SVG import is wired.',
|
|
2477
|
+
},
|
|
2478
|
+
{ status: 501, headers: { 'Cache-Control': 'no-store' } }
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
const declared = Number(req.headers.get('content-length') || '0');
|
|
2482
|
+
if (Number.isFinite(declared) && declared > SVG_MAX_BYTES) {
|
|
2483
|
+
return Response.json(
|
|
2484
|
+
{ ok: false, error: `SVG exceeds the ${SVG_MAX_BYTES / (1024 * 1024)} MB cap` },
|
|
2485
|
+
{ status: 413, headers: { 'Cache-Control': 'no-store' } }
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
if (!req.body) return new Response('empty body', { status: 400 });
|
|
2489
|
+
let svgText: string;
|
|
2490
|
+
try {
|
|
2491
|
+
svgText = await new Response(req.body).text();
|
|
2492
|
+
} catch {
|
|
2493
|
+
return new Response('could not read body', { status: 400 });
|
|
2494
|
+
}
|
|
2495
|
+
try {
|
|
2496
|
+
const result = await importSvg(svgText, {
|
|
2497
|
+
root: ctx.paths.repoRoot,
|
|
2498
|
+
designRootRel: ctx.paths.designRel,
|
|
2499
|
+
});
|
|
2500
|
+
return Response.json(
|
|
2501
|
+
{ ok: true, path: result.path },
|
|
2502
|
+
{ status: 201, headers: { 'Cache-Control': 'no-store' } }
|
|
2503
|
+
);
|
|
2504
|
+
} catch (err) {
|
|
2505
|
+
const status =
|
|
2506
|
+
err instanceof ImportAssetError
|
|
2507
|
+
? err.code === 3
|
|
2508
|
+
? 400
|
|
2509
|
+
: err.code === 5
|
|
2510
|
+
? 415
|
|
2511
|
+
: err.code === 6
|
|
2512
|
+
? 500
|
|
2513
|
+
: 400
|
|
2514
|
+
: 500;
|
|
2515
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2516
|
+
return Response.json(
|
|
2517
|
+
{ ok: false, error: message },
|
|
2518
|
+
{ status, headers: { 'Cache-Control': 'no-store' } }
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
},
|
|
2522
|
+
|
|
2523
|
+
// DDR-173 (Phase 3 / T12) — brand-file typed-cue extraction for the
|
|
2524
|
+
// in-app Brand-upload panel. Privileged + main-origin-only, same posture
|
|
2525
|
+
// as /_api/import-asset: absent from CANVAS_SAFE_API + startCanvasServer's
|
|
2526
|
+
// routes map. Takes a SERVER-GENERATED asset path from a prior
|
|
2527
|
+
// /_api/import-asset response — never a client-supplied filesystem path
|
|
2528
|
+
// (DDR-173 Decision 2: extraction operates only on DDR-167's already-
|
|
2529
|
+
// gated, already-sanitized output, no parallel ungated read). The path is
|
|
2530
|
+
// still realpath-contained + charset-asserted here, never trusted blindly
|
|
2531
|
+
// just because it LOOKS server-shaped.
|
|
2532
|
+
'/_api/import-brand': async (req: Request) => {
|
|
2533
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2534
|
+
if (!sameOriginWrite(req))
|
|
2535
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2536
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2537
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2538
|
+
let body: { assetPath?: unknown };
|
|
2539
|
+
try {
|
|
2540
|
+
body = (await req.json()) as { assetPath?: unknown };
|
|
2541
|
+
} catch {
|
|
2542
|
+
return new Response('invalid JSON body', { status: 400 });
|
|
2543
|
+
}
|
|
2544
|
+
const assetPath = body.assetPath;
|
|
2545
|
+
if (typeof assetPath !== 'string' || !/^assets\/[a-z0-9]{8}\.svg$/.test(assetPath)) {
|
|
2546
|
+
return Response.json(
|
|
2547
|
+
{ ok: false, error: 'assetPath must be a server-generated assets/<sha8>.svg path' },
|
|
2548
|
+
{ status: 400, headers: { 'Cache-Control': 'no-store' } }
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
const designAbs = resolve(ctx.paths.repoRoot, ctx.paths.designRel);
|
|
2552
|
+
const resolvedAsset = resolve(designAbs, assetPath);
|
|
2553
|
+
if (!resolvedAsset.startsWith(resolve(designAbs, 'assets') + sep)) {
|
|
2554
|
+
return Response.json(
|
|
2555
|
+
{ ok: false, error: 'assetPath escapes the assets directory' },
|
|
2556
|
+
{ status: 400, headers: { 'Cache-Control': 'no-store' } }
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
try {
|
|
2560
|
+
const result = await importBrand({
|
|
2561
|
+
sanitizedSvgPath: resolvedAsset,
|
|
2562
|
+
root: ctx.paths.repoRoot,
|
|
2563
|
+
designRootRel: ctx.paths.designRel,
|
|
2564
|
+
});
|
|
2565
|
+
return Response.json(
|
|
2566
|
+
{ ok: true, ...result },
|
|
2567
|
+
{ status: 201, headers: { 'Cache-Control': 'no-store' } }
|
|
2568
|
+
);
|
|
2569
|
+
} catch (err) {
|
|
2570
|
+
const status =
|
|
2571
|
+
err instanceof ImportBrandError
|
|
2572
|
+
? err.code === 3
|
|
2573
|
+
? 400
|
|
2574
|
+
: err.code === 4
|
|
2575
|
+
? 404
|
|
2576
|
+
: err.code === 6
|
|
2577
|
+
? 500
|
|
2578
|
+
: 400
|
|
2579
|
+
: 500;
|
|
2580
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2581
|
+
return Response.json(
|
|
2582
|
+
{ ok: false, error: message },
|
|
2583
|
+
{ status, headers: { 'Cache-Control': 'no-store' } }
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
},
|
|
2587
|
+
|
|
2081
2588
|
'/_api/export-history': async (req: Request) => {
|
|
2082
2589
|
// Phase 6.5 T10 — read-only recent-exports feed for the dialog's
|
|
2083
2590
|
// Recent tab. Writes happen as a side-effect of job completion
|
|
@@ -2212,6 +2719,433 @@ export function createHttp(
|
|
|
2212
2719
|
});
|
|
2213
2720
|
},
|
|
2214
2721
|
|
|
2722
|
+
// feature-ai-media-generation (DDR-16x) — the background AI-media generation
|
|
2723
|
+
// job queue. The privileged sibling of /_api/export-jobs: MAIN-ORIGIN ONLY
|
|
2724
|
+
// (absent from CANVAS_SAFE_API + startCanvasServer's routes — the untrusted
|
|
2725
|
+
// canvas iframe must never reach a route that resolves a provider KEY and
|
|
2726
|
+
// makes an outbound provider call; it sees only the produced /assets/<sha8>).
|
|
2727
|
+
// loopback-Host gated on every method; the mutating POST is additionally
|
|
2728
|
+
// sameOriginWrite CSRF-gated. See DDR-16x + the canvas-origin-gate test.
|
|
2729
|
+
'/_api/generate-jobs': async (req: Request) => {
|
|
2730
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2731
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2732
|
+
if (req.method === 'GET') {
|
|
2733
|
+
return Response.json(
|
|
2734
|
+
{ jobs: generateJobs.list() },
|
|
2735
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2739
|
+
if (!sameOriginWrite(req))
|
|
2740
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2741
|
+
const body = await readJson<Record<string, unknown>>(req, 64 * 1024);
|
|
2742
|
+
if (!body) return new Response('body required', { status: 400 });
|
|
2743
|
+
const check = validateGenRequest(body);
|
|
2744
|
+
if (!check.ok) return new Response(check.errors.join('; '), { status: 400 });
|
|
2745
|
+
const genReq = body as unknown as import('./generation/types.ts').GenRequest;
|
|
2746
|
+
if (!hasProvider(genReq.provider))
|
|
2747
|
+
return new Response(`unknown provider: ${genReq.provider}`, { status: 400 });
|
|
2748
|
+
const descriptor = getProviderDescriptor(genReq.provider);
|
|
2749
|
+
if (descriptor && !descriptor.modalities.includes(genReq.modality))
|
|
2750
|
+
return new Response(`provider ${genReq.provider} cannot do ${genReq.modality}`, {
|
|
2751
|
+
status: 400,
|
|
2752
|
+
});
|
|
2753
|
+
|
|
2754
|
+
try {
|
|
2755
|
+
const { id } = generateJobs.enqueue({
|
|
2756
|
+
provider: genReq.provider,
|
|
2757
|
+
modality: genReq.modality,
|
|
2758
|
+
model: genReq.model,
|
|
2759
|
+
// The job's work: resolve the key AT RUN TIME (never cached), build the
|
|
2760
|
+
// per-request adapter context with the saveAsset-backed localizer, and
|
|
2761
|
+
// submit → result → localize each produced asset into assets/<sha8>.
|
|
2762
|
+
run: async (signal) => {
|
|
2763
|
+
const apiKey = await getProviderKey(genReq.provider);
|
|
2764
|
+
const adapter = createAdapter(genReq.provider, {
|
|
2765
|
+
apiKey,
|
|
2766
|
+
signal,
|
|
2767
|
+
localize: (asset) => localizeGenAsset(asset, { saveAsset: api.saveAsset }),
|
|
2768
|
+
// Task 1.2 — source-asset access for the maskless-edit / i2v flows
|
|
2769
|
+
// (Nano Banana reads the source into an inlineData part). Contained
|
|
2770
|
+
// to assets/ host-side; the adapter never touches the filesystem.
|
|
2771
|
+
readSourceAsset: (rel) => api.readAssetBytes(rel),
|
|
2772
|
+
});
|
|
2773
|
+
const job = await adapter.submit(genReq);
|
|
2774
|
+
const result = await job.result();
|
|
2775
|
+
const assets: string[] = [];
|
|
2776
|
+
for (const asset of result.assets) {
|
|
2777
|
+
// Task 2.6 — a CLOUD STT result (ElevenLabs Scribe / Groq) is
|
|
2778
|
+
// caption TEXT, not media: it lands as a `assets/<sha8>.srt|.vtt`
|
|
2779
|
+
// sidecar next to its source (the SAME path the local whisper verb
|
|
2780
|
+
// writes), never through the magic-byte media store. Everything
|
|
2781
|
+
// else (image/video/audio) localizes into the content-addressed
|
|
2782
|
+
// media store as before.
|
|
2783
|
+
if (asset.kind === 'transcription' && typeof asset.text === 'string') {
|
|
2784
|
+
if (!genReq.sourceAsset)
|
|
2785
|
+
throw new Error('transcription result has no source asset to sidecar');
|
|
2786
|
+
const fmt = asset.mime === 'text/vtt' ? 'vtt' : 'srt';
|
|
2787
|
+
const saved = await api.writeCaptionSidecar(genReq.sourceAsset, fmt, asset.text);
|
|
2788
|
+
if (!saved.ok || !saved.path)
|
|
2789
|
+
throw new Error(`caption sidecar write failed: ${saved.error ?? 'unknown'}`);
|
|
2790
|
+
assets.push(saved.path);
|
|
2791
|
+
} else {
|
|
2792
|
+
assets.push(await localizeGenAsset(asset, { saveAsset: api.saveAsset }));
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
// Task 2.5 — record the AUDIO INTENT next to a generated audio asset
|
|
2796
|
+
// (the durable, semantic reuse index) so a later near-identical
|
|
2797
|
+
// request can offer the existing track instead of paying again.
|
|
2798
|
+
if (genReq.modality === 'audio' && assets.length > 0) {
|
|
2799
|
+
const kind = (genReq.params as Record<string, unknown> | undefined)?.audioKind;
|
|
2800
|
+
await api.writeAudioIntent(assets[0], {
|
|
2801
|
+
kind: typeof kind === 'string' ? kind : undefined,
|
|
2802
|
+
prompt: genReq.prompt,
|
|
2803
|
+
provider: genReq.provider,
|
|
2804
|
+
model: genReq.model,
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
// Task 3.2 — a generated VIDEO clip gets a provenance FootageAnalysis
|
|
2808
|
+
// stub next to it (assets/<sha8>.footage.json), so it is immediately
|
|
2809
|
+
// KNOWN to the footage/reel pipeline: the director sees the clip is
|
|
2810
|
+
// synthetic (the `ai-generated` tag) and what it was made for (the
|
|
2811
|
+
// prompt), and the footage-analyst later fills the real shots. A
|
|
2812
|
+
// sidecar hiccup must NOT lose the (expensive) clip — best-effort.
|
|
2813
|
+
if (genReq.modality === 'video' && assets.length > 0) {
|
|
2814
|
+
try {
|
|
2815
|
+
await footageStore.saveAnalysis(
|
|
2816
|
+
assets[0],
|
|
2817
|
+
generatedClipAnalysis(assets[0], {
|
|
2818
|
+
provider: genReq.provider,
|
|
2819
|
+
model: genReq.model,
|
|
2820
|
+
prompt: genReq.prompt,
|
|
2821
|
+
})
|
|
2822
|
+
);
|
|
2823
|
+
} catch {
|
|
2824
|
+
/* provenance-only — the clip already landed in assets/ */
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
return { assets, usage: result.usage };
|
|
2828
|
+
},
|
|
2829
|
+
});
|
|
2830
|
+
// Don't await the run — the job's own status (GET + the generate:job WS
|
|
2831
|
+
// push) is the completion signal. A failure is recorded on the job
|
|
2832
|
+
// record, so swallow the rejection here to avoid an unhandled rejection.
|
|
2833
|
+
return Response.json(
|
|
2834
|
+
{ jobId: id },
|
|
2835
|
+
{ status: 202, headers: { 'Cache-Control': 'no-store' } }
|
|
2836
|
+
);
|
|
2837
|
+
} catch (err) {
|
|
2838
|
+
if (err instanceof GenerationQueueFullError)
|
|
2839
|
+
return new Response(err.message, { status: 429 });
|
|
2840
|
+
throw err;
|
|
2841
|
+
}
|
|
2842
|
+
},
|
|
2843
|
+
|
|
2844
|
+
// feature-ai-media-generation (DDR-16x) — inert provider catalogue for the
|
|
2845
|
+
// Settings panel + generate dialog: descriptors (id/label/modalities/notes/
|
|
2846
|
+
// keyUrl) + presence-only `configured` flags. NO secret ever crosses here.
|
|
2847
|
+
// MAIN-ORIGIN ONLY, loopback-gated.
|
|
2848
|
+
'/_api/generate/providers': async (req: Request) => {
|
|
2849
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2850
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2851
|
+
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2852
|
+
const configured = new Set(configuredProviders());
|
|
2853
|
+
return Response.json(
|
|
2854
|
+
{
|
|
2855
|
+
providers: listProviders().map((d) => ({ ...d, configured: configured.has(d.id) })),
|
|
2856
|
+
},
|
|
2857
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
2858
|
+
);
|
|
2859
|
+
},
|
|
2860
|
+
|
|
2861
|
+
// feature-ai-media-generation (DDR-16x) — key management. WRITE-ONLY from the
|
|
2862
|
+
// main origin: POST sets a key, DELETE removes one, GET reports presence only
|
|
2863
|
+
// ({configured:[...]}) — a key value is NEVER echoed back (mirrors
|
|
2864
|
+
// github_is_signed_in). MAIN-ORIGIN ONLY, loopback + sameOriginWrite gated.
|
|
2865
|
+
'/_api/generate/keys': async (req: Request) => {
|
|
2866
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2867
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2868
|
+
if (req.method === 'GET') {
|
|
2869
|
+
return Response.json(
|
|
2870
|
+
{ configured: configuredProviders() },
|
|
2871
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
if (!sameOriginWrite(req))
|
|
2875
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2876
|
+
const body = await readJson<{ provider?: unknown; key?: unknown }>(req, 16 * 1024);
|
|
2877
|
+
if (!body || typeof body.provider !== 'string')
|
|
2878
|
+
return new Response('provider required', { status: 400 });
|
|
2879
|
+
if (!hasProvider(body.provider))
|
|
2880
|
+
return new Response(`unknown provider: ${body.provider}`, { status: 400 });
|
|
2881
|
+
try {
|
|
2882
|
+
if (req.method === 'DELETE') {
|
|
2883
|
+
deleteProviderKey(body.provider);
|
|
2884
|
+
return Response.json({ configured: false }, { headers: { 'Cache-Control': 'no-store' } });
|
|
2885
|
+
}
|
|
2886
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2887
|
+
if (typeof body.key !== 'string' || !body.key.trim())
|
|
2888
|
+
return new Response('key required', { status: 400 });
|
|
2889
|
+
setProviderKey(body.provider, body.key);
|
|
2890
|
+
// Presence flag only — never the key. Deliberately does not read the
|
|
2891
|
+
// value back so a proxy/log between here and the client can't capture it.
|
|
2892
|
+
return Response.json(
|
|
2893
|
+
{ configured: isConfigured(body.provider) },
|
|
2894
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
2895
|
+
);
|
|
2896
|
+
} catch (err) {
|
|
2897
|
+
return new Response(err instanceof Error ? err.message : 'key write failed', {
|
|
2898
|
+
status: 400,
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
},
|
|
2902
|
+
|
|
2903
|
+
// feature-ai-media-generation (Task 2.6, DDR-164) — NON-SECRET generation
|
|
2904
|
+
// preferences (the transcription-engine choice). GET returns the current
|
|
2905
|
+
// choice; POST persists it into `.design/config.json` and hot-reloads the
|
|
2906
|
+
// config so the next transcribe picks it up without a restart. MAIN-ORIGIN
|
|
2907
|
+
// ONLY, loopback + sameOriginWrite gated. NEVER touches a key — that's the
|
|
2908
|
+
// separate /_api/generate/keys route.
|
|
2909
|
+
'/_api/generate/prefs': async (req: Request) => {
|
|
2910
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2911
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2912
|
+
if (req.method === 'GET') {
|
|
2913
|
+
return Response.json(
|
|
2914
|
+
{ transcriptionProvider: readTranscriptionProvider(ctx.paths.repoRoot) },
|
|
2915
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
2916
|
+
);
|
|
2917
|
+
}
|
|
2918
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2919
|
+
if (!sameOriginWrite(req))
|
|
2920
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2921
|
+
const body = await readJson<{ transcriptionProvider?: unknown }>(req, 4 * 1024);
|
|
2922
|
+
const provider = body?.transcriptionProvider;
|
|
2923
|
+
if (!isTranscriptionProvider(provider))
|
|
2924
|
+
return new Response('transcriptionProvider must be whisper|elevenlabs|groq', {
|
|
2925
|
+
status: 400,
|
|
2926
|
+
});
|
|
2927
|
+
try {
|
|
2928
|
+
await writeTranscriptionProvider(ctx.paths.repoRoot, provider);
|
|
2929
|
+
reloadConfig(ctx);
|
|
2930
|
+
return Response.json(
|
|
2931
|
+
{ transcriptionProvider: provider },
|
|
2932
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
2933
|
+
);
|
|
2934
|
+
} catch (err) {
|
|
2935
|
+
return new Response(err instanceof Error ? err.message : 'prefs write failed', {
|
|
2936
|
+
status: 400,
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
},
|
|
2940
|
+
|
|
2941
|
+
// feature-unified-settings-modal — NON-SECRET UI / view preferences (theme +
|
|
2942
|
+
// the Canvas & View toggles), persisted to `~/.config/maude/prefs.json` so
|
|
2943
|
+
// they survive a restart and a cleared localStorage. GET returns the merged
|
|
2944
|
+
// prefs; POST merge-patches only the provided keys. MAIN-ORIGIN ONLY, loopback
|
|
2945
|
+
// + sameOriginWrite gated (never reachable from the untrusted canvas origin).
|
|
2946
|
+
'/_api/ui-prefs': async (req: Request) => {
|
|
2947
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
2948
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2949
|
+
if (req.method === 'GET') {
|
|
2950
|
+
return Response.json(readUiPrefs(), { headers: { 'Cache-Control': 'no-store' } });
|
|
2951
|
+
}
|
|
2952
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2953
|
+
if (!sameOriginWrite(req))
|
|
2954
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
2955
|
+
const body = await readJson<Record<string, unknown>>(req, 4 * 1024);
|
|
2956
|
+
if (!body || typeof body !== 'object')
|
|
2957
|
+
return new Response('body must be a JSON object', { status: 400 });
|
|
2958
|
+
// Build a clean patch — only well-typed known keys pass through, so a bad
|
|
2959
|
+
// field is rejected rather than silently resetting a stored value.
|
|
2960
|
+
const patch: Partial<UiPrefs> = {};
|
|
2961
|
+
if ('theme' in body) {
|
|
2962
|
+
if (body.theme !== 'light' && body.theme !== 'dark')
|
|
2963
|
+
return new Response('theme must be light|dark', { status: 400 });
|
|
2964
|
+
patch.theme = body.theme;
|
|
2965
|
+
}
|
|
2966
|
+
for (const k of ['minimap', 'zoom', 'annotations', 'autoOpenInspector'] as const) {
|
|
2967
|
+
if (k in body) {
|
|
2968
|
+
if (typeof body[k] !== 'boolean')
|
|
2969
|
+
return new Response(`${k} must be a boolean`, { status: 400 });
|
|
2970
|
+
patch[k] = body[k] as boolean;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
if ('layersMode' in body) {
|
|
2974
|
+
if (body.layersMode !== 'separate' && body.layersMode !== 'in-inspector')
|
|
2975
|
+
return new Response('layersMode must be separate|in-inspector', { status: 400 });
|
|
2976
|
+
patch.layersMode = body.layersMode;
|
|
2977
|
+
}
|
|
2978
|
+
if ('panelSides' in body) {
|
|
2979
|
+
const ps = body.panelSides;
|
|
2980
|
+
if (!ps || typeof ps !== 'object' || Array.isArray(ps))
|
|
2981
|
+
return new Response('panelSides must be an object', { status: 400 });
|
|
2982
|
+
for (const v of Object.values(ps as Record<string, unknown>)) {
|
|
2983
|
+
if (v !== 'left' && v !== 'right')
|
|
2984
|
+
return new Response('panelSides values must be left|right', { status: 400 });
|
|
2985
|
+
}
|
|
2986
|
+
// writeUiPrefs → coerce keeps only known ids, so unknown keys are dropped.
|
|
2987
|
+
patch.panelSides = ps as UiPrefs['panelSides'];
|
|
2988
|
+
}
|
|
2989
|
+
try {
|
|
2990
|
+
return Response.json(writeUiPrefs(patch), { headers: { 'Cache-Control': 'no-store' } });
|
|
2991
|
+
} catch (err) {
|
|
2992
|
+
return new Response(err instanceof Error ? err.message : 'ui-prefs write failed', {
|
|
2993
|
+
status: 500,
|
|
2994
|
+
});
|
|
2995
|
+
}
|
|
2996
|
+
},
|
|
2997
|
+
|
|
2998
|
+
// feature-ai-media-generation (Task 2.5, DDR-164) — reuse-before-you-pay for
|
|
2999
|
+
// AUDIO. GET searches the project's OWN generated audio (intent sidecars) and,
|
|
3000
|
+
// when ElevenLabs is configured, the user's re-downloadable History (free —
|
|
3001
|
+
// already paid), returning ranked reuse candidates. MAIN-ORIGIN ONLY,
|
|
3002
|
+
// loopback-gated (it resolves the provider key server-side).
|
|
3003
|
+
'/_api/generate/audio-search': async (req: Request) => {
|
|
3004
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
3005
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3006
|
+
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3007
|
+
// F1 (ethical-hacker) — this GET resolves the user's key + fans out to the
|
|
3008
|
+
// provider, so it must NOT be cross-site-triggerable (a no-cors GET can slip
|
|
3009
|
+
// past the loopback guard). Fetch-Metadata gate: browser cross-site → 403;
|
|
3010
|
+
// the CLI (no Sec-Fetch-Site) → allowed.
|
|
3011
|
+
if (!sameOriginRead(req)) return new Response('cross-site read rejected', { status: 403 });
|
|
3012
|
+
const q = new URL(req.url).searchParams.get('q') ?? '';
|
|
3013
|
+
if (!q.trim()) return new Response('q query param required', { status: 400 });
|
|
3014
|
+
const local: AudioMatch[] = await api.searchAudioLibrary(q, 10);
|
|
3015
|
+
let history: AudioMatch[] = [];
|
|
3016
|
+
// Only reach the provider when its key is present (no key → local-only).
|
|
3017
|
+
if (isConfigured('elevenlabs')) {
|
|
3018
|
+
try {
|
|
3019
|
+
const apiKey = await getProviderKey('elevenlabs');
|
|
3020
|
+
const adapter = createAdapter('elevenlabs', {
|
|
3021
|
+
apiKey,
|
|
3022
|
+
localize: (asset) => localizeGenAsset(asset, { saveAsset: api.saveAsset }),
|
|
3023
|
+
});
|
|
3024
|
+
if (adapter.listHistory) {
|
|
3025
|
+
const items = await adapter.listHistory();
|
|
3026
|
+
const candidates: Candidate[] = items.map((it) => ({
|
|
3027
|
+
source: 'history' as const,
|
|
3028
|
+
ref: it.id,
|
|
3029
|
+
// History `text` is the user's own generation source, but a TTS of
|
|
3030
|
+
// attacker-supplied text could carry injection — sanitize before it
|
|
3031
|
+
// reaches the agent-facing output (F3).
|
|
3032
|
+
text: sanitizeReuseText(it.text),
|
|
3033
|
+
provider: 'elevenlabs',
|
|
3034
|
+
at: it.at,
|
|
3035
|
+
}));
|
|
3036
|
+
history = rankMatches(q, candidates, { limit: 10 });
|
|
3037
|
+
}
|
|
3038
|
+
} catch {
|
|
3039
|
+
// History is best-effort (rate limits / transient) — degrade to local.
|
|
3040
|
+
history = [];
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
return Response.json({ local, history }, { headers: { 'Cache-Control': 'no-store' } });
|
|
3044
|
+
},
|
|
3045
|
+
|
|
3046
|
+
// feature-ai-media-generation (Task 2.5) — reuse a History item: re-download
|
|
3047
|
+
// its bytes (NO credit — already paid) through the host's magic-byte-sniffed
|
|
3048
|
+
// saveAsset, localize into assets/<sha8>, and record the audio intent so the
|
|
3049
|
+
// reused track is itself searchable. MAIN-ORIGIN ONLY, loopback +
|
|
3050
|
+
// sameOriginWrite gated.
|
|
3051
|
+
'/_api/generate/audio-reuse': async (req: Request) => {
|
|
3052
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
3053
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3054
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3055
|
+
if (!sameOriginWrite(req))
|
|
3056
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
3057
|
+
const body = await readJson<{ id?: unknown }>(req, 8 * 1024);
|
|
3058
|
+
const id = body?.id;
|
|
3059
|
+
if (typeof id !== 'string' || !id.trim())
|
|
3060
|
+
return new Response('history item id required', { status: 400 });
|
|
3061
|
+
if (!isConfigured('elevenlabs'))
|
|
3062
|
+
return new Response('no ElevenLabs key configured', { status: 400 });
|
|
3063
|
+
try {
|
|
3064
|
+
const apiKey = await getProviderKey('elevenlabs');
|
|
3065
|
+
const adapter = createAdapter('elevenlabs', {
|
|
3066
|
+
apiKey,
|
|
3067
|
+
localize: (asset) => localizeGenAsset(asset, { saveAsset: api.saveAsset }),
|
|
3068
|
+
});
|
|
3069
|
+
if (!adapter.fetchHistoryAudio)
|
|
3070
|
+
return new Response('provider has no history re-download', { status: 400 });
|
|
3071
|
+
const asset = await adapter.fetchHistoryAudio(id);
|
|
3072
|
+
const rel = await localizeGenAsset(asset, { saveAsset: api.saveAsset });
|
|
3073
|
+
await api.writeAudioIntent(rel, {
|
|
3074
|
+
kind: 'tts',
|
|
3075
|
+
prompt: `reused from ElevenLabs history ${id}`,
|
|
3076
|
+
provider: 'elevenlabs',
|
|
3077
|
+
});
|
|
3078
|
+
return Response.json({ asset: rel }, { headers: { 'Cache-Control': 'no-store' } });
|
|
3079
|
+
} catch (err) {
|
|
3080
|
+
return new Response(err instanceof Error ? err.message : 'reuse failed', { status: 400 });
|
|
3081
|
+
}
|
|
3082
|
+
},
|
|
3083
|
+
|
|
3084
|
+
// feature-ai-media-generation (Task 2.7 approach A, DDR-164) — managed local
|
|
3085
|
+
// whisper.cpp GGML models: GET lists the registry + which are downloaded +
|
|
3086
|
+
// any in-flight download; POST {id} downloads one (SSRF-hardened, from the
|
|
3087
|
+
// frozen ggerganov/whisper.cpp allowlist); DELETE {id} reclaims disk. This is
|
|
3088
|
+
// the "one-click local subtitles" model half — after a download,
|
|
3089
|
+
// `maude design transcribe --provider whisper` auto-resolves it. MAIN-ORIGIN
|
|
3090
|
+
// ONLY, loopback + sameOriginWrite gated.
|
|
3091
|
+
'/_api/generate/whisper-model': async (req: Request) => {
|
|
3092
|
+
if (!isLoopbackHost(req.headers.get('host')))
|
|
3093
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3094
|
+
if (req.method === 'GET') {
|
|
3095
|
+
return Response.json(
|
|
3096
|
+
{ models: listWhisperModels(), downloading: whisperDownload },
|
|
3097
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
if (!sameOriginWrite(req))
|
|
3101
|
+
return new Response('cross-origin write rejected', { status: 403 });
|
|
3102
|
+
const body = await readJson<{ id?: unknown }>(req, 4 * 1024);
|
|
3103
|
+
const id = typeof body?.id === 'string' ? body.id : '';
|
|
3104
|
+
if (!getWhisperModel(id)) return new Response('unknown model id', { status: 400 });
|
|
3105
|
+
|
|
3106
|
+
if (req.method === 'DELETE') {
|
|
3107
|
+
const removed = await removeWhisperModel(id);
|
|
3108
|
+
return Response.json({ removed }, { headers: { 'Cache-Control': 'no-store' } });
|
|
3109
|
+
}
|
|
3110
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3111
|
+
if (whisperDownload && !whisperDownload.error)
|
|
3112
|
+
return new Response(`a download is already in progress (${whisperDownload.id})`, {
|
|
3113
|
+
status: 409,
|
|
3114
|
+
});
|
|
3115
|
+
// Kick off the streamed download; progress is polled via GET. Swallow the
|
|
3116
|
+
// rejection here (recorded on the state) to avoid an unhandled rejection.
|
|
3117
|
+
// A generous wall-clock timeout (ethical-hacker Finding 3) so a stalled
|
|
3118
|
+
// connection aborts → errors → frees the single slot instead of wedging it
|
|
3119
|
+
// forever (large models on a slow link can legitimately take many minutes).
|
|
3120
|
+
whisperDownload = { id, received: 0, total: 0 };
|
|
3121
|
+
void downloadWhisperModel(
|
|
3122
|
+
id,
|
|
3123
|
+
(received, total) => {
|
|
3124
|
+
if (whisperDownload && whisperDownload.id === id) {
|
|
3125
|
+
whisperDownload.received = received;
|
|
3126
|
+
whisperDownload.total = total;
|
|
3127
|
+
}
|
|
3128
|
+
},
|
|
3129
|
+
AbortSignal.timeout(45 * 60_000)
|
|
3130
|
+
)
|
|
3131
|
+
.then(() => {
|
|
3132
|
+
if (whisperDownload?.id === id) whisperDownload = null; // done → drops out of GET
|
|
3133
|
+
})
|
|
3134
|
+
.catch((err) => {
|
|
3135
|
+
if (whisperDownload?.id === id)
|
|
3136
|
+
whisperDownload = {
|
|
3137
|
+
id,
|
|
3138
|
+
received: whisperDownload.received,
|
|
3139
|
+
total: whisperDownload.total,
|
|
3140
|
+
error: err instanceof Error ? err.message : 'download failed',
|
|
3141
|
+
};
|
|
3142
|
+
});
|
|
3143
|
+
return Response.json(
|
|
3144
|
+
{ started: id },
|
|
3145
|
+
{ status: 202, headers: { 'Cache-Control': 'no-store' } }
|
|
3146
|
+
);
|
|
3147
|
+
},
|
|
3148
|
+
|
|
2215
3149
|
'/_canvas-state': async (req: Request) => {
|
|
2216
3150
|
const url = new URL(req.url);
|
|
2217
3151
|
if (req.method === 'GET') {
|
|
@@ -2340,14 +3274,21 @@ export function createHttp(
|
|
|
2340
3274
|
} catch {
|
|
2341
3275
|
return new Response('Bad request', { status: 400 });
|
|
2342
3276
|
}
|
|
2343
|
-
|
|
3277
|
+
// DDR-173 (Phase 3 / T12) — brand-logo assets live one level down at
|
|
3278
|
+
// `assets/logos/<sha8>.<ext>` (DDR-141's own convention). This is the
|
|
3279
|
+
// ONE legitimate subdirectory allowed here, matched by its own exact,
|
|
3280
|
+
// closed-charset shape — not a general "any subdirectory" opt-in,
|
|
3281
|
+
// which would widen this route's containment beyond what DDR-141
|
|
3282
|
+
// actually calls for.
|
|
3283
|
+
const isFlat =
|
|
2344
3284
|
name &&
|
|
2345
3285
|
!name.includes('/') &&
|
|
2346
3286
|
!name.includes('\\') &&
|
|
2347
3287
|
!name.includes('..') &&
|
|
2348
3288
|
!name.startsWith('_') &&
|
|
2349
|
-
CANVAS_ASSET_EXTS.has(ext(name))
|
|
2350
|
-
|
|
3289
|
+
CANVAS_ASSET_EXTS.has(ext(name));
|
|
3290
|
+
const isLogoSubdir = /^logos\/[a-z0-9]{8}\.(svg|png)$/.test(name);
|
|
3291
|
+
if (isFlat || isLogoSubdir) {
|
|
2351
3292
|
const abs = join(ctx.paths.designRoot, 'assets', name);
|
|
2352
3293
|
// Range-aware for video/audio (scrubbing + WKWebView compat).
|
|
2353
3294
|
if (RANGE_MEDIA_EXTS.has(ext(name))) {
|
|
@@ -2367,6 +3308,81 @@ export function createHttp(
|
|
|
2367
3308
|
}
|
|
2368
3309
|
}
|
|
2369
3310
|
|
|
3311
|
+
// Phase 4 (feature-whiteboard-annotation-improvements) — bundled sticker
|
|
3312
|
+
// PNGs, served from MAUDE's OWN STICKERS_DIR (paths.ts, DDR-045), never
|
|
3313
|
+
// the served project's designRoot (unlike /assets/ above). MAIN-ORIGIN
|
|
3314
|
+
// ONLY: absent from CANVAS_SAFE_API + startCanvasServer's own routes
|
|
3315
|
+
// object (server.ts) — a canvas-origin request 404s via isCanvasSafeRoute
|
|
3316
|
+
// before ever reaching here (dual-allowlist, DDR-054/DDR-088).
|
|
3317
|
+
if (pathname.startsWith('/_stickers/')) {
|
|
3318
|
+
const rest = pathname.slice('/_stickers/'.length);
|
|
3319
|
+
let decoded = '';
|
|
3320
|
+
try {
|
|
3321
|
+
decoded = decodeURIComponent(rest);
|
|
3322
|
+
} catch {
|
|
3323
|
+
return new Response('Bad request', { status: 400 });
|
|
3324
|
+
}
|
|
3325
|
+
const parts = decoded.split('/');
|
|
3326
|
+
const [pack, name] = parts;
|
|
3327
|
+
if (
|
|
3328
|
+
parts.length === 2 &&
|
|
3329
|
+
pack &&
|
|
3330
|
+
name &&
|
|
3331
|
+
/^[a-z0-9-]+$/.test(pack) &&
|
|
3332
|
+
!name.includes('..') &&
|
|
3333
|
+
!name.includes('/') &&
|
|
3334
|
+
!name.includes('\\') &&
|
|
3335
|
+
ext(name) === '.png'
|
|
3336
|
+
) {
|
|
3337
|
+
const abs = join(STICKERS_DIR, pack, name);
|
|
3338
|
+
const f = Bun.file(abs);
|
|
3339
|
+
if (await f.exists()) {
|
|
3340
|
+
return new Response(f, {
|
|
3341
|
+
headers: {
|
|
3342
|
+
'Content-Type': 'image/png',
|
|
3343
|
+
'Cache-Control': 'public, max-age=31536000, immutable', // bundled with this maude version, never changes at this URL
|
|
3344
|
+
'X-Content-Type-Options': 'nosniff',
|
|
3345
|
+
},
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
return new Response('Not found', { status: 404 });
|
|
3349
|
+
}
|
|
3350
|
+
return new Response('Not found', { status: 404 });
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
// DDR-166 Phase 1 / T2 — bundled Maude-product media (the intro
|
|
3354
|
+
// showreel), served from MAUDE's OWN MEDIA_DIR (paths.ts, DDR-045),
|
|
3355
|
+
// never the served project's designRoot — every user of the canvas
|
|
3356
|
+
// browser sees the same intro regardless of which project is open.
|
|
3357
|
+
// Same shape as /_stickers/ above: MAIN-ORIGIN ONLY (absent from
|
|
3358
|
+
// CANVAS_SAFE_API + startCanvasServer's routes), Range-capable via
|
|
3359
|
+
// serveMediaFile (the wizard/Help player needs to scrub).
|
|
3360
|
+
if (pathname.startsWith('/_media/')) {
|
|
3361
|
+
const name = pathname.slice('/_media/'.length);
|
|
3362
|
+
let decoded = '';
|
|
3363
|
+
try {
|
|
3364
|
+
decoded = decodeURIComponent(name);
|
|
3365
|
+
} catch {
|
|
3366
|
+
return new Response('Bad request', { status: 400 });
|
|
3367
|
+
}
|
|
3368
|
+
if (
|
|
3369
|
+
decoded &&
|
|
3370
|
+
!decoded.includes('..') &&
|
|
3371
|
+
!decoded.includes('/') &&
|
|
3372
|
+
!decoded.includes('\\') &&
|
|
3373
|
+
RANGE_MEDIA_EXTS.has(ext(decoded))
|
|
3374
|
+
) {
|
|
3375
|
+
const abs = join(MEDIA_DIR, decoded);
|
|
3376
|
+
if (await Bun.file(abs).exists()) {
|
|
3377
|
+
return serveMediaFile(abs, req, {
|
|
3378
|
+
'Cache-Control': 'public, max-age=31536000, immutable', // bundled with this maude version, never changes at this URL
|
|
3379
|
+
'X-Content-Type-Options': 'nosniff',
|
|
3380
|
+
});
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return new Response('Not found', { status: 404 });
|
|
3384
|
+
}
|
|
3385
|
+
|
|
2370
3386
|
// Fall-through: serve user repo files (designRoot + everything under repoRoot).
|
|
2371
3387
|
const fp = safePathUnderRoot(req.url, ctx.paths.repoRoot);
|
|
2372
3388
|
if (!fp) return new Response('Forbidden', { status: 403 });
|
|
@@ -2458,7 +3474,8 @@ export function createHttp(
|
|
|
2458
3474
|
'/_api/git-user', // presence display name
|
|
2459
3475
|
'/_api/canvas-meta', // layout/viewport sidecar (GET + PATCH)
|
|
2460
3476
|
'/_api/annotations', // annotation SVG (GET + PUT) — drives the collab bridge
|
|
2461
|
-
'/_api/asset', // Phase 23 — capped binary image upload (sniff+
|
|
3477
|
+
'/_api/asset', // Phase 23 — capped binary image upload (sniff+category cap+sha8 name+no-SVG)
|
|
3478
|
+
'/_api/photo-edit', // feature-photo-editor — PhotoEdit sidecar GET/PUT (cap-stack gated). MIRROR in server.ts routes.
|
|
2462
3479
|
'/_api/git-committers', // @mention autocomplete
|
|
2463
3480
|
'/_api/ai', // AI-activity banner
|
|
2464
3481
|
'/_comments', // per-file comment list (renders pins)
|