@hraness/slopcamera 3.2.4
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/LICENSE +21 -0
- package/NOTICE.md +60 -0
- package/PRIVACY.md +46 -0
- package/README.md +311 -0
- package/SECURITY.md +21 -0
- package/apps/desktop/README.md +966 -0
- package/apps/desktop/analysis/protocol.ts +223 -0
- package/apps/desktop/application/capability-binding.ts +594 -0
- package/apps/desktop/application/context.ts +97 -0
- package/apps/desktop/application/creative-iteration.ts +1385 -0
- package/apps/desktop/application/default-registry.ts +146 -0
- package/apps/desktop/application/directing-contract.ts +218 -0
- package/apps/desktop/application/directing-plan.ts +178 -0
- package/apps/desktop/application/errors.ts +18 -0
- package/apps/desktop/application/gateway-port.ts +867 -0
- package/apps/desktop/application/html-overlay-browser-runtime.ts +687 -0
- package/apps/desktop/application/html-overlay-integrity.ts +337 -0
- package/apps/desktop/application/html-overlay-renderer.ts +45 -0
- package/apps/desktop/application/index.ts +13 -0
- package/apps/desktop/application/operation-completion-checkpoint.ts +333 -0
- package/apps/desktop/application/operation-effects.ts +94 -0
- package/apps/desktop/application/operation.ts +224 -0
- package/apps/desktop/application/operations/analysis/faces.ts +244 -0
- package/apps/desktop/application/operations/analysis/music.ts +248 -0
- package/apps/desktop/application/operations/analysis/project-auto-zooms.ts +302 -0
- package/apps/desktop/application/operations/analysis/project-inactivity.ts +381 -0
- package/apps/desktop/application/operations/analysis/scenes.ts +272 -0
- package/apps/desktop/application/operations/analysis/shared.ts +202 -0
- package/apps/desktop/application/operations/derive/edit-batch.ts +967 -0
- package/apps/desktop/application/operations/derive/follow-faces.ts +454 -0
- package/apps/desktop/application/operations/edit/bind-candidate-revision.ts +91 -0
- package/apps/desktop/application/operations/edit/create-candidate-revision.ts +377 -0
- package/apps/desktop/application/operations/edit/create-revision.ts +255 -0
- package/apps/desktop/application/operations/edit/freeze-revision.ts +182 -0
- package/apps/desktop/application/operations/gateway/image.ts +41 -0
- package/apps/desktop/application/operations/gateway/index.ts +17 -0
- package/apps/desktop/application/operations/gateway/shared.ts +122 -0
- package/apps/desktop/application/operations/gateway/speech.ts +41 -0
- package/apps/desktop/application/operations/gateway/transcription.ts +42 -0
- package/apps/desktop/application/operations/gateway/video.ts +41 -0
- package/apps/desktop/application/operations/index.ts +31 -0
- package/apps/desktop/application/operations/iteration/create-candidate.ts +240 -0
- package/apps/desktop/application/operations/iteration/create-matrix.ts +117 -0
- package/apps/desktop/application/operations/iteration/select.ts +155 -0
- package/apps/desktop/application/operations/iteration/shared.ts +250 -0
- package/apps/desktop/application/operations/media/audio-effects.ts +376 -0
- package/apps/desktop/application/operations/media/binding.ts +79 -0
- package/apps/desktop/application/operations/media/capabilities.ts +112 -0
- package/apps/desktop/application/operations/media/color-grade.ts +343 -0
- package/apps/desktop/application/operations/media/html-overlay.ts +802 -0
- package/apps/desktop/application/operations/media/index.ts +8 -0
- package/apps/desktop/application/operations/media/ingest.ts +303 -0
- package/apps/desktop/application/operations/media/overlay.ts +729 -0
- package/apps/desktop/application/operations/media/shared.ts +845 -0
- package/apps/desktop/application/operations/media/transform-platform.ts +120 -0
- package/apps/desktop/application/operations/project/commit-edits.ts +2054 -0
- package/apps/desktop/application/operations/project/promote-selection.ts +495 -0
- package/apps/desktop/application/operations/project/snapshot.ts +95 -0
- package/apps/desktop/application/operations/recording/index.ts +5 -0
- package/apps/desktop/application/operations/recording/pause.ts +34 -0
- package/apps/desktop/application/operations/recording/resume.ts +34 -0
- package/apps/desktop/application/operations/recording/shared.ts +199 -0
- package/apps/desktop/application/operations/recording/start.ts +169 -0
- package/apps/desktop/application/operations/recording/stop.ts +34 -0
- package/apps/desktop/application/operations/render/bind-candidate-output.ts +324 -0
- package/apps/desktop/application/operations/render/materialize-selection.ts +322 -0
- package/apps/desktop/application/operations/render/project-caption-plan.ts +287 -0
- package/apps/desktop/application/operations/render/project-plan.ts +939 -0
- package/apps/desktop/application/operations/render/project-spatial-receipt.ts +70 -0
- package/apps/desktop/application/operations/render/project.ts +1922 -0
- package/apps/desktop/application/operations/shared.ts +67 -0
- package/apps/desktop/application/operations/slopcamera-portable.ts +1188 -0
- package/apps/desktop/application/operations/slopcamera-visuals.ts +784 -0
- package/apps/desktop/application/operations/spatial-project.ts +209 -0
- package/apps/desktop/application/operations/spatial-render.ts +415 -0
- package/apps/desktop/application/operations/spatial-scene.ts +97 -0
- package/apps/desktop/application/operations/studio.ts +143 -0
- package/apps/desktop/application/output-publication-lease.ts +163 -0
- package/apps/desktop/application/project-publication-lease.ts +233 -0
- package/apps/desktop/application/project-store.ts +196 -0
- package/apps/desktop/application/receipts.ts +533 -0
- package/apps/desktop/application/registry.ts +182 -0
- package/apps/desktop/application/spatial-assets.ts +675 -0
- package/apps/desktop/application/spatial-compositor-cadence.ts +23 -0
- package/apps/desktop/application/spatial-project-authority.ts +35 -0
- package/apps/desktop/application/spatial-project-lease.ts +126 -0
- package/apps/desktop/application/spatial-project-render-preparation.ts +225 -0
- package/apps/desktop/application/spatial-project-store.ts +286 -0
- package/apps/desktop/application/spatial-render-projection.ts +201 -0
- package/apps/desktop/application/spatial-render.ts +677 -0
- package/apps/desktop/application/spatial-spz.ts +44 -0
- package/apps/desktop/application/spatial-world-import.ts +135 -0
- package/apps/desktop/application/spatial-world-provenance.ts +46 -0
- package/apps/desktop/application/studio-port.ts +58 -0
- package/apps/desktop/application/verified-receipt-reconciliation.ts +1574 -0
- package/apps/desktop/capture/protocol.ts +689 -0
- package/apps/desktop/cli/analyzer.ts +406 -0
- package/apps/desktop/cli/args.ts +3181 -0
- package/apps/desktop/cli/asset-ingest.ts +747 -0
- package/apps/desktop/cli/atomic-render-effects.ts +210 -0
- package/apps/desktop/cli/atomic-render-platform.ts +205 -0
- package/apps/desktop/cli/atomic-render.ts +50 -0
- package/apps/desktop/cli/audio-analysis.ts +252 -0
- package/apps/desktop/cli/bounded-map.ts +39 -0
- package/apps/desktop/cli/bundle-service.ts +256 -0
- package/apps/desktop/cli/capabilities.ts +212 -0
- package/apps/desktop/cli/capture-bundle.ts +1117 -0
- package/apps/desktop/cli/command-host-resources.ts +251 -0
- package/apps/desktop/cli/commands.ts +6836 -0
- package/apps/desktop/cli/directing-blob.ts +291 -0
- package/apps/desktop/cli/directing-media.ts +517 -0
- package/apps/desktop/cli/directing-quote.ts +89 -0
- package/apps/desktop/cli/directing-service.ts +288 -0
- package/apps/desktop/cli/emoji-assets.ts +538 -0
- package/apps/desktop/cli/errors.ts +29 -0
- package/apps/desktop/cli/face-analysis-service.ts +940 -0
- package/apps/desktop/cli/filter-script.ts +76 -0
- package/apps/desktop/cli/gateway-application-port.ts +1142 -0
- package/apps/desktop/cli/gateway-credential.ts +81 -0
- package/apps/desktop/cli/gateway-media-artifacts.ts +723 -0
- package/apps/desktop/cli/gateway-media-catalog.ts +922 -0
- package/apps/desktop/cli/gateway-media-service.ts +3106 -0
- package/apps/desktop/cli/gateway-media-signature.ts +122 -0
- package/apps/desktop/cli/gateway-provider-options.ts +562 -0
- package/apps/desktop/cli/gateway-scene-provider.ts +391 -0
- package/apps/desktop/cli/help.ts +501 -0
- package/apps/desktop/cli/html-overlay-gpu.ts +97 -0
- package/apps/desktop/cli/html-overlay-renderer.ts +2478 -0
- package/apps/desktop/cli/io.ts +290 -0
- package/apps/desktop/cli/main.ts +123 -0
- package/apps/desktop/cli/media-effects-service.ts +885 -0
- package/apps/desktop/cli/media-ingest-model.ts +364 -0
- package/apps/desktop/cli/media-ingest-platform.ts +227 -0
- package/apps/desktop/cli/media-ingest-program.ts +200 -0
- package/apps/desktop/cli/media-ingest.ts +30 -0
- package/apps/desktop/cli/music-analysis-service.ts +434 -0
- package/apps/desktop/cli/mutation-lock.ts +530 -0
- package/apps/desktop/cli/native-assets.d.ts +12 -0
- package/apps/desktop/cli/native-media-runtime.macos.ts +7 -0
- package/apps/desktop/cli/overlay-playback.ts +177 -0
- package/apps/desktop/cli/paths.ts +265 -0
- package/apps/desktop/cli/portable-surface.ts +242 -0
- package/apps/desktop/cli/project-camera-receipt.ts +112 -0
- package/apps/desktop/cli/project-face-camera.ts +394 -0
- package/apps/desktop/cli/project-inactivity-service.ts +552 -0
- package/apps/desktop/cli/project-media-integrity.ts +121 -0
- package/apps/desktop/cli/project-renderer.ts +1526 -0
- package/apps/desktop/cli/project-service.ts +519 -0
- package/apps/desktop/cli/project-state-transaction.ts +520 -0
- package/apps/desktop/cli/query-limits.ts +1 -0
- package/apps/desktop/cli/recording-controller.ts +1567 -0
- package/apps/desktop/cli/recording-daemon.ts +931 -0
- package/apps/desktop/cli/recording-ref.ts +56 -0
- package/apps/desktop/cli/renamed-environment.ts +6 -0
- package/apps/desktop/cli/renderer.ts +1439 -0
- package/apps/desktop/cli/run-cli-test-helper.ts +60 -0
- package/apps/desktop/cli/scene-analysis-service.ts +879 -0
- package/apps/desktop/cli/spatial-compositor-verifier.ts +213 -0
- package/apps/desktop/cli/spatial-project-service.ts +90 -0
- package/apps/desktop/cli/spatial-scene-service.ts +105 -0
- package/apps/desktop/cli/spatial-world-service.ts +39 -0
- package/apps/desktop/cli/speech-analysis-service.ts +682 -0
- package/apps/desktop/cli/studio-assemble.ts +58 -0
- package/apps/desktop/cli/studio-command.ts +60 -0
- package/apps/desktop/cli/studio-custody.ts +60 -0
- package/apps/desktop/cli/studio-encode.ts +216 -0
- package/apps/desktop/cli/studio-exr.ts +52 -0
- package/apps/desktop/cli/studio-files.ts +179 -0
- package/apps/desktop/cli/studio-output-validation.ts +162 -0
- package/apps/desktop/cli/studio-process.ts +145 -0
- package/apps/desktop/cli/studio-runtime.ts +77 -0
- package/apps/desktop/cli/studio-scaffold.ts +70 -0
- package/apps/desktop/cli/studio-service.ts +301 -0
- package/apps/desktop/cli/studio-spatial-asset.ts +248 -0
- package/apps/desktop/cli/studio-template-names.ts +3 -0
- package/apps/desktop/cli/studio-workflow.ts +19 -0
- package/apps/desktop/cli/time.ts +25 -0
- package/apps/desktop/cli/workflow-code.ts +584 -0
- package/apps/desktop/cli/workflow-runs.ts +576 -0
- package/apps/desktop/code/advanced.ts +139 -0
- package/apps/desktop/code/application-node-planner.ts +1288 -0
- package/apps/desktop/code/compiler.ts +329 -0
- package/apps/desktop/code/contracts.ts +13 -0
- package/apps/desktop/code/creative-recipes.ts +94 -0
- package/apps/desktop/code/define-workflow.ts +115 -0
- package/apps/desktop/code/editing.ts +284 -0
- package/apps/desktop/code/file-candidate-provenance.ts +420 -0
- package/apps/desktop/code/file-candidate.ts +69 -0
- package/apps/desktop/code/graph-builder.ts +10 -0
- package/apps/desktop/code/host-resource-policy.ts +55 -0
- package/apps/desktop/code/plan-contracts.ts +159 -0
- package/apps/desktop/code/planning.ts +361 -0
- package/apps/desktop/code/public.ts +134 -0
- package/apps/desktop/code/reference-projection.ts +62 -0
- package/apps/desktop/code/run-contracts.ts +466 -0
- package/apps/desktop/code/run-store.ts +2478 -0
- package/apps/desktop/code/runtime-identity.ts +328 -0
- package/apps/desktop/code/scheduler.ts +3067 -0
- package/apps/desktop/code/semantic-builder.ts +1436 -0
- package/apps/desktop/code/source-bundle.ts +1641 -0
- package/apps/desktop/code/source-typecheck.ts +211 -0
- package/apps/desktop/code/testing.ts +79 -0
- package/apps/desktop/code/worker-client.ts +1675 -0
- package/apps/desktop/code/worker-effects.ts +141 -0
- package/apps/desktop/code/worker-entry.ts +435 -0
- package/apps/desktop/code/worker-lease-guardian.ts +179 -0
- package/apps/desktop/code/worker-process-identity.ts +484 -0
- package/apps/desktop/code/worker-protocol.ts +211 -0
- package/apps/desktop/code/workflow-effects.ts +132 -0
- package/apps/desktop/contracts/analysis.ts +949 -0
- package/apps/desktop/contracts/edit.ts +576 -0
- package/apps/desktop/contracts/index.ts +9 -0
- package/apps/desktop/contracts/media-effects.ts +139 -0
- package/apps/desktop/contracts/project-render.ts +644 -0
- package/apps/desktop/contracts/project.ts +740 -0
- package/apps/desktop/contracts/recording.ts +1434 -0
- package/apps/desktop/contracts/render.ts +334 -0
- package/apps/desktop/contracts/runtime.ts +155 -0
- package/apps/desktop/contracts/spatial-compositor.ts +58 -0
- package/apps/desktop/contracts/spatial-project.ts +159 -0
- package/apps/desktop/contracts/spatial-world.ts +48 -0
- package/apps/desktop/contracts/video-effects.ts +120 -0
- package/apps/desktop/core/alignment-apply.ts +400 -0
- package/apps/desktop/core/audio-alignment.ts +466 -0
- package/apps/desktop/core/auto-zoom.ts +209 -0
- package/apps/desktop/core/canonical-json.ts +14 -0
- package/apps/desktop/core/emoji.ts +210 -0
- package/apps/desktop/core/events.ts +409 -0
- package/apps/desktop/core/face-framing.ts +533 -0
- package/apps/desktop/core/face-tracks.ts +326 -0
- package/apps/desktop/core/inactivity.ts +137 -0
- package/apps/desktop/core/index.ts +30 -0
- package/apps/desktop/core/intervals.ts +123 -0
- package/apps/desktop/core/montage.ts +236 -0
- package/apps/desktop/core/music-analysis.ts +518 -0
- package/apps/desktop/core/plan.ts +244 -0
- package/apps/desktop/core/project-auto-zoom.ts +75 -0
- package/apps/desktop/core/project-camera-spatial.ts +676 -0
- package/apps/desktop/core/project-camera.ts +403 -0
- package/apps/desktop/core/project-captions.ts +888 -0
- package/apps/desktop/core/project-inactivity.ts +125 -0
- package/apps/desktop/core/project-metadata.ts +927 -0
- package/apps/desktop/core/project-plan.ts +375 -0
- package/apps/desktop/core/project-render-plan.ts +303 -0
- package/apps/desktop/core/project-time.ts +469 -0
- package/apps/desktop/core/render-plan.ts +442 -0
- package/apps/desktop/core/scene-cache.ts +174 -0
- package/apps/desktop/core/scene-sampling.ts +448 -0
- package/apps/desktop/core/spatial-compositor.ts +22 -0
- package/apps/desktop/core/spatial-durability.ts +99 -0
- package/apps/desktop/core/spatial-project.ts +169 -0
- package/apps/desktop/core/spatial-shot-clock.ts +32 -0
- package/apps/desktop/core/speech-analysis.ts +333 -0
- package/apps/desktop/core/storage.ts +970 -0
- package/apps/desktop/core/time-map.ts +91 -0
- package/apps/desktop/core/time.ts +61 -0
- package/apps/desktop/core/video-effects.ts +660 -0
- package/apps/desktop/dist/cli/NebulaSans-Bold-26se8aek.otf +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Book-5ax05zvn.woff2 +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Book-8cenzchw.otf +0 -0
- package/apps/desktop/dist/cli/main.js +5086 -0
- package/apps/desktop/html-overlay/catalog.ts +171 -0
- package/apps/desktop/html-overlay/contracts.ts +372 -0
- package/apps/desktop/html-overlay/execution-profile.ts +77 -0
- package/apps/desktop/html-overlay/index.ts +98 -0
- package/apps/desktop/html-overlay/libraries.ts +263 -0
- package/apps/desktop/html-overlay/random.ts +49 -0
- package/apps/desktop/html-overlay/runtime.ts +813 -0
- package/apps/desktop/html-overlay/scaffolds.ts +792 -0
- package/apps/desktop/html-overlay/spark-runtime.ts +88 -0
- package/apps/desktop/html-overlay/spatial-splat-runtime.ts +60 -0
- package/apps/desktop/html-overlay/spatial.ts +720 -0
- package/apps/desktop/studio/assets/README.md +39 -0
- package/apps/desktop/studio/assets/contracts.ts +129 -0
- package/apps/desktop/studio/assets/network.ts +64 -0
- package/apps/desktop/studio/assets/poly-haven.ts +148 -0
- package/apps/desktop/studio/drivers/blender_driver.py +466 -0
- package/apps/desktop/studio/drivers/cadquery_driver.py +176 -0
- package/apps/desktop/studio/education/driver.py +308 -0
- package/apps/desktop/studio/education/lesson.py +180 -0
- package/apps/desktop/studio/education/toolkit.py +249 -0
- package/apps/desktop/workflows/chaptered-demo.ts +48 -0
- package/apps/desktop/workflows/creative-iteration.ts +107 -0
- package/apps/desktop/workflows/creative-selection.ts +92 -0
- package/apps/desktop/workflows/fragments.ts +486 -0
- package/apps/desktop/workflows/index.ts +93 -0
- package/apps/desktop/workflows/polished-screen-demo.ts +128 -0
- package/apps/desktop/workflows/scene-direction.ts +12 -0
- package/apps/desktop/workflows/social-variants.ts +75 -0
- package/apps/desktop/workflows/talking-head-cleanup.ts +53 -0
- package/dist/NebulaSans-Bold-26se8aek.otf +0 -0
- package/dist/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
- package/dist/NebulaSans-Book-5ax05zvn.woff2 +0 -0
- package/dist/NebulaSans-Book-8cenzchw.otf +0 -0
- package/dist/cli.js +479 -0
- package/dist/code/advanced.js +270 -0
- package/dist/code/index.js +2240 -0
- package/dist/generate.js +24 -0
- package/dist/host-resources.js +26 -0
- package/dist/index-42zsesc1.js +2257 -0
- package/dist/index-7308egqr.js +48 -0
- package/dist/index-8txs6fkn.js +712 -0
- package/dist/index-mcy8z0br.js +192 -0
- package/dist/index-p63wavx0.js +2029 -0
- package/dist/index-r7gdhmsp.js +424 -0
- package/dist/index-sh6xbav6.js +1219 -0
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index-z7239b4h.js +2363 -0
- package/dist/index-zfeg9hmw.js +2013 -0
- package/dist/index.js +241 -0
- package/dist/operations.js +29 -0
- package/dist/skill-install-dh0ntqba.js +10 -0
- package/dist/vectorize/worker.js +142 -0
- package/dist/workflow.js +16 -0
- package/docs/README.md +38 -0
- package/docs/studio.md +215 -0
- package/examples/studio/blender/README.md +26 -0
- package/examples/studio/blender/character.py +152 -0
- package/examples/studio/blender/cloth.py +101 -0
- package/examples/studio/blender/color_chart.py +29 -0
- package/examples/studio/blender/fluid.py +78 -0
- package/examples/studio/blender/import_model.py +74 -0
- package/examples/studio/blender/product.py +53 -0
- package/examples/studio/blender/shaded_street.py +481 -0
- package/examples/studio/blender/studio_scene.py +147 -0
- package/examples/studio/cadquery/README.md +13 -0
- package/examples/studio/cadquery/bracket.py +36 -0
- package/examples/studio/cadquery/import_step.py +12 -0
- package/examples/studio/education/README.md +46 -0
- package/examples/studio/education/lesson.json +99 -0
- package/examples/studio/education/scene.py +7 -0
- package/examples/studio/hybrid-scene.ts +96 -0
- package/examples/studio/native-workflow.ts +14 -0
- package/examples/studio/vgpu/README.md +64 -0
- package/examples/studio/vgpu/heat-field.mjs +290 -0
- package/package.json +304 -0
- package/packages/scene/src/index.ts +1 -0
- package/packages/scene/src/scene.ts +215 -0
- package/schema/diagram.schema.json +334 -0
- package/skills/slopcamera/SKILL.md +45 -0
- package/skills/slopcamera/agents/openai.yaml +4 -0
- package/skills/slopcamera/references/customization.md +77 -0
- package/skills/slopcamera/references/diagrams.md +153 -0
- package/skills/slopcamera/references/directed-scenes.md +42 -0
- package/skills/slopcamera/references/directing-video.md +18 -0
- package/skills/slopcamera/references/educational-video.md +26 -0
- package/skills/slopcamera/references/gateway-media.md +134 -0
- package/skills/slopcamera/references/install.md +62 -0
- package/skills/slopcamera/references/native-studio.md +31 -0
- package/skills/slopcamera/references/reference-led-3d.md +140 -0
- package/skills/slopcamera/references/rubber-stamp-examples/poster-example-1.jpg +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/poster-example-2.jpg +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-1.png +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-2.png +0 -0
- package/skills/slopcamera/references/rubber-stamp-field-notes.md +144 -0
- package/skills/slopcamera/references/vectorization.md +29 -0
- package/skills/slopcamera/references/video-projects.md +187 -0
- package/skills/slopcamera/references/visual-communication.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +45 -0
- package/skills/slopcamera/scripts/compose-rubber-stamp-field-note.ts +229 -0
- package/src/artifacts.ts +106 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +21 -0
- package/src/assets.d.ts +9 -0
- package/src/cli.ts +601 -0
- package/src/cloud-errors.ts +26 -0
- package/src/code/advanced.ts +10 -0
- package/src/code/boundary.ts +21 -0
- package/src/code/canonical-json.ts +129 -0
- package/src/code/compiler.ts +953 -0
- package/src/code/contracts.ts +576 -0
- package/src/code/define-workflow.ts +334 -0
- package/src/code/errors.ts +41 -0
- package/src/code/graph-builder.ts +945 -0
- package/src/code/index.ts +121 -0
- package/src/code/json-snapshot.ts +899 -0
- package/src/code/json-utf8.ts +88 -0
- package/src/code/portable-builder.ts +136 -0
- package/src/code/projection.ts +378 -0
- package/src/code/public-operations.ts +450 -0
- package/src/code/runtime.ts +580 -0
- package/src/code/sha256.ts +36 -0
- package/src/config.ts +180 -0
- package/src/desktop.ts +253 -0
- package/src/fs.ts +10 -0
- package/src/generate.ts +704 -0
- package/src/host-resource-posix.ts +161 -0
- package/src/host-resources.ts +1749 -0
- package/src/icons.ts +58 -0
- package/src/index.ts +200 -0
- package/src/label-layout.ts +130 -0
- package/src/layout.ts +413 -0
- package/src/lint.ts +127 -0
- package/src/mcp/boundary.ts +354 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/server.ts +272 -0
- package/src/mcp/tools.ts +963 -0
- package/src/mcp/types.ts +71 -0
- package/src/operations.ts +775 -0
- package/src/parse.ts +861 -0
- package/src/process-environment.ts +17 -0
- package/src/render.ts +437 -0
- package/src/skill-install.ts +49 -0
- package/src/spatial-scene/authoring.ts +27 -0
- package/src/spatial-scene/camera-track.ts +83 -0
- package/src/spatial-scene/contracts.ts +277 -0
- package/src/spatial-scene/evaluate.ts +145 -0
- package/src/spatial-scene/gltf.ts +558 -0
- package/src/spatial-scene/identity.ts +260 -0
- package/src/spatial-scene/index.ts +10 -0
- package/src/spatial-scene/inspect.ts +68 -0
- package/src/spatial-scene/math.ts +280 -0
- package/src/spatial-scene/patch.ts +131 -0
- package/src/spatial-scene/time.ts +53 -0
- package/src/studio/contracts.ts +129 -0
- package/src/studio/index.ts +2 -0
- package/src/studio/plan.ts +156 -0
- package/src/studio/shared.ts +41 -0
- package/src/theme.ts +68 -0
- package/src/tldr.ts +474 -0
- package/src/types.ts +212 -0
- package/src/vectorize/archive.ts +101 -0
- package/src/vectorize/command.ts +797 -0
- package/src/vectorize/index.ts +27 -0
- package/src/vectorize/limits.ts +61 -0
- package/src/vectorize/metrics.ts +351 -0
- package/src/vectorize/pixels.ts +323 -0
- package/src/vectorize/supervisor.ts +339 -0
- package/src/vectorize/svg.ts +188 -0
- package/src/vectorize/tool.ts +542 -0
- package/src/vectorize/types.ts +113 -0
- package/src/vectorize/vectorize.ts +634 -0
- package/src/vectorize/worker-protocol.ts +40 -0
- package/src/vectorize/worker.ts +209 -0
- package/src/version.ts +1 -0
- package/src/workflow.ts +409 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants, type Stats } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
type FileHandle,
|
|
5
|
+
link,
|
|
6
|
+
lstat,
|
|
7
|
+
open,
|
|
8
|
+
realpath,
|
|
9
|
+
rm,
|
|
10
|
+
} from "node:fs/promises";
|
|
11
|
+
import {
|
|
12
|
+
dirname,
|
|
13
|
+
isAbsolute,
|
|
14
|
+
join,
|
|
15
|
+
relative,
|
|
16
|
+
resolve,
|
|
17
|
+
} from "node:path";
|
|
18
|
+
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
RepositoryRelativePathSchema,
|
|
23
|
+
Sha256Schema,
|
|
24
|
+
} from "../../../contracts";
|
|
25
|
+
import { canonicalJson } from "../../../core/canonical-json";
|
|
26
|
+
import type {
|
|
27
|
+
ApplicationContext,
|
|
28
|
+
ApplicationProcessRunOptions,
|
|
29
|
+
ApplicationProcessRunResult,
|
|
30
|
+
ApplicationProcessRunner,
|
|
31
|
+
} from "../../context";
|
|
32
|
+
import { ApplicationError } from "../../errors";
|
|
33
|
+
import type { OperationExecutionContext } from "../../operation";
|
|
34
|
+
import {
|
|
35
|
+
ensurePhysicalPrivateDirectoryWithin,
|
|
36
|
+
ensurePrivateDirectory,
|
|
37
|
+
} from "../../../cli/paths";
|
|
38
|
+
import { throwIfAborted } from "../shared";
|
|
39
|
+
|
|
40
|
+
export const MAXIMUM_MEDIA_INGEST_INPUT_BYTES = 4 * 1024 * 1024 * 1024 * 1024;
|
|
41
|
+
export const MAXIMUM_MEDIA_EFFECT_INPUT_BYTES = 512 * 1024 * 1024 * 1024;
|
|
42
|
+
|
|
43
|
+
export const MediaArtifactReferenceSchema = z.strictObject({
|
|
44
|
+
bytes: z.number().int().safe().positive(),
|
|
45
|
+
path: RepositoryRelativePathSchema,
|
|
46
|
+
sha256: Sha256Schema,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The path-only shape is the progressive authoring surface. A host planner
|
|
51
|
+
* may replace it with the integrity-bound shape before hashing a node plan.
|
|
52
|
+
*/
|
|
53
|
+
export const MediaArtifactRequestSchema = z.union([
|
|
54
|
+
z.strictObject({
|
|
55
|
+
path: RepositoryRelativePathSchema,
|
|
56
|
+
}),
|
|
57
|
+
MediaArtifactReferenceSchema,
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
export type MediaArtifactReference = z.infer<typeof MediaArtifactReferenceSchema>;
|
|
61
|
+
export type MediaArtifactRequest = z.infer<typeof MediaArtifactRequestSchema>;
|
|
62
|
+
|
|
63
|
+
export interface BoundRepositoryMedia {
|
|
64
|
+
readonly absolutePath: string;
|
|
65
|
+
readonly artifact: MediaArtifactReference;
|
|
66
|
+
readonly expectedInput: {
|
|
67
|
+
readonly bytes: number;
|
|
68
|
+
readonly device: number;
|
|
69
|
+
readonly inode: number;
|
|
70
|
+
readonly modifiedAtMs: number;
|
|
71
|
+
readonly sha256: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface LoadedRepositoryMedia extends BoundRepositoryMedia {
|
|
76
|
+
readonly data: Uint8Array;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface PhysicalRepositoryEntry {
|
|
80
|
+
readonly absolutePath: string;
|
|
81
|
+
readonly device: number;
|
|
82
|
+
readonly inode: number;
|
|
83
|
+
readonly kind: "directory" | "file";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface PhysicalRepositoryPath {
|
|
87
|
+
readonly absolutePath: string;
|
|
88
|
+
readonly entries: readonly PhysicalRepositoryEntry[];
|
|
89
|
+
readonly leaf: Stats;
|
|
90
|
+
readonly lexicalRoot: string;
|
|
91
|
+
readonly path: string;
|
|
92
|
+
readonly repositoryRoot: string;
|
|
93
|
+
readonly root: PhysicalRepositoryEntry;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isWithin(root: string, candidate: string): boolean {
|
|
97
|
+
const fromRoot = relative(root, candidate);
|
|
98
|
+
return fromRoot === ""
|
|
99
|
+
|| (!fromRoot.startsWith("..") && !isAbsolute(fromRoot));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function noEntry(error: unknown): boolean {
|
|
103
|
+
return error instanceof Error
|
|
104
|
+
&& "code" in error
|
|
105
|
+
&& error.code === "ENOENT";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function samePhysicalEntry(
|
|
109
|
+
details: Pick<Stats, "dev" | "ino">,
|
|
110
|
+
identity: Pick<PhysicalRepositoryEntry, "device" | "inode">,
|
|
111
|
+
): boolean {
|
|
112
|
+
return details.dev === identity.device && details.ino === identity.inode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function physicalEntry(
|
|
116
|
+
absolutePath: string,
|
|
117
|
+
details: Stats,
|
|
118
|
+
kind: PhysicalRepositoryEntry["kind"],
|
|
119
|
+
): PhysicalRepositoryEntry {
|
|
120
|
+
return {
|
|
121
|
+
absolutePath,
|
|
122
|
+
device: details.dev,
|
|
123
|
+
inode: details.ino,
|
|
124
|
+
kind,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function repositoryMediaNotFound(path: string): ApplicationError {
|
|
129
|
+
return new ApplicationError(
|
|
130
|
+
"not-found",
|
|
131
|
+
`Repository media does not exist: ${path}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function existingRealpath(path: string, requestedPath: string): Promise<string> {
|
|
136
|
+
return await realpath(path).catch((error: unknown) => {
|
|
137
|
+
if (noEntry(error)) throw repositoryMediaNotFound(requestedPath);
|
|
138
|
+
throw error;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function syncDirectory(path: string): Promise<void> {
|
|
143
|
+
const directory = await open(
|
|
144
|
+
path,
|
|
145
|
+
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0),
|
|
146
|
+
);
|
|
147
|
+
try {
|
|
148
|
+
await directory.sync();
|
|
149
|
+
} finally {
|
|
150
|
+
await directory.close();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function readAndHashOpenFile(
|
|
155
|
+
handle: FileHandle,
|
|
156
|
+
bytes: number,
|
|
157
|
+
signal: AbortSignal,
|
|
158
|
+
loadBytes: boolean,
|
|
159
|
+
): Promise<Readonly<{
|
|
160
|
+
readonly data?: Uint8Array;
|
|
161
|
+
readonly sha256: string;
|
|
162
|
+
}>> {
|
|
163
|
+
const hash = createHash("sha256");
|
|
164
|
+
const data = loadBytes ? new Uint8Array(bytes) : undefined;
|
|
165
|
+
const buffer = data ?? Buffer.allocUnsafe(1024 * 1024);
|
|
166
|
+
let offset = 0;
|
|
167
|
+
while (offset < bytes) {
|
|
168
|
+
throwIfAborted(signal);
|
|
169
|
+
const bufferOffset = data === undefined ? 0 : offset;
|
|
170
|
+
const result = await handle.read(
|
|
171
|
+
buffer,
|
|
172
|
+
bufferOffset,
|
|
173
|
+
Math.min(1024 * 1024, bytes - offset),
|
|
174
|
+
offset,
|
|
175
|
+
);
|
|
176
|
+
if (result.bytesRead === 0) break;
|
|
177
|
+
hash.update(
|
|
178
|
+
buffer.subarray(bufferOffset, bufferOffset + result.bytesRead),
|
|
179
|
+
);
|
|
180
|
+
offset += result.bytesRead;
|
|
181
|
+
}
|
|
182
|
+
if (offset !== bytes) {
|
|
183
|
+
throw new ApplicationError(
|
|
184
|
+
"conflict",
|
|
185
|
+
"Media ended while its integrity was being recorded.",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
...(data === undefined ? {} : { data }),
|
|
190
|
+
sha256: hash.digest("hex"),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function physicalRepositoryPath(
|
|
195
|
+
application: ApplicationContext,
|
|
196
|
+
requestedPath: string,
|
|
197
|
+
): Promise<PhysicalRepositoryPath> {
|
|
198
|
+
const path = RepositoryRelativePathSchema.parse(requestedPath);
|
|
199
|
+
const lexicalRoot = resolve(application.paths.repositoryRoot);
|
|
200
|
+
const rootDetails = await lstat(lexicalRoot);
|
|
201
|
+
if (rootDetails.isSymbolicLink() || !rootDetails.isDirectory()) {
|
|
202
|
+
throw new ApplicationError(
|
|
203
|
+
"unsafe-path",
|
|
204
|
+
"The repository root must be a physical directory.",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const repositoryRoot = await realpath(lexicalRoot);
|
|
208
|
+
const physicalRootDetails = await lstat(repositoryRoot);
|
|
209
|
+
if (
|
|
210
|
+
physicalRootDetails.isSymbolicLink()
|
|
211
|
+
|| !physicalRootDetails.isDirectory()
|
|
212
|
+
|| !samePhysicalEntry(physicalRootDetails, {
|
|
213
|
+
device: rootDetails.dev,
|
|
214
|
+
inode: rootDetails.ino,
|
|
215
|
+
})
|
|
216
|
+
) {
|
|
217
|
+
throw new ApplicationError(
|
|
218
|
+
"unsafe-path",
|
|
219
|
+
"The repository root changed while its physical identity was resolved.",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const root = physicalEntry(repositoryRoot, physicalRootDetails, "directory");
|
|
223
|
+
const candidate = resolve(repositoryRoot, path);
|
|
224
|
+
if (!isWithin(repositoryRoot, candidate) || candidate === repositoryRoot) {
|
|
225
|
+
throw new ApplicationError(
|
|
226
|
+
"unsafe-path",
|
|
227
|
+
`Repository media escaped its repository: ${path}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const absolutePath = await existingRealpath(candidate, path);
|
|
231
|
+
if (
|
|
232
|
+
!isWithin(repositoryRoot, absolutePath)
|
|
233
|
+
|| absolutePath === repositoryRoot
|
|
234
|
+
|| resolve(absolutePath) !== candidate
|
|
235
|
+
) {
|
|
236
|
+
throw new ApplicationError(
|
|
237
|
+
"unsafe-path",
|
|
238
|
+
`Repository media paths must not traverse symlinks: ${path}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
let current = repositoryRoot;
|
|
242
|
+
const parts = path.split("/");
|
|
243
|
+
const entries: PhysicalRepositoryEntry[] = [];
|
|
244
|
+
let leaf: Stats | undefined;
|
|
245
|
+
for (const [index, part] of parts.entries()) {
|
|
246
|
+
current = join(current, part);
|
|
247
|
+
const details = await lstat(current).catch((error: unknown) => {
|
|
248
|
+
if (noEntry(error)) throw repositoryMediaNotFound(path);
|
|
249
|
+
throw error;
|
|
250
|
+
});
|
|
251
|
+
if (details.isSymbolicLink()) {
|
|
252
|
+
throw new ApplicationError(
|
|
253
|
+
"unsafe-path",
|
|
254
|
+
`Repository media paths must not traverse symlinks: ${path}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const isLeaf = index === parts.length - 1;
|
|
258
|
+
if ((!isLeaf && !details.isDirectory()) || (isLeaf && !details.isFile())) {
|
|
259
|
+
throw new ApplicationError(
|
|
260
|
+
"unsafe-path",
|
|
261
|
+
`Repository media must resolve to a physical regular file: ${path}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
entries.push(physicalEntry(
|
|
265
|
+
current,
|
|
266
|
+
details,
|
|
267
|
+
isLeaf ? "file" : "directory",
|
|
268
|
+
));
|
|
269
|
+
if (isLeaf) leaf = details;
|
|
270
|
+
}
|
|
271
|
+
const confirmedPath = await existingRealpath(candidate, path);
|
|
272
|
+
if (confirmedPath !== absolutePath || current !== absolutePath) {
|
|
273
|
+
throw new ApplicationError(
|
|
274
|
+
"conflict",
|
|
275
|
+
"Repository media changed while its physical path was being resolved.",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (leaf === undefined) {
|
|
279
|
+
throw new ApplicationError(
|
|
280
|
+
"internal",
|
|
281
|
+
"Repository media resolution produced no file.",
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
absolutePath,
|
|
286
|
+
entries,
|
|
287
|
+
leaf,
|
|
288
|
+
lexicalRoot,
|
|
289
|
+
path,
|
|
290
|
+
repositoryRoot,
|
|
291
|
+
root,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function assertPhysicalRepositoryPath(
|
|
296
|
+
identity: PhysicalRepositoryPath,
|
|
297
|
+
opened: Stats,
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
const rootDetails = await lstat(identity.lexicalRoot).catch(
|
|
300
|
+
(error: unknown) => {
|
|
301
|
+
if (noEntry(error)) {
|
|
302
|
+
throw new ApplicationError(
|
|
303
|
+
"conflict",
|
|
304
|
+
"The repository root changed while media was being verified.",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
if (
|
|
311
|
+
rootDetails.isSymbolicLink()
|
|
312
|
+
|| !rootDetails.isDirectory()
|
|
313
|
+
|| !samePhysicalEntry(rootDetails, identity.root)
|
|
314
|
+
) {
|
|
315
|
+
throw new ApplicationError(
|
|
316
|
+
"conflict",
|
|
317
|
+
"The repository root changed while media was being verified.",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const [repositoryRoot, absolutePath] = await Promise.all([
|
|
321
|
+
realpath(identity.lexicalRoot),
|
|
322
|
+
realpath(resolve(identity.repositoryRoot, identity.path)),
|
|
323
|
+
]).catch(() => {
|
|
324
|
+
throw new ApplicationError(
|
|
325
|
+
"conflict",
|
|
326
|
+
"Repository media changed while its physical identity was being verified.",
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
if (
|
|
330
|
+
repositoryRoot !== identity.repositoryRoot
|
|
331
|
+
|| absolutePath !== identity.absolutePath
|
|
332
|
+
) {
|
|
333
|
+
throw new ApplicationError(
|
|
334
|
+
"conflict",
|
|
335
|
+
"Repository media changed while its physical identity was being verified.",
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
for (const entry of identity.entries) {
|
|
339
|
+
const details = await lstat(entry.absolutePath).catch(() => {
|
|
340
|
+
throw new ApplicationError(
|
|
341
|
+
"conflict",
|
|
342
|
+
"Repository media changed while its physical identity was being verified.",
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
if (
|
|
346
|
+
details.isSymbolicLink()
|
|
347
|
+
|| (entry.kind === "directory" && !details.isDirectory())
|
|
348
|
+
|| (entry.kind === "file" && !details.isFile())
|
|
349
|
+
|| !samePhysicalEntry(details, entry)
|
|
350
|
+
) {
|
|
351
|
+
throw new ApplicationError(
|
|
352
|
+
"conflict",
|
|
353
|
+
"Repository media changed while its physical identity was being verified.",
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
!opened.isFile()
|
|
359
|
+
|| opened.dev !== identity.leaf.dev
|
|
360
|
+
|| opened.ino !== identity.leaf.ino
|
|
361
|
+
|| opened.size !== identity.leaf.size
|
|
362
|
+
|| opened.mtimeMs !== identity.leaf.mtimeMs
|
|
363
|
+
|| opened.ctimeMs !== identity.leaf.ctimeMs
|
|
364
|
+
) {
|
|
365
|
+
throw new ApplicationError(
|
|
366
|
+
"conflict",
|
|
367
|
+
"Repository media changed while its descriptor was being verified.",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function inspectRepositoryMedia(
|
|
373
|
+
application: ApplicationContext,
|
|
374
|
+
input: MediaArtifactRequest,
|
|
375
|
+
signal: AbortSignal,
|
|
376
|
+
maximumBytes: number,
|
|
377
|
+
loadBytes: boolean,
|
|
378
|
+
): Promise<BoundRepositoryMedia | LoadedRepositoryMedia> {
|
|
379
|
+
throwIfAborted(signal);
|
|
380
|
+
const request = MediaArtifactRequestSchema.parse(input);
|
|
381
|
+
const physical = await physicalRepositoryPath(application, request.path);
|
|
382
|
+
if (
|
|
383
|
+
physical.leaf.isSymbolicLink()
|
|
384
|
+
|| !physical.leaf.isFile()
|
|
385
|
+
|| !Number.isSafeInteger(physical.leaf.size)
|
|
386
|
+
|| physical.leaf.size <= 0
|
|
387
|
+
|| physical.leaf.size > maximumBytes
|
|
388
|
+
) {
|
|
389
|
+
throw new ApplicationError(
|
|
390
|
+
"invalid-data",
|
|
391
|
+
`Media must contain 1 through ${String(maximumBytes)} bytes.`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
const handle = await open(
|
|
395
|
+
physical.absolutePath,
|
|
396
|
+
constants.O_RDONLY
|
|
397
|
+
| (constants.O_NOFOLLOW ?? 0)
|
|
398
|
+
| (constants.O_NONBLOCK ?? 0),
|
|
399
|
+
);
|
|
400
|
+
try {
|
|
401
|
+
const before = await handle.stat();
|
|
402
|
+
const exactBefore = await handle.stat({ bigint: true });
|
|
403
|
+
if (
|
|
404
|
+
!before.isFile()
|
|
405
|
+
|| !exactBefore.isFile()
|
|
406
|
+
|| before.dev !== physical.leaf.dev
|
|
407
|
+
|| before.ino !== physical.leaf.ino
|
|
408
|
+
|| before.size !== physical.leaf.size
|
|
409
|
+
|| before.mtimeMs !== physical.leaf.mtimeMs
|
|
410
|
+
|| before.ctimeMs !== physical.leaf.ctimeMs
|
|
411
|
+
) {
|
|
412
|
+
throw new ApplicationError(
|
|
413
|
+
"conflict",
|
|
414
|
+
"Media changed while its descriptor was being opened.",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
await assertPhysicalRepositoryPath(physical, before);
|
|
418
|
+
const inspected = await readAndHashOpenFile(
|
|
419
|
+
handle,
|
|
420
|
+
before.size,
|
|
421
|
+
signal,
|
|
422
|
+
loadBytes,
|
|
423
|
+
);
|
|
424
|
+
const after = await handle.stat();
|
|
425
|
+
const exactAfter = await handle.stat({ bigint: true });
|
|
426
|
+
if (
|
|
427
|
+
after.dev !== before.dev
|
|
428
|
+
|| after.ino !== before.ino
|
|
429
|
+
|| after.size !== before.size
|
|
430
|
+
|| after.mtimeMs !== before.mtimeMs
|
|
431
|
+
|| after.ctimeMs !== before.ctimeMs
|
|
432
|
+
|| exactAfter.dev !== exactBefore.dev
|
|
433
|
+
|| exactAfter.ino !== exactBefore.ino
|
|
434
|
+
|| exactAfter.size !== exactBefore.size
|
|
435
|
+
|| exactAfter.mtimeNs !== exactBefore.mtimeNs
|
|
436
|
+
|| exactAfter.ctimeNs !== exactBefore.ctimeNs
|
|
437
|
+
) {
|
|
438
|
+
throw new ApplicationError(
|
|
439
|
+
"conflict",
|
|
440
|
+
"Media changed while its integrity was being recorded.",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
await assertPhysicalRepositoryPath(physical, after);
|
|
444
|
+
const artifact = MediaArtifactReferenceSchema.parse({
|
|
445
|
+
bytes: before.size,
|
|
446
|
+
path: relative(physical.repositoryRoot, physical.absolutePath),
|
|
447
|
+
sha256: inspected.sha256,
|
|
448
|
+
});
|
|
449
|
+
if (
|
|
450
|
+
"bytes" in request
|
|
451
|
+
&& (
|
|
452
|
+
request.bytes !== artifact.bytes
|
|
453
|
+
|| request.sha256 !== artifact.sha256
|
|
454
|
+
)
|
|
455
|
+
) {
|
|
456
|
+
throw new ApplicationError(
|
|
457
|
+
"conflict",
|
|
458
|
+
"Media no longer matches its integrity-bound workflow input.",
|
|
459
|
+
{
|
|
460
|
+
actualBytes: artifact.bytes,
|
|
461
|
+
actualSha256: artifact.sha256,
|
|
462
|
+
expectedBytes: request.bytes,
|
|
463
|
+
expectedSha256: request.sha256,
|
|
464
|
+
path: artifact.path,
|
|
465
|
+
},
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
absolutePath: physical.absolutePath,
|
|
470
|
+
artifact,
|
|
471
|
+
...(inspected.data === undefined ? {} : { data: inspected.data }),
|
|
472
|
+
expectedInput: {
|
|
473
|
+
bytes: before.size,
|
|
474
|
+
device: before.dev,
|
|
475
|
+
inode: before.ino,
|
|
476
|
+
modifiedAtMs: before.mtimeMs,
|
|
477
|
+
sha256: inspected.sha256,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
} finally {
|
|
481
|
+
await handle.close();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export async function bindRepositoryMedia(
|
|
486
|
+
application: ApplicationContext,
|
|
487
|
+
input: MediaArtifactRequest,
|
|
488
|
+
signal: AbortSignal,
|
|
489
|
+
maximumBytes: number,
|
|
490
|
+
): Promise<BoundRepositoryMedia> {
|
|
491
|
+
return await inspectRepositoryMedia(
|
|
492
|
+
application,
|
|
493
|
+
input,
|
|
494
|
+
signal,
|
|
495
|
+
maximumBytes,
|
|
496
|
+
false,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export async function loadRepositoryMedia(
|
|
501
|
+
application: ApplicationContext,
|
|
502
|
+
input: MediaArtifactRequest,
|
|
503
|
+
signal: AbortSignal,
|
|
504
|
+
maximumBytes: number,
|
|
505
|
+
): Promise<LoadedRepositoryMedia> {
|
|
506
|
+
const loaded = await inspectRepositoryMedia(
|
|
507
|
+
application,
|
|
508
|
+
input,
|
|
509
|
+
signal,
|
|
510
|
+
maximumBytes,
|
|
511
|
+
true,
|
|
512
|
+
);
|
|
513
|
+
if (!("data" in loaded)) {
|
|
514
|
+
throw new ApplicationError(
|
|
515
|
+
"internal",
|
|
516
|
+
"Repository media loading returned no pinned bytes.",
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return loaded;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export class AbortBoundApplicationRunner implements ApplicationProcessRunner {
|
|
523
|
+
readonly #runner: ApplicationProcessRunner;
|
|
524
|
+
readonly #signal: AbortSignal;
|
|
525
|
+
|
|
526
|
+
constructor(runner: ApplicationProcessRunner, signal: AbortSignal) {
|
|
527
|
+
this.#runner = runner;
|
|
528
|
+
this.#signal = signal;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async run(
|
|
532
|
+
argv: readonly [string, ...string[]],
|
|
533
|
+
options: ApplicationProcessRunOptions = {},
|
|
534
|
+
): Promise<ApplicationProcessRunResult> {
|
|
535
|
+
throwIfAborted(this.#signal);
|
|
536
|
+
return await this.#runner.run(argv, {
|
|
537
|
+
...options,
|
|
538
|
+
abortSignal: this.#signal,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export interface MediaOperationWorkspace {
|
|
544
|
+
readonly path: string;
|
|
545
|
+
dispose(): Promise<void>;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function assertPrivateWorkspace(
|
|
549
|
+
application: ApplicationContext,
|
|
550
|
+
path: string,
|
|
551
|
+
): Promise<string> {
|
|
552
|
+
await ensurePrivateDirectory(application.paths.privateRoot);
|
|
553
|
+
const [privateRoot, details] = await Promise.all([
|
|
554
|
+
realpath(application.paths.privateRoot),
|
|
555
|
+
lstat(path),
|
|
556
|
+
]);
|
|
557
|
+
if (
|
|
558
|
+
details.isSymbolicLink()
|
|
559
|
+
|| !details.isDirectory()
|
|
560
|
+
|| (details.mode & 0o077) !== 0
|
|
561
|
+
) {
|
|
562
|
+
throw new ApplicationError(
|
|
563
|
+
"unsafe-path",
|
|
564
|
+
"Media-operation staging must be a private physical directory.",
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
const physical = await realpath(path);
|
|
568
|
+
if (!isWithin(privateRoot, physical) || physical === privateRoot) {
|
|
569
|
+
throw new ApplicationError(
|
|
570
|
+
"unsafe-path",
|
|
571
|
+
"Media-operation staging escaped the application private root.",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return physical;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export async function createMediaOperationWorkspace(
|
|
578
|
+
context: OperationExecutionContext,
|
|
579
|
+
): Promise<MediaOperationWorkspace> {
|
|
580
|
+
const supplied = context.workflow?.workspaceDirectory;
|
|
581
|
+
if (supplied !== undefined) {
|
|
582
|
+
return {
|
|
583
|
+
dispose: () => Promise.resolve(),
|
|
584
|
+
path: await assertPrivateWorkspace(context.application, supplied),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
await ensurePrivateDirectory(context.application.paths.privateRoot);
|
|
588
|
+
const path = await ensurePhysicalPrivateDirectoryWithin(
|
|
589
|
+
context.application.paths.privateRoot,
|
|
590
|
+
`media-operations/${randomUUID()}`,
|
|
591
|
+
);
|
|
592
|
+
return {
|
|
593
|
+
dispose: async () => await rm(path, { force: true, recursive: true }),
|
|
594
|
+
path: await assertPrivateWorkspace(context.application, path),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function generatedDirectory(
|
|
599
|
+
application: ApplicationContext,
|
|
600
|
+
category: "outputs" | "receipts",
|
|
601
|
+
): Promise<string> {
|
|
602
|
+
return await ensurePhysicalPrivateDirectoryWithin(
|
|
603
|
+
application.paths.repositoryRoot,
|
|
604
|
+
relative(
|
|
605
|
+
application.paths.repositoryRoot,
|
|
606
|
+
join(dirname(application.paths.artifactRoot), `generated/media-operations/${category}`),
|
|
607
|
+
),
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function digestPhysicalAbsoluteFile(
|
|
612
|
+
application: ApplicationContext,
|
|
613
|
+
absolutePath: string,
|
|
614
|
+
signal: AbortSignal,
|
|
615
|
+
maximumBytes: number,
|
|
616
|
+
): Promise<MediaArtifactReference> {
|
|
617
|
+
const repositoryRoot = await realpath(application.paths.repositoryRoot);
|
|
618
|
+
const resolved = resolve(absolutePath);
|
|
619
|
+
if (!isWithin(repositoryRoot, resolved) || resolved === repositoryRoot) {
|
|
620
|
+
throw new ApplicationError(
|
|
621
|
+
"unsafe-path",
|
|
622
|
+
"Published media must remain inside the repository.",
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
const details = await lstat(resolved);
|
|
626
|
+
if (
|
|
627
|
+
details.isSymbolicLink()
|
|
628
|
+
|| !details.isFile()
|
|
629
|
+
|| details.nlink !== 1
|
|
630
|
+
|| !Number.isSafeInteger(details.size)
|
|
631
|
+
|| details.size <= 0
|
|
632
|
+
|| details.size > maximumBytes
|
|
633
|
+
) {
|
|
634
|
+
throw new ApplicationError(
|
|
635
|
+
"invalid-data",
|
|
636
|
+
"Published media is unsafe, empty, multiply linked, or too large.",
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const handle = await open(
|
|
640
|
+
resolved,
|
|
641
|
+
constants.O_RDONLY
|
|
642
|
+
| (constants.O_NOFOLLOW ?? 0)
|
|
643
|
+
| (constants.O_NONBLOCK ?? 0),
|
|
644
|
+
);
|
|
645
|
+
try {
|
|
646
|
+
const before = await handle.stat();
|
|
647
|
+
if (
|
|
648
|
+
!before.isFile()
|
|
649
|
+
|| before.dev !== details.dev
|
|
650
|
+
|| before.ino !== details.ino
|
|
651
|
+
|| before.size !== details.size
|
|
652
|
+
|| before.nlink !== 1
|
|
653
|
+
) {
|
|
654
|
+
throw new ApplicationError(
|
|
655
|
+
"conflict",
|
|
656
|
+
"Published media changed while it was being opened.",
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
const { sha256 } = await readAndHashOpenFile(
|
|
660
|
+
handle,
|
|
661
|
+
before.size,
|
|
662
|
+
signal,
|
|
663
|
+
false,
|
|
664
|
+
);
|
|
665
|
+
const after = await handle.stat();
|
|
666
|
+
if (
|
|
667
|
+
after.dev !== before.dev
|
|
668
|
+
|| after.ino !== before.ino
|
|
669
|
+
|| after.size !== before.size
|
|
670
|
+
|| after.nlink !== before.nlink
|
|
671
|
+
|| after.mtimeMs !== before.mtimeMs
|
|
672
|
+
|| after.ctimeMs !== before.ctimeMs
|
|
673
|
+
) {
|
|
674
|
+
throw new ApplicationError(
|
|
675
|
+
"conflict",
|
|
676
|
+
"Published media changed while it was being verified.",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return MediaArtifactReferenceSchema.parse({
|
|
680
|
+
bytes: before.size,
|
|
681
|
+
path: relative(repositoryRoot, resolved),
|
|
682
|
+
sha256,
|
|
683
|
+
});
|
|
684
|
+
} finally {
|
|
685
|
+
await handle.close();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function verifyExistingContentAddress(
|
|
690
|
+
application: ApplicationContext,
|
|
691
|
+
path: string,
|
|
692
|
+
expected: Pick<MediaArtifactReference, "bytes" | "sha256">,
|
|
693
|
+
signal: AbortSignal,
|
|
694
|
+
maximumBytes: number,
|
|
695
|
+
): Promise<void> {
|
|
696
|
+
const actual = await digestPhysicalAbsoluteFile(
|
|
697
|
+
application,
|
|
698
|
+
path,
|
|
699
|
+
signal,
|
|
700
|
+
maximumBytes,
|
|
701
|
+
);
|
|
702
|
+
if (
|
|
703
|
+
actual.bytes !== expected.bytes
|
|
704
|
+
|| actual.sha256 !== expected.sha256
|
|
705
|
+
) {
|
|
706
|
+
throw new ApplicationError(
|
|
707
|
+
"conflict",
|
|
708
|
+
"A content-addressed media destination contains different bytes.",
|
|
709
|
+
{
|
|
710
|
+
actualBytes: actual.bytes,
|
|
711
|
+
actualSha256: actual.sha256,
|
|
712
|
+
expectedBytes: expected.bytes,
|
|
713
|
+
expectedSha256: expected.sha256,
|
|
714
|
+
},
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export async function publishContentAddressedMedia(options: {
|
|
720
|
+
readonly beforePublication?: () => Promise<void>;
|
|
721
|
+
readonly context: OperationExecutionContext;
|
|
722
|
+
readonly extension: string;
|
|
723
|
+
readonly maximumBytes: number;
|
|
724
|
+
readonly stagedPath: string;
|
|
725
|
+
}): Promise<{
|
|
726
|
+
readonly artifact: MediaArtifactReference;
|
|
727
|
+
readonly created: boolean;
|
|
728
|
+
}> {
|
|
729
|
+
const staged = await digestPhysicalAbsoluteFile(
|
|
730
|
+
options.context.application,
|
|
731
|
+
options.stagedPath,
|
|
732
|
+
options.context.abortSignal,
|
|
733
|
+
options.maximumBytes,
|
|
734
|
+
);
|
|
735
|
+
if (!/^\.[a-z0-9]{1,12}$/u.test(options.extension)) {
|
|
736
|
+
throw new ApplicationError("internal", "Media output extension is invalid.");
|
|
737
|
+
}
|
|
738
|
+
const directory = await generatedDirectory(
|
|
739
|
+
options.context.application,
|
|
740
|
+
"outputs",
|
|
741
|
+
);
|
|
742
|
+
const destination = join(
|
|
743
|
+
directory,
|
|
744
|
+
`${staged.sha256}${options.extension}`,
|
|
745
|
+
);
|
|
746
|
+
await options.context.workflow?.beforePublication();
|
|
747
|
+
await options.beforePublication?.();
|
|
748
|
+
throwIfAborted(options.context.abortSignal);
|
|
749
|
+
let created = false;
|
|
750
|
+
try {
|
|
751
|
+
await link(options.stagedPath, destination);
|
|
752
|
+
created = true;
|
|
753
|
+
await syncDirectory(directory);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) {
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
await verifyExistingContentAddress(
|
|
759
|
+
options.context.application,
|
|
760
|
+
destination,
|
|
761
|
+
staged,
|
|
762
|
+
options.context.abortSignal,
|
|
763
|
+
options.maximumBytes,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
await rm(options.stagedPath, { force: true });
|
|
767
|
+
return {
|
|
768
|
+
artifact: MediaArtifactReferenceSchema.parse({
|
|
769
|
+
...staged,
|
|
770
|
+
path: relative(
|
|
771
|
+
await realpath(options.context.application.paths.repositoryRoot),
|
|
772
|
+
destination,
|
|
773
|
+
),
|
|
774
|
+
}),
|
|
775
|
+
created,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export async function publishContentAddressedReceipt(options: {
|
|
780
|
+
readonly beforePublication?: () => Promise<void>;
|
|
781
|
+
readonly context: OperationExecutionContext;
|
|
782
|
+
readonly receipt: unknown;
|
|
783
|
+
readonly workspace: MediaOperationWorkspace;
|
|
784
|
+
}): Promise<MediaArtifactReference> {
|
|
785
|
+
const source = `${canonicalJson(options.receipt)}\n`;
|
|
786
|
+
const sourceBytes = new TextEncoder().encode(source);
|
|
787
|
+
if (sourceBytes.byteLength < 1 || sourceBytes.byteLength > 1024 * 1024) {
|
|
788
|
+
throw new ApplicationError(
|
|
789
|
+
"invalid-data",
|
|
790
|
+
"Media operation receipt exceeds its one-megabyte bound.",
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
const sha256 = createHash("sha256").update(sourceBytes).digest("hex");
|
|
794
|
+
const temporary = join(
|
|
795
|
+
options.workspace.path,
|
|
796
|
+
`.receipt-${randomUUID()}.json`,
|
|
797
|
+
);
|
|
798
|
+
const handle = await open(
|
|
799
|
+
temporary,
|
|
800
|
+
constants.O_CREAT
|
|
801
|
+
| constants.O_EXCL
|
|
802
|
+
| constants.O_WRONLY
|
|
803
|
+
| (constants.O_NOFOLLOW ?? 0),
|
|
804
|
+
0o600,
|
|
805
|
+
);
|
|
806
|
+
try {
|
|
807
|
+
await handle.writeFile(sourceBytes);
|
|
808
|
+
await handle.sync();
|
|
809
|
+
} finally {
|
|
810
|
+
await handle.close();
|
|
811
|
+
}
|
|
812
|
+
const directory = await generatedDirectory(
|
|
813
|
+
options.context.application,
|
|
814
|
+
"receipts",
|
|
815
|
+
);
|
|
816
|
+
const destination = join(directory, `${sha256}.json`);
|
|
817
|
+
await options.context.workflow?.beforePublication();
|
|
818
|
+
await options.beforePublication?.();
|
|
819
|
+
throwIfAborted(options.context.abortSignal);
|
|
820
|
+
try {
|
|
821
|
+
await link(temporary, destination);
|
|
822
|
+
await syncDirectory(directory);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) {
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
await verifyExistingContentAddress(
|
|
828
|
+
options.context.application,
|
|
829
|
+
destination,
|
|
830
|
+
{ bytes: sourceBytes.byteLength, sha256 },
|
|
831
|
+
options.context.abortSignal,
|
|
832
|
+
1024 * 1024,
|
|
833
|
+
);
|
|
834
|
+
} finally {
|
|
835
|
+
await rm(temporary, { force: true });
|
|
836
|
+
}
|
|
837
|
+
return MediaArtifactReferenceSchema.parse({
|
|
838
|
+
bytes: sourceBytes.byteLength,
|
|
839
|
+
path: relative(
|
|
840
|
+
await realpath(options.context.application.paths.repositoryRoot),
|
|
841
|
+
destination,
|
|
842
|
+
),
|
|
843
|
+
sha256,
|
|
844
|
+
});
|
|
845
|
+
}
|