@1agh/maude 0.42.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.
Files changed (236) hide show
  1. package/apps/studio/.ai/cache/_stats.json +7 -0
  2. package/apps/studio/acp/bootstrap-brief.ts +12 -2
  3. package/apps/studio/annotations-layer.tsx +1055 -278
  4. package/apps/studio/annotations-model.ts +58 -0
  5. package/apps/studio/api.ts +173 -22
  6. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  7. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  8. package/apps/studio/bin/_video-playwright.mjs +141 -71
  9. package/apps/studio/bin/ingest-footage.sh +27 -0
  10. package/apps/studio/bin/photo-adjust.sh +163 -0
  11. package/apps/studio/bin/photo-bg-remove.sh +160 -0
  12. package/apps/studio/bin/probe-footage.sh +28 -0
  13. package/apps/studio/bin/read-annotations.mjs +125 -2
  14. package/apps/studio/canvas-edit.ts +459 -10
  15. package/apps/studio/canvas-lib.tsx +523 -5
  16. package/apps/studio/canvas-pipeline.ts +65 -7
  17. package/apps/studio/canvas-shell.tsx +156 -16
  18. package/apps/studio/client/app.jsx +451 -34
  19. package/apps/studio/client/export-center.jsx +35 -1
  20. package/apps/studio/client/github.js +4 -0
  21. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  22. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  23. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  24. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  25. package/apps/studio/client/photo-knobs.jsx +437 -0
  26. package/apps/studio/client/styles/3-shell-maude.css +42 -0
  27. package/apps/studio/commands/edit-source-command.ts +31 -4
  28. package/apps/studio/context-menu.tsx +31 -6
  29. package/apps/studio/dist/client.bundle.js +5561 -19
  30. package/apps/studio/dist/comment-mount.js +2 -2
  31. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  32. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  33. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  34. package/apps/studio/dist/styles.css +1 -1
  35. package/apps/studio/dom-selection.ts +25 -0
  36. package/apps/studio/exporters/jobs.ts +47 -5
  37. package/apps/studio/exporters/video.ts +24 -4
  38. package/apps/studio/footage/schema.test.ts +195 -0
  39. package/apps/studio/footage/schema.ts +484 -0
  40. package/apps/studio/footage-store.ts +235 -0
  41. package/apps/studio/fs-watch.ts +10 -0
  42. package/apps/studio/git/endpoints.ts +68 -7
  43. package/apps/studio/git/service.ts +196 -20
  44. package/apps/studio/github/service.ts +67 -0
  45. package/apps/studio/handoff.ts +3 -2
  46. package/apps/studio/http.ts +173 -14
  47. package/apps/studio/input-router.tsx +10 -0
  48. package/apps/studio/inspect.ts +53 -1
  49. package/apps/studio/paths.ts +10 -0
  50. package/apps/studio/photo/filters.ts +285 -0
  51. package/apps/studio/photo/pipeline.ts +607 -0
  52. package/apps/studio/photo/schema.ts +367 -0
  53. package/apps/studio/photo-store.ts +154 -0
  54. package/apps/studio/runtime-bundle.ts +29 -6
  55. package/apps/studio/server.ts +5 -1
  56. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  57. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  58. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  59. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  60. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  61. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  62. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  63. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  64. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  65. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  66. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  67. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  68. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  69. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  70. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  71. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  72. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  73. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  74. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  75. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  76. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  77. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  78. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  79. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  80. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  81. package/apps/studio/stickers/life-style/best.png +0 -0
  82. package/apps/studio/stickers/life-style/come-on.png +0 -0
  83. package/apps/studio/stickers/life-style/cut.png +0 -0
  84. package/apps/studio/stickers/life-style/detox.png +0 -0
  85. package/apps/studio/stickers/life-style/just.png +0 -0
  86. package/apps/studio/stickers/life-style/manifest.json +68 -0
  87. package/apps/studio/stickers/life-style/mirror.png +0 -0
  88. package/apps/studio/stickers/life-style/nice.png +0 -0
  89. package/apps/studio/stickers/life-style/objects.png +0 -0
  90. package/apps/studio/stickers/life-style/ok.png +0 -0
  91. package/apps/studio/stickers/life-style/queen.png +0 -0
  92. package/apps/studio/stickers/life-style/star.png +0 -0
  93. package/apps/studio/stickers/life-style/sun.png +0 -0
  94. package/apps/studio/stickers/life-style/water.png +0 -0
  95. package/apps/studio/stickers/life-style/yeah.png +0 -0
  96. package/apps/studio/stickers/life-style/you-can.png +0 -0
  97. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  98. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  99. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  100. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  101. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  102. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  103. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  104. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  105. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  106. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  107. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  108. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  109. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  110. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  111. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  112. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  113. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  114. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  115. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  116. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  117. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  118. package/apps/studio/stickers/project-status/group-100.png +0 -0
  119. package/apps/studio/stickers/project-status/group-101.png +0 -0
  120. package/apps/studio/stickers/project-status/group-102.png +0 -0
  121. package/apps/studio/stickers/project-status/group-103.png +0 -0
  122. package/apps/studio/stickers/project-status/group-104.png +0 -0
  123. package/apps/studio/stickers/project-status/group-105.png +0 -0
  124. package/apps/studio/stickers/project-status/group-106.png +0 -0
  125. package/apps/studio/stickers/project-status/group-107.png +0 -0
  126. package/apps/studio/stickers/project-status/group-108.png +0 -0
  127. package/apps/studio/stickers/project-status/group-109.png +0 -0
  128. package/apps/studio/stickers/project-status/group-110.png +0 -0
  129. package/apps/studio/stickers/project-status/group-111.png +0 -0
  130. package/apps/studio/stickers/project-status/group-112.png +0 -0
  131. package/apps/studio/stickers/project-status/group-113.png +0 -0
  132. package/apps/studio/stickers/project-status/group-114.png +0 -0
  133. package/apps/studio/stickers/project-status/group-115.png +0 -0
  134. package/apps/studio/stickers/project-status/group-117.png +0 -0
  135. package/apps/studio/stickers/project-status/group-118.png +0 -0
  136. package/apps/studio/stickers/project-status/group-119.png +0 -0
  137. package/apps/studio/stickers/project-status/group-120.png +0 -0
  138. package/apps/studio/stickers/project-status/group-121.png +0 -0
  139. package/apps/studio/stickers/project-status/group-122.png +0 -0
  140. package/apps/studio/stickers/project-status/group-41.png +0 -0
  141. package/apps/studio/stickers/project-status/group-42.png +0 -0
  142. package/apps/studio/stickers/project-status/group-43.png +0 -0
  143. package/apps/studio/stickers/project-status/group-44.png +0 -0
  144. package/apps/studio/stickers/project-status/group-45.png +0 -0
  145. package/apps/studio/stickers/project-status/group-46.png +0 -0
  146. package/apps/studio/stickers/project-status/group-47.png +0 -0
  147. package/apps/studio/stickers/project-status/group-48.png +0 -0
  148. package/apps/studio/stickers/project-status/group-49.png +0 -0
  149. package/apps/studio/stickers/project-status/group-50.png +0 -0
  150. package/apps/studio/stickers/project-status/group-51.png +0 -0
  151. package/apps/studio/stickers/project-status/group-52.png +0 -0
  152. package/apps/studio/stickers/project-status/group-53.png +0 -0
  153. package/apps/studio/stickers/project-status/group-54.png +0 -0
  154. package/apps/studio/stickers/project-status/group-55.png +0 -0
  155. package/apps/studio/stickers/project-status/group-56.png +0 -0
  156. package/apps/studio/stickers/project-status/group-57.png +0 -0
  157. package/apps/studio/stickers/project-status/group-58.png +0 -0
  158. package/apps/studio/stickers/project-status/group-59.png +0 -0
  159. package/apps/studio/stickers/project-status/group-60.png +0 -0
  160. package/apps/studio/stickers/project-status/group-61.png +0 -0
  161. package/apps/studio/stickers/project-status/group-62.png +0 -0
  162. package/apps/studio/stickers/project-status/group-63.png +0 -0
  163. package/apps/studio/stickers/project-status/group-64.png +0 -0
  164. package/apps/studio/stickers/project-status/group-65.png +0 -0
  165. package/apps/studio/stickers/project-status/group-66.png +0 -0
  166. package/apps/studio/stickers/project-status/group-67.png +0 -0
  167. package/apps/studio/stickers/project-status/group-68.png +0 -0
  168. package/apps/studio/stickers/project-status/group-69.png +0 -0
  169. package/apps/studio/stickers/project-status/group-70.png +0 -0
  170. package/apps/studio/stickers/project-status/group-71.png +0 -0
  171. package/apps/studio/stickers/project-status/group-72.png +0 -0
  172. package/apps/studio/stickers/project-status/group-73.png +0 -0
  173. package/apps/studio/stickers/project-status/group-74.png +0 -0
  174. package/apps/studio/stickers/project-status/group-75.png +0 -0
  175. package/apps/studio/stickers/project-status/group-76.png +0 -0
  176. package/apps/studio/stickers/project-status/group-77.png +0 -0
  177. package/apps/studio/stickers/project-status/group-78.png +0 -0
  178. package/apps/studio/stickers/project-status/group-79.png +0 -0
  179. package/apps/studio/stickers/project-status/group-80.png +0 -0
  180. package/apps/studio/stickers/project-status/group-81.png +0 -0
  181. package/apps/studio/stickers/project-status/group-82.png +0 -0
  182. package/apps/studio/stickers/project-status/group-83.png +0 -0
  183. package/apps/studio/stickers/project-status/group-84.png +0 -0
  184. package/apps/studio/stickers/project-status/group-85.png +0 -0
  185. package/apps/studio/stickers/project-status/group-86.png +0 -0
  186. package/apps/studio/stickers/project-status/group-87.png +0 -0
  187. package/apps/studio/stickers/project-status/group-88.png +0 -0
  188. package/apps/studio/stickers/project-status/group-89.png +0 -0
  189. package/apps/studio/stickers/project-status/group-90.png +0 -0
  190. package/apps/studio/stickers/project-status/group-91.png +0 -0
  191. package/apps/studio/stickers/project-status/group-92.png +0 -0
  192. package/apps/studio/stickers/project-status/group-93.png +0 -0
  193. package/apps/studio/stickers/project-status/group-94.png +0 -0
  194. package/apps/studio/stickers/project-status/group-96.png +0 -0
  195. package/apps/studio/stickers/project-status/group-97.png +0 -0
  196. package/apps/studio/stickers/project-status/group-98.png +0 -0
  197. package/apps/studio/stickers/project-status/group-99.png +0 -0
  198. package/apps/studio/stickers/project-status/manifest.json +328 -0
  199. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  200. package/apps/studio/test/acp-commands.test.ts +2 -1
  201. package/apps/studio/test/asset-api.test.ts +3 -3
  202. package/apps/studio/test/canvas-edit.test.ts +17 -6
  203. package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
  204. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  205. package/apps/studio/test/edit-source-command.test.ts +23 -0
  206. package/apps/studio/test/element-structural-edit.test.ts +46 -0
  207. package/apps/studio/test/footage-store.test.ts +100 -0
  208. package/apps/studio/test/git-branches.test.ts +97 -0
  209. package/apps/studio/test/github-api.test.ts +56 -1
  210. package/apps/studio/test/input-router.test.ts +33 -0
  211. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  212. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  213. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  214. package/apps/studio/test/photo-filters.test.ts +149 -0
  215. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  216. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  217. package/apps/studio/test/read-annotations.test.ts +164 -0
  218. package/apps/studio/test/shell-importmap.test.ts +49 -0
  219. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  220. package/apps/studio/test/timeline-parse.test.ts +57 -0
  221. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  222. package/apps/studio/test/undo-stack.test.ts +33 -0
  223. package/apps/studio/test/video-asset.test.ts +5 -5
  224. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  225. package/apps/studio/text-caret.ts +239 -0
  226. package/apps/studio/tool-palette.tsx +43 -20
  227. package/apps/studio/undo-stack.ts +94 -2
  228. package/apps/studio/use-annotation-resize.tsx +4 -4
  229. package/apps/studio/use-canvas-media-drop.tsx +38 -1
  230. package/apps/studio/use-selection-set.tsx +21 -0
  231. package/apps/studio/video-comp.tsx +48 -7
  232. package/apps/studio/whats-new.json +53 -0
  233. package/cli/commands/design.mjs +34 -3
  234. package/cli/lib/gitignore-block.mjs +1 -0
  235. package/package.json +10 -10
  236. package/plugins/design/templates/_shell.html +1 -0
@@ -10,7 +10,7 @@ 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';
@@ -19,12 +19,14 @@ import type { Context } from './context.ts';
19
19
  import { type Format, isFormat, isScope, type Scope } from './exporters/index.ts';
20
20
  import { type ExportJobQueue, ExportQueueFullError } from './exporters/jobs.ts';
21
21
  import type { ActiveJsonShape } from './exporters/scope.ts';
22
+ import { createFootageStore, FOOTAGE_MAX_BYTES } from './footage-store.ts';
22
23
  import { createGitEndpoints } from './git/endpoints.ts';
23
24
  import { gitShowFile } from './git/service.ts';
24
25
  import { createGitHubEndpoints } from './github/endpoints.ts';
25
26
  import type { Inspect } from './inspect.ts';
26
27
  import { canvasSlug, writeLocator } from './locator.ts';
27
- 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';
28
30
  import { probeReadiness } from './readiness.ts';
29
31
  import { getRuntimeBundle, packageForSlug } from './runtime-bundle.ts';
30
32
  import { linkHub } from './sync/hub-link.ts';
@@ -721,6 +723,12 @@ export function createHttp(
721
723
  // `routes` map (the dual-allowlist rule), so the untrusted canvas iframe origin
722
724
  // can never reach status/commit/publish/get-latest. http.ts owns the gating;
723
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);
724
732
  const gitApi = createGitEndpoints(ctx);
725
733
  // Phase 28 (E3) — `/_api/github/*`. Same dual-allowlist rule: main-origin only,
726
734
  // and every route is token-bearing (server-held keychain token via the loopback
@@ -836,8 +844,8 @@ export function createHttp(
836
844
  // expands to so Claude can Read it. MAIN-ORIGIN ONLY: sameOriginWrite CSRF gate
837
845
  // on POST + deliberately absent from CANVAS_SAFE_API + startCanvasServer routes,
838
846
  // so the untrusted canvas iframe is 403'd. The disk caps live in
839
- // api.saveChatAttachment (magic-byte sniff / 10 MB / content-addressed name /
840
- // session write budget).
847
+ // api.saveChatAttachment (magic-byte sniff / ASSET_MAX_BYTES / content-
848
+ // addressed name / session write budget).
841
849
  //
842
850
  // GET ?name=<sha8>.<ext> serves the pasted image back to the chat feed
843
851
  // (thumbnail + lightbox). The name is regex-allowlisted in
@@ -864,9 +872,12 @@ export function createHttp(
864
872
  if (!isLoopbackHost(req.headers.get('host')))
865
873
  return new Response('local request required (DNS-rebinding guard)', { status: 403 });
866
874
  const declared = Number(req.headers.get('content-length') || '0');
867
- if (Number.isFinite(declared) && declared > 10 * 1024 * 1024) {
875
+ if (Number.isFinite(declared) && declared > ASSET_MAX_BYTES) {
868
876
  return Response.json(
869
- { ok: false, error: 'attachment exceeds the 10 MB cap' },
877
+ {
878
+ ok: false,
879
+ error: `attachment exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`,
880
+ },
870
881
  { status: 413, headers: { 'Cache-Control': 'no-store' } }
871
882
  );
872
883
  }
@@ -969,6 +980,87 @@ export function createHttp(
969
980
  return new Response('Method not allowed', { status: 405 });
970
981
  },
971
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
+
972
1064
  '/_api/canvas-source': async (req: Request) => {
973
1065
  // DDR-148 — read-only raw .tsx source for the Timeline panel's sequence/
974
1066
  // keyframe parser. GET ?file=<canvas rel or slug> → { source }.
@@ -1475,10 +1567,13 @@ export function createHttp(
1475
1567
  return new Response('cross-origin write rejected', { status: 403 });
1476
1568
  if (!isLoopbackHost(req.headers.get('host')))
1477
1569
  return new Response('local request required (DNS-rebinding guard)', { status: 403 });
1478
- const body = await readJson<{ canvas?: unknown; id?: unknown; text?: unknown }>(
1479
- req,
1480
- 16 * 1024
1481
- );
1570
+ const body = await readJson<{
1571
+ canvas?: unknown;
1572
+ id?: unknown;
1573
+ text?: unknown;
1574
+ occurrence?: unknown;
1575
+ before?: unknown;
1576
+ }>(req, 16 * 1024);
1482
1577
  if (!body) return new Response('body required', { status: 400 });
1483
1578
  const result = await api.editText(body);
1484
1579
  if (!result.ok) {
@@ -1852,9 +1947,11 @@ export function createHttp(
1852
1947
  },
1853
1948
 
1854
1949
  '/_api/insert-element': async (req: Request) => {
1855
- // Stage I — insert a synthesized div/text/image relative to `refId`. POST
1856
- // { canvas, refId, position, kind, src?, refIndex? } api.insertElementOp.
1857
- // Returns the new element's post-transpile id so the shell can select it.
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.
1858
1955
  // Whole-file undo seq. MAIN-ORIGIN ONLY (absent from both allowlists);
1859
1956
  // sameOriginWrite + loopback-Host gated. An `image` src is contained to
1860
1957
  // assets/ in the engine (no remote hotlink / ../ / scheme).
@@ -1866,6 +1963,7 @@ export function createHttp(
1866
1963
  const body = await readJson<{
1867
1964
  canvas?: unknown;
1868
1965
  refId?: unknown;
1966
+ artboardId?: unknown;
1869
1967
  position?: unknown;
1870
1968
  kind?: unknown;
1871
1969
  src?: unknown;
@@ -1961,6 +2059,24 @@ export function createHttp(
1961
2059
  );
1962
2060
  },
1963
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
+
1964
2080
  '/_api/edit-scope': async (req: Request) => {
1965
2081
  // Stage H (feature-element-editing-robustness) — the INV-3 predictability
1966
2082
  // verdict. GET ?canvas&id&rendered → { ok, scope:'local'|'shared',
@@ -2367,6 +2483,48 @@ export function createHttp(
2367
2483
  }
2368
2484
  }
2369
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
+
2370
2528
  // Fall-through: serve user repo files (designRoot + everything under repoRoot).
2371
2529
  const fp = safePathUnderRoot(req.url, ctx.paths.repoRoot);
2372
2530
  if (!fp) return new Response('Forbidden', { status: 403 });
@@ -2458,7 +2616,8 @@ export function createHttp(
2458
2616
  '/_api/git-user', // presence display name
2459
2617
  '/_api/canvas-meta', // layout/viewport sidecar (GET + PATCH)
2460
2618
  '/_api/annotations', // annotation SVG (GET + PUT) — drives the collab bridge
2461
- '/_api/asset', // Phase 23 — capped binary image upload (sniff+10MB+sha8 name+no-SVG)
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.
2462
2621
  '/_api/git-committers', // @mention autocomplete
2463
2622
  '/_api/ai', // AI-activity banner
2464
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
 
@@ -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
- var id = k === 'i' ? 'inspector' : k === 'm' ? 'comments' : k === 'e' ? 'export' : k === 'h' ? 'handoff' : null;
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) {}
@@ -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