@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,922 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
chmod,
|
|
4
|
+
lstat,
|
|
5
|
+
mkdir,
|
|
6
|
+
open,
|
|
7
|
+
rename,
|
|
8
|
+
rm,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import { dirname, isAbsolute } from "node:path";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
import { canonicalJson, canonicalJsonSha256 } from "../core/canonical-json";
|
|
14
|
+
|
|
15
|
+
export const GATEWAY_MEDIA_CATALOG_URL = "https://ai-gateway.vercel.sh/v1/models";
|
|
16
|
+
export const GATEWAY_MEDIA_KINDS = [
|
|
17
|
+
"image",
|
|
18
|
+
"speech",
|
|
19
|
+
"transcription",
|
|
20
|
+
"video",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type GatewayMediaKind = (typeof GATEWAY_MEDIA_KINDS)[number];
|
|
24
|
+
export type GatewayMediaExecutionMode =
|
|
25
|
+
| "image-model"
|
|
26
|
+
| "language-image"
|
|
27
|
+
| "speech-model"
|
|
28
|
+
| "transcription-model"
|
|
29
|
+
| "video-model";
|
|
30
|
+
export type GatewayCatalogModelType = GatewayMediaKind | "language";
|
|
31
|
+
export type GatewayJsonValue =
|
|
32
|
+
| boolean
|
|
33
|
+
| null
|
|
34
|
+
| number
|
|
35
|
+
| string
|
|
36
|
+
| readonly GatewayJsonValue[]
|
|
37
|
+
| { readonly [key: string]: GatewayJsonValue };
|
|
38
|
+
|
|
39
|
+
export interface GatewayMediaModel {
|
|
40
|
+
readonly capabilities: Readonly<Record<string, GatewayJsonValue>> | null;
|
|
41
|
+
readonly created: number | null;
|
|
42
|
+
readonly description: string;
|
|
43
|
+
readonly executionMode: GatewayMediaExecutionMode;
|
|
44
|
+
readonly gatewayType: GatewayCatalogModelType;
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly kind: GatewayMediaKind;
|
|
47
|
+
readonly modalities: Readonly<{
|
|
48
|
+
input: readonly string[];
|
|
49
|
+
output: readonly string[];
|
|
50
|
+
}>;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly ownedBy: string;
|
|
53
|
+
readonly pricing: Readonly<Record<string, GatewayJsonValue>>;
|
|
54
|
+
readonly released: number | null;
|
|
55
|
+
readonly supportedParameters: readonly string[];
|
|
56
|
+
readonly tags: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface GatewayCatalogValidators {
|
|
60
|
+
readonly etag?: string;
|
|
61
|
+
readonly lastModified?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GatewayMediaCatalogSnapshot {
|
|
65
|
+
readonly fetchedAt: string;
|
|
66
|
+
readonly kind:
|
|
67
|
+
| "slopcamera.gateway-media-catalog"
|
|
68
|
+
| "studio.gateway-media-catalog";
|
|
69
|
+
readonly models: readonly GatewayMediaModel[];
|
|
70
|
+
readonly schemaVersion: 1;
|
|
71
|
+
readonly snapshotId: `sha256:${string}`;
|
|
72
|
+
readonly validatedAt: string;
|
|
73
|
+
readonly validators?: GatewayCatalogValidators;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface GatewayMediaCatalogView {
|
|
77
|
+
readonly snapshot: GatewayMediaCatalogSnapshot;
|
|
78
|
+
readonly source: "disk" | "memory" | "network";
|
|
79
|
+
readonly status: "fresh" | "stale";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GatewayMediaModelSummary {
|
|
83
|
+
readonly executionMode: GatewayMediaExecutionMode;
|
|
84
|
+
readonly id: string;
|
|
85
|
+
readonly kind: GatewayMediaKind;
|
|
86
|
+
readonly name: string;
|
|
87
|
+
readonly operations: readonly string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type GatewayCatalogRefresh =
|
|
91
|
+
| Readonly<{
|
|
92
|
+
fetchedAt?: string;
|
|
93
|
+
payload: unknown;
|
|
94
|
+
status: "modified";
|
|
95
|
+
validators?: GatewayCatalogValidators;
|
|
96
|
+
}>
|
|
97
|
+
| Readonly<{
|
|
98
|
+
validatedAt?: string;
|
|
99
|
+
status: "not-modified";
|
|
100
|
+
validators?: GatewayCatalogValidators;
|
|
101
|
+
}>;
|
|
102
|
+
|
|
103
|
+
export interface GatewayMediaCatalogTransport {
|
|
104
|
+
refresh(
|
|
105
|
+
validators: GatewayCatalogValidators | undefined,
|
|
106
|
+
signal: AbortSignal | undefined,
|
|
107
|
+
): Promise<GatewayCatalogRefresh>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface GatewayMediaCatalogSnapshotStore {
|
|
111
|
+
read(): Promise<unknown>;
|
|
112
|
+
write(snapshot: GatewayMediaCatalogSnapshot): Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type GatewayCatalogFetch = (
|
|
116
|
+
input: RequestInfo | URL,
|
|
117
|
+
init?: RequestInit,
|
|
118
|
+
) => Promise<Response>;
|
|
119
|
+
|
|
120
|
+
export type GatewayMediaCatalogErrorCode =
|
|
121
|
+
| "catalog-invalid"
|
|
122
|
+
| "catalog-unavailable"
|
|
123
|
+
| "invalid-request";
|
|
124
|
+
|
|
125
|
+
const CATALOG_RESPONSE_MAX_BYTES = 6 * 1024 * 1024;
|
|
126
|
+
const CATALOG_STORE_MAX_BYTES = 6 * 1024 * 1024;
|
|
127
|
+
const CATALOG_MODEL_LIMIT = 1_000;
|
|
128
|
+
const CATALOG_JSON_MAX_DEPTH = 16;
|
|
129
|
+
const CATALOG_JSON_MAX_NODES = 50_000;
|
|
130
|
+
const DEFAULT_CATALOG_TIMEOUT_MS = 20_000;
|
|
131
|
+
const DEFAULT_FRESH_MS = 5 * 60_000;
|
|
132
|
+
const DEFAULT_STALE_MS = 24 * 60 * 60_000;
|
|
133
|
+
|
|
134
|
+
const ERROR_MESSAGES: Readonly<Record<GatewayMediaCatalogErrorCode, string>> = {
|
|
135
|
+
"catalog-invalid": "The Vercel AI Gateway media catalog is invalid.",
|
|
136
|
+
"catalog-unavailable": "The Vercel AI Gateway media catalog is unavailable.",
|
|
137
|
+
"invalid-request": "The media catalog request is invalid.",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export class GatewayMediaCatalogError extends Error {
|
|
141
|
+
readonly code: GatewayMediaCatalogErrorCode;
|
|
142
|
+
|
|
143
|
+
constructor(code: GatewayMediaCatalogErrorCode) {
|
|
144
|
+
super(ERROR_MESSAGES[code]);
|
|
145
|
+
this.code = code;
|
|
146
|
+
this.name = "GatewayMediaCatalogError";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
151
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isMediaKind(value: unknown): value is GatewayMediaKind {
|
|
155
|
+
return GATEWAY_MEDIA_KINDS.some(kind => kind === value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isCatalogModelType(value: unknown): value is GatewayCatalogModelType {
|
|
159
|
+
return isMediaKind(value) || value === "language";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseCanonicalTimestamp(value: unknown): string {
|
|
163
|
+
if (typeof value !== "string") throw new GatewayMediaCatalogError("catalog-invalid");
|
|
164
|
+
const milliseconds = Date.parse(value);
|
|
165
|
+
if (!Number.isFinite(milliseconds) || new Date(milliseconds).toISOString() !== value) {
|
|
166
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseBoundedString(
|
|
172
|
+
value: unknown,
|
|
173
|
+
maximumLength: number,
|
|
174
|
+
allowEmpty = false,
|
|
175
|
+
): string {
|
|
176
|
+
if (
|
|
177
|
+
typeof value !== "string"
|
|
178
|
+
|| value.length > maximumLength
|
|
179
|
+
|| (!allowEmpty && value.length === 0)
|
|
180
|
+
|| [...value].some((character) => {
|
|
181
|
+
const codePoint = character.codePointAt(0);
|
|
182
|
+
return codePoint !== undefined && (
|
|
183
|
+
codePoint <= 8
|
|
184
|
+
|| codePoint === 11
|
|
185
|
+
|| codePoint === 12
|
|
186
|
+
|| (codePoint >= 14 && codePoint <= 31)
|
|
187
|
+
|| codePoint === 127
|
|
188
|
+
);
|
|
189
|
+
})
|
|
190
|
+
) {
|
|
191
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseStringArray(
|
|
197
|
+
value: unknown,
|
|
198
|
+
maximumItems: number,
|
|
199
|
+
maximumItemLength: number,
|
|
200
|
+
): readonly string[] {
|
|
201
|
+
if (value === undefined) return [];
|
|
202
|
+
if (!Array.isArray(value) || value.length > maximumItems) {
|
|
203
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
204
|
+
}
|
|
205
|
+
const parsed = value.map(item => parseBoundedString(item, maximumItemLength));
|
|
206
|
+
if (new Set(parsed).size !== parsed.length) {
|
|
207
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
208
|
+
}
|
|
209
|
+
return parsed;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface JsonParseBudget {
|
|
213
|
+
nodes: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parseGatewayJson(
|
|
217
|
+
value: unknown,
|
|
218
|
+
budget: JsonParseBudget,
|
|
219
|
+
depth = 0,
|
|
220
|
+
): GatewayJsonValue {
|
|
221
|
+
budget.nodes += 1;
|
|
222
|
+
if (budget.nodes > CATALOG_JSON_MAX_NODES || depth > CATALOG_JSON_MAX_DEPTH) {
|
|
223
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
224
|
+
}
|
|
225
|
+
if (
|
|
226
|
+
value === null
|
|
227
|
+
|| typeof value === "boolean"
|
|
228
|
+
|| typeof value === "string"
|
|
229
|
+
) {
|
|
230
|
+
return typeof value === "string"
|
|
231
|
+
? parseBoundedString(value, 32_768, true)
|
|
232
|
+
: value;
|
|
233
|
+
}
|
|
234
|
+
if (typeof value === "number") {
|
|
235
|
+
if (!Number.isFinite(value)) throw new GatewayMediaCatalogError("catalog-invalid");
|
|
236
|
+
return Object.is(value, -0) ? 0 : value;
|
|
237
|
+
}
|
|
238
|
+
if (Array.isArray(value)) {
|
|
239
|
+
if (value.length > 4_096) throw new GatewayMediaCatalogError("catalog-invalid");
|
|
240
|
+
return value.map(item => parseGatewayJson(item, budget, depth + 1));
|
|
241
|
+
}
|
|
242
|
+
if (!isRecord(value) || Object.keys(value).length > 2_048) {
|
|
243
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
244
|
+
}
|
|
245
|
+
const parsed: Record<string, GatewayJsonValue> = Object.create(null) as Record<
|
|
246
|
+
string,
|
|
247
|
+
GatewayJsonValue
|
|
248
|
+
>;
|
|
249
|
+
for (const [key, item] of Object.entries(value)) {
|
|
250
|
+
const parsedKey = parseBoundedString(key, 256);
|
|
251
|
+
parsed[parsedKey] = parseGatewayJson(item, budget, depth + 1);
|
|
252
|
+
}
|
|
253
|
+
return parsed;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseJsonRecord(
|
|
257
|
+
value: unknown,
|
|
258
|
+
optional = false,
|
|
259
|
+
): Readonly<Record<string, GatewayJsonValue>> | null {
|
|
260
|
+
if (optional && (value === undefined || value === null)) return null;
|
|
261
|
+
if (!isRecord(value)) throw new GatewayMediaCatalogError("catalog-invalid");
|
|
262
|
+
return parseGatewayJson(value, { nodes: 0 }) as Readonly<
|
|
263
|
+
Record<string, GatewayJsonValue>
|
|
264
|
+
>;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseEpochSeconds(value: unknown): number | null {
|
|
268
|
+
if (value === undefined || value === null) return null;
|
|
269
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
270
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
271
|
+
}
|
|
272
|
+
return value as number;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseModalities(value: unknown): GatewayMediaModel["modalities"] {
|
|
276
|
+
if (!isRecord(value)) throw new GatewayMediaCatalogError("catalog-invalid");
|
|
277
|
+
return {
|
|
278
|
+
input: parseStringArray(value.input, 16, 64),
|
|
279
|
+
output: parseStringArray(value.output, 16, 64),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseCatalogModel(value: unknown): GatewayMediaModel | null {
|
|
284
|
+
if (!isRecord(value) || !isCatalogModelType(value.type)) return null;
|
|
285
|
+
const gatewayType = value.type;
|
|
286
|
+
const tags = parseStringArray(value.tags, 256, 128);
|
|
287
|
+
const modalities = parseModalities(value.modalities);
|
|
288
|
+
const isLanguageImage = gatewayType === "language"
|
|
289
|
+
&& tags.includes("image-generation")
|
|
290
|
+
&& modalities.output.includes("image");
|
|
291
|
+
if (gatewayType === "language" && !isLanguageImage) return null;
|
|
292
|
+
const kind: GatewayMediaKind = gatewayType === "language"
|
|
293
|
+
? "image"
|
|
294
|
+
: gatewayType;
|
|
295
|
+
const id = parseBoundedString(value.id, 256);
|
|
296
|
+
if (
|
|
297
|
+
!id.includes("/")
|
|
298
|
+
|| /\s/u.test(id)
|
|
299
|
+
|| id.startsWith("/")
|
|
300
|
+
|| id.endsWith("/")
|
|
301
|
+
) {
|
|
302
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
303
|
+
}
|
|
304
|
+
const capabilities = parseJsonRecord(value[`${gatewayType}_capabilities`], true);
|
|
305
|
+
const executionMode: GatewayMediaExecutionMode = isLanguageImage
|
|
306
|
+
? "language-image"
|
|
307
|
+
: kind === "image"
|
|
308
|
+
? "image-model"
|
|
309
|
+
: kind === "speech"
|
|
310
|
+
? "speech-model"
|
|
311
|
+
: kind === "transcription"
|
|
312
|
+
? "transcription-model"
|
|
313
|
+
: "video-model";
|
|
314
|
+
return {
|
|
315
|
+
capabilities,
|
|
316
|
+
created: parseEpochSeconds(value.created),
|
|
317
|
+
description: parseBoundedString(value.description, 32_768, true),
|
|
318
|
+
executionMode,
|
|
319
|
+
gatewayType,
|
|
320
|
+
id,
|
|
321
|
+
kind,
|
|
322
|
+
modalities,
|
|
323
|
+
name: parseBoundedString(value.name, 512),
|
|
324
|
+
ownedBy: parseBoundedString(value.owned_by, 256),
|
|
325
|
+
pricing: parseJsonRecord(value.pricing) ?? {},
|
|
326
|
+
released: parseEpochSeconds(value.released),
|
|
327
|
+
supportedParameters: parseStringArray(value.supported_parameters, 256, 128),
|
|
328
|
+
tags,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function modelSort(
|
|
333
|
+
left: GatewayMediaModel,
|
|
334
|
+
right: GatewayMediaModel,
|
|
335
|
+
): number {
|
|
336
|
+
return left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function snapshotDigest(models: readonly GatewayMediaModel[]): `sha256:${string}` {
|
|
340
|
+
return `sha256:${canonicalJsonSha256([...models].sort(modelSort))}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function parseGatewayMediaCatalog(
|
|
344
|
+
value: unknown,
|
|
345
|
+
timestamps: Readonly<{
|
|
346
|
+
fetchedAt: string;
|
|
347
|
+
validatedAt?: string;
|
|
348
|
+
validators?: GatewayCatalogValidators;
|
|
349
|
+
}>,
|
|
350
|
+
): GatewayMediaCatalogSnapshot {
|
|
351
|
+
if (!isRecord(value) || !Array.isArray(value.data) || value.data.length > CATALOG_MODEL_LIMIT) {
|
|
352
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
353
|
+
}
|
|
354
|
+
const models: GatewayMediaModel[] = [];
|
|
355
|
+
const ids = new Set<string>();
|
|
356
|
+
for (const row of value.data) {
|
|
357
|
+
const model = parseCatalogModel(row);
|
|
358
|
+
if (model === null) continue;
|
|
359
|
+
if (ids.has(model.id)) throw new GatewayMediaCatalogError("catalog-invalid");
|
|
360
|
+
ids.add(model.id);
|
|
361
|
+
models.push(model);
|
|
362
|
+
}
|
|
363
|
+
if (models.length === 0) throw new GatewayMediaCatalogError("catalog-invalid");
|
|
364
|
+
models.sort(modelSort);
|
|
365
|
+
const fetchedAt = parseCanonicalTimestamp(timestamps.fetchedAt);
|
|
366
|
+
const validatedAt = parseCanonicalTimestamp(timestamps.validatedAt ?? fetchedAt);
|
|
367
|
+
if (Date.parse(validatedAt) < Date.parse(fetchedAt)) {
|
|
368
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
369
|
+
}
|
|
370
|
+
const validators = parseValidators(timestamps.validators);
|
|
371
|
+
return {
|
|
372
|
+
fetchedAt,
|
|
373
|
+
kind: "slopcamera.gateway-media-catalog",
|
|
374
|
+
models,
|
|
375
|
+
schemaVersion: 1,
|
|
376
|
+
snapshotId: snapshotDigest(models),
|
|
377
|
+
validatedAt,
|
|
378
|
+
...(validators === undefined ? {} : { validators }),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function parseValidators(
|
|
383
|
+
validators: GatewayCatalogValidators | undefined,
|
|
384
|
+
): GatewayCatalogValidators | undefined {
|
|
385
|
+
if (validators === undefined) return undefined;
|
|
386
|
+
const etag = validators.etag === undefined
|
|
387
|
+
? undefined
|
|
388
|
+
: parseBoundedString(validators.etag, 1_024);
|
|
389
|
+
const lastModified = validators.lastModified === undefined
|
|
390
|
+
? undefined
|
|
391
|
+
: parseBoundedString(validators.lastModified, 1_024);
|
|
392
|
+
if (etag === undefined && lastModified === undefined) return undefined;
|
|
393
|
+
return {
|
|
394
|
+
...(etag === undefined ? {} : { etag }),
|
|
395
|
+
...(lastModified === undefined ? {} : { lastModified }),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function parseGatewayMediaCatalogSnapshot(
|
|
400
|
+
value: unknown,
|
|
401
|
+
): GatewayMediaCatalogSnapshot {
|
|
402
|
+
if (
|
|
403
|
+
!isRecord(value)
|
|
404
|
+
|| (
|
|
405
|
+
value.kind !== "slopcamera.gateway-media-catalog"
|
|
406
|
+
&& value.kind !== "studio.gateway-media-catalog"
|
|
407
|
+
)
|
|
408
|
+
|| value.schemaVersion !== 1
|
|
409
|
+
|| !Array.isArray(value.models)
|
|
410
|
+
|| typeof value.snapshotId !== "string"
|
|
411
|
+
) {
|
|
412
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
413
|
+
}
|
|
414
|
+
const envelope = {
|
|
415
|
+
data: value.models.map(model => {
|
|
416
|
+
if (
|
|
417
|
+
!isRecord(model)
|
|
418
|
+
|| !isMediaKind(model.kind)
|
|
419
|
+
|| !isCatalogModelType(model.gatewayType)
|
|
420
|
+
) {
|
|
421
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
created: model.created,
|
|
425
|
+
description: model.description,
|
|
426
|
+
id: model.id,
|
|
427
|
+
modalities: model.modalities,
|
|
428
|
+
name: model.name,
|
|
429
|
+
object: "model",
|
|
430
|
+
owned_by: model.ownedBy,
|
|
431
|
+
pricing: model.pricing,
|
|
432
|
+
released: model.released,
|
|
433
|
+
supported_parameters: model.supportedParameters,
|
|
434
|
+
tags: model.tags,
|
|
435
|
+
type: model.gatewayType,
|
|
436
|
+
[`${model.gatewayType}_capabilities`]: model.capabilities,
|
|
437
|
+
};
|
|
438
|
+
}),
|
|
439
|
+
};
|
|
440
|
+
const snapshotValidators = isRecord(value.validators)
|
|
441
|
+
? {
|
|
442
|
+
...(typeof value.validators.etag === "string"
|
|
443
|
+
? { etag: value.validators.etag }
|
|
444
|
+
: {}),
|
|
445
|
+
...(typeof value.validators.lastModified === "string"
|
|
446
|
+
? { lastModified: value.validators.lastModified }
|
|
447
|
+
: {}),
|
|
448
|
+
}
|
|
449
|
+
: undefined;
|
|
450
|
+
const snapshot = parseGatewayMediaCatalog(envelope, {
|
|
451
|
+
fetchedAt: value.fetchedAt as string,
|
|
452
|
+
validatedAt: value.validatedAt as string,
|
|
453
|
+
...(snapshotValidators === undefined
|
|
454
|
+
? {}
|
|
455
|
+
: { validators: snapshotValidators }),
|
|
456
|
+
});
|
|
457
|
+
if (snapshot.snapshotId !== value.snapshotId) {
|
|
458
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
459
|
+
}
|
|
460
|
+
return { ...snapshot, kind: value.kind };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function supportedOperations(model: GatewayMediaModel): readonly string[] {
|
|
464
|
+
if (model.kind === "transcription") {
|
|
465
|
+
if (model.tags.includes("websocket-realtime")) {
|
|
466
|
+
return ["streaming-transcription"];
|
|
467
|
+
}
|
|
468
|
+
if (model.tags.includes("websocket-transcription")) {
|
|
469
|
+
return ["batch-transcription", "streaming-transcription"];
|
|
470
|
+
}
|
|
471
|
+
return ["batch-transcription"];
|
|
472
|
+
}
|
|
473
|
+
const operations = model.capabilities?.supported_operations;
|
|
474
|
+
return Array.isArray(operations)
|
|
475
|
+
? operations.filter((item): item is string => typeof item === "string")
|
|
476
|
+
: [];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function listGatewayMediaModels(
|
|
480
|
+
snapshot: GatewayMediaCatalogSnapshot,
|
|
481
|
+
options: Readonly<{
|
|
482
|
+
kind?: GatewayMediaKind;
|
|
483
|
+
limit?: number;
|
|
484
|
+
query?: string;
|
|
485
|
+
}> = {},
|
|
486
|
+
): readonly GatewayMediaModelSummary[] {
|
|
487
|
+
const limit = options.limit ?? 50;
|
|
488
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
489
|
+
throw new GatewayMediaCatalogError("invalid-request");
|
|
490
|
+
}
|
|
491
|
+
const query = options.query?.trim().toLocaleLowerCase("en-US");
|
|
492
|
+
if (query !== undefined && query.length > 256) {
|
|
493
|
+
throw new GatewayMediaCatalogError("invalid-request");
|
|
494
|
+
}
|
|
495
|
+
return snapshot.models
|
|
496
|
+
.filter(model => options.kind === undefined || model.kind === options.kind)
|
|
497
|
+
.filter(model => query === undefined || query.length === 0 || (
|
|
498
|
+
model.id.toLocaleLowerCase("en-US").includes(query)
|
|
499
|
+
|| model.name.toLocaleLowerCase("en-US").includes(query)
|
|
500
|
+
|| model.description.toLocaleLowerCase("en-US").includes(query)
|
|
501
|
+
))
|
|
502
|
+
.slice(0, limit)
|
|
503
|
+
.map(model => ({
|
|
504
|
+
executionMode: model.executionMode,
|
|
505
|
+
id: model.id,
|
|
506
|
+
kind: model.kind,
|
|
507
|
+
name: model.name,
|
|
508
|
+
operations: supportedOperations(model),
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function inspectGatewayMediaModel(
|
|
513
|
+
snapshot: GatewayMediaCatalogSnapshot,
|
|
514
|
+
id: string,
|
|
515
|
+
): GatewayMediaModel | null {
|
|
516
|
+
return snapshot.models.find(model => model.id === id) ?? null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function mergeValidators(
|
|
520
|
+
current: GatewayCatalogValidators | undefined,
|
|
521
|
+
next: GatewayCatalogValidators | undefined,
|
|
522
|
+
): GatewayCatalogValidators | undefined {
|
|
523
|
+
return parseValidators({
|
|
524
|
+
...(current ?? {}),
|
|
525
|
+
...(next ?? {}),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function parseCacheOptions(options: Readonly<{
|
|
530
|
+
freshMs?: number;
|
|
531
|
+
staleMs?: number;
|
|
532
|
+
}>): Readonly<{ freshMs: number; staleMs: number }> {
|
|
533
|
+
const freshMs = options.freshMs ?? DEFAULT_FRESH_MS;
|
|
534
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
535
|
+
if (
|
|
536
|
+
!Number.isSafeInteger(freshMs)
|
|
537
|
+
|| !Number.isSafeInteger(staleMs)
|
|
538
|
+
|| freshMs < 0
|
|
539
|
+
|| staleMs < freshMs
|
|
540
|
+
) {
|
|
541
|
+
throw new GatewayMediaCatalogError("invalid-request");
|
|
542
|
+
}
|
|
543
|
+
return { freshMs, staleMs };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function createGatewayMediaCatalogCache(options: Readonly<{
|
|
547
|
+
freshMs?: number;
|
|
548
|
+
now?: () => number;
|
|
549
|
+
snapshotStore?: GatewayMediaCatalogSnapshotStore;
|
|
550
|
+
staleMs?: number;
|
|
551
|
+
transport: GatewayMediaCatalogTransport;
|
|
552
|
+
}>): Readonly<{
|
|
553
|
+
get(input?: Readonly<{
|
|
554
|
+
forceRefresh?: boolean;
|
|
555
|
+
freshness?: "allow-stale" | "require-fresh";
|
|
556
|
+
signal?: AbortSignal;
|
|
557
|
+
}>): Promise<GatewayMediaCatalogView>;
|
|
558
|
+
}> {
|
|
559
|
+
const { freshMs, staleMs } = parseCacheOptions(options);
|
|
560
|
+
const now = options.now ?? Date.now;
|
|
561
|
+
let cached:
|
|
562
|
+
| Readonly<{ snapshot: GatewayMediaCatalogSnapshot; source: "disk" | "memory" | "network" }>
|
|
563
|
+
| undefined;
|
|
564
|
+
let storeLoad: Promise<void> | undefined;
|
|
565
|
+
let refreshFlight: Promise<GatewayMediaCatalogView> | undefined;
|
|
566
|
+
|
|
567
|
+
const loadStoredSnapshot = (): Promise<void> => {
|
|
568
|
+
storeLoad ??= (async () => {
|
|
569
|
+
if (options.snapshotStore === undefined) return;
|
|
570
|
+
try {
|
|
571
|
+
const value = await options.snapshotStore.read();
|
|
572
|
+
if (value !== null) {
|
|
573
|
+
cached = {
|
|
574
|
+
snapshot: parseGatewayMediaCatalogSnapshot(value),
|
|
575
|
+
source: "disk",
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
// A corrupt or inaccessible cache is never authority. Refresh below.
|
|
580
|
+
}
|
|
581
|
+
})();
|
|
582
|
+
return storeLoad;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const cachedView = (
|
|
586
|
+
freshness: "allow-stale" | "require-fresh",
|
|
587
|
+
): GatewayMediaCatalogView | null => {
|
|
588
|
+
if (cached === undefined) return null;
|
|
589
|
+
const age = now() - Date.parse(cached.snapshot.validatedAt);
|
|
590
|
+
if (age >= 0 && age <= freshMs) {
|
|
591
|
+
return { snapshot: cached.snapshot, source: cached.source, status: "fresh" };
|
|
592
|
+
}
|
|
593
|
+
if (freshness === "allow-stale" && age >= 0 && age <= staleMs) {
|
|
594
|
+
return { snapshot: cached.snapshot, source: cached.source, status: "stale" };
|
|
595
|
+
}
|
|
596
|
+
return null;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const callerAbortError = (signal: AbortSignal): Error => (
|
|
600
|
+
signal.reason instanceof Error
|
|
601
|
+
? signal.reason
|
|
602
|
+
: new DOMException("The catalog request was aborted.", "AbortError")
|
|
603
|
+
);
|
|
604
|
+
const throwIfCallerAborted = (signal: AbortSignal | undefined): void => {
|
|
605
|
+
if (signal?.aborted === true) throw callerAbortError(signal);
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const awaitWithCallerSignal = async <Value>(
|
|
609
|
+
promise: Promise<Value>,
|
|
610
|
+
signal: AbortSignal | undefined,
|
|
611
|
+
): Promise<Value> => {
|
|
612
|
+
if (signal === undefined) return await promise;
|
|
613
|
+
if (signal.aborted) throw callerAbortError(signal);
|
|
614
|
+
return await new Promise<Value>((resolve, reject) => {
|
|
615
|
+
const abort = (): void => reject(callerAbortError(signal));
|
|
616
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
617
|
+
void promise.then(
|
|
618
|
+
value => {
|
|
619
|
+
signal.removeEventListener("abort", abort);
|
|
620
|
+
resolve(value);
|
|
621
|
+
},
|
|
622
|
+
error => {
|
|
623
|
+
signal.removeEventListener("abort", abort);
|
|
624
|
+
reject(
|
|
625
|
+
error instanceof Error
|
|
626
|
+
? error
|
|
627
|
+
: new GatewayMediaCatalogError("catalog-unavailable"),
|
|
628
|
+
);
|
|
629
|
+
},
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const refresh = (): Promise<GatewayMediaCatalogView> => {
|
|
635
|
+
if (refreshFlight !== undefined) return refreshFlight;
|
|
636
|
+
const current = (async (): Promise<GatewayMediaCatalogView> => {
|
|
637
|
+
const refreshed = await options.transport.refresh(
|
|
638
|
+
cached?.snapshot.validators,
|
|
639
|
+
undefined,
|
|
640
|
+
);
|
|
641
|
+
if (refreshed.status === "not-modified") {
|
|
642
|
+
if (cached === undefined) {
|
|
643
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
644
|
+
}
|
|
645
|
+
const validatedAt = parseCanonicalTimestamp(
|
|
646
|
+
refreshed.validatedAt ?? new Date(now()).toISOString(),
|
|
647
|
+
);
|
|
648
|
+
const validators = mergeValidators(
|
|
649
|
+
cached.snapshot.validators,
|
|
650
|
+
refreshed.validators,
|
|
651
|
+
);
|
|
652
|
+
const snapshot: GatewayMediaCatalogSnapshot = {
|
|
653
|
+
...cached.snapshot,
|
|
654
|
+
kind: "slopcamera.gateway-media-catalog",
|
|
655
|
+
validatedAt,
|
|
656
|
+
...(validators === undefined ? {} : { validators }),
|
|
657
|
+
};
|
|
658
|
+
cached = { snapshot, source: "network" };
|
|
659
|
+
await options.snapshotStore?.write(snapshot).catch(() => undefined);
|
|
660
|
+
return { snapshot, source: "network", status: "fresh" };
|
|
661
|
+
}
|
|
662
|
+
const fetchedAt = refreshed.fetchedAt ?? new Date(now()).toISOString();
|
|
663
|
+
const snapshot = parseGatewayMediaCatalog(refreshed.payload, {
|
|
664
|
+
fetchedAt,
|
|
665
|
+
...(refreshed.validators === undefined
|
|
666
|
+
? {}
|
|
667
|
+
: { validators: refreshed.validators }),
|
|
668
|
+
});
|
|
669
|
+
cached = { snapshot, source: "network" };
|
|
670
|
+
await options.snapshotStore?.write(snapshot).catch(() => undefined);
|
|
671
|
+
return { snapshot, source: "network", status: "fresh" };
|
|
672
|
+
})();
|
|
673
|
+
refreshFlight = current;
|
|
674
|
+
void current.finally(() => {
|
|
675
|
+
if (refreshFlight === current) refreshFlight = undefined;
|
|
676
|
+
}).catch(() => undefined);
|
|
677
|
+
return current;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
get: async (input = {}) => {
|
|
682
|
+
throwIfCallerAborted(input.signal);
|
|
683
|
+
await awaitWithCallerSignal(loadStoredSnapshot(), input.signal);
|
|
684
|
+
const freshness = input.freshness ?? "allow-stale";
|
|
685
|
+
if (!input.forceRefresh) {
|
|
686
|
+
const available = cachedView("require-fresh");
|
|
687
|
+
if (available !== null) return available;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
return await awaitWithCallerSignal(refresh(), input.signal);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
throwIfCallerAborted(input.signal);
|
|
693
|
+
const available = cachedView(freshness);
|
|
694
|
+
if (available !== null) return available;
|
|
695
|
+
if (error instanceof GatewayMediaCatalogError) throw error;
|
|
696
|
+
throw new GatewayMediaCatalogError("catalog-unavailable");
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function readBoundedResponse(
|
|
703
|
+
response: Response,
|
|
704
|
+
maximumBytes: number,
|
|
705
|
+
): Promise<Uint8Array> {
|
|
706
|
+
const declared = response.headers.get("content-length");
|
|
707
|
+
if (declared !== null) {
|
|
708
|
+
const bytes = Number(declared);
|
|
709
|
+
if (Number.isFinite(bytes) && bytes > maximumBytes) {
|
|
710
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (response.body === null) throw new GatewayMediaCatalogError("catalog-invalid");
|
|
714
|
+
const reader = response.body.getReader();
|
|
715
|
+
const chunks: Uint8Array[] = [];
|
|
716
|
+
let length = 0;
|
|
717
|
+
try {
|
|
718
|
+
while (true) {
|
|
719
|
+
const next = await reader.read();
|
|
720
|
+
if (next.done) break;
|
|
721
|
+
length += next.value.byteLength;
|
|
722
|
+
if (length > maximumBytes) {
|
|
723
|
+
await reader.cancel().catch(() => undefined);
|
|
724
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
725
|
+
}
|
|
726
|
+
chunks.push(next.value);
|
|
727
|
+
}
|
|
728
|
+
} finally {
|
|
729
|
+
reader.releaseLock();
|
|
730
|
+
}
|
|
731
|
+
const body = new Uint8Array(length);
|
|
732
|
+
let offset = 0;
|
|
733
|
+
for (const chunk of chunks) {
|
|
734
|
+
body.set(chunk, offset);
|
|
735
|
+
offset += chunk.byteLength;
|
|
736
|
+
}
|
|
737
|
+
return body;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function combinedAbortSignal(
|
|
741
|
+
callerSignal: AbortSignal | undefined,
|
|
742
|
+
timeoutMs: number,
|
|
743
|
+
): Readonly<{ dispose(): void; signal: AbortSignal }> {
|
|
744
|
+
const controller = new AbortController();
|
|
745
|
+
const abortFromCaller = (): void => controller.abort(callerSignal?.reason);
|
|
746
|
+
if (callerSignal?.aborted === true) abortFromCaller();
|
|
747
|
+
callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
748
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
749
|
+
return {
|
|
750
|
+
dispose: () => {
|
|
751
|
+
clearTimeout(timer);
|
|
752
|
+
callerSignal?.removeEventListener("abort", abortFromCaller);
|
|
753
|
+
},
|
|
754
|
+
signal: controller.signal,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export function createHttpGatewayMediaCatalogTransport(options: Readonly<{
|
|
759
|
+
fetch?: GatewayCatalogFetch;
|
|
760
|
+
timeoutMs?: number;
|
|
761
|
+
}> = {}): GatewayMediaCatalogTransport {
|
|
762
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
763
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CATALOG_TIMEOUT_MS;
|
|
764
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000) {
|
|
765
|
+
throw new GatewayMediaCatalogError("invalid-request");
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
refresh: async (validators, signal) => {
|
|
769
|
+
const combined = combinedAbortSignal(signal, timeoutMs);
|
|
770
|
+
try {
|
|
771
|
+
const response = await fetchImplementation(GATEWAY_MEDIA_CATALOG_URL, {
|
|
772
|
+
headers: {
|
|
773
|
+
accept: "application/json",
|
|
774
|
+
...(validators?.etag === undefined
|
|
775
|
+
? {}
|
|
776
|
+
: { "if-none-match": validators.etag }),
|
|
777
|
+
...(validators?.lastModified === undefined
|
|
778
|
+
? {}
|
|
779
|
+
: { "if-modified-since": validators.lastModified }),
|
|
780
|
+
},
|
|
781
|
+
method: "GET",
|
|
782
|
+
redirect: "error",
|
|
783
|
+
signal: combined.signal,
|
|
784
|
+
});
|
|
785
|
+
if (
|
|
786
|
+
response.redirected
|
|
787
|
+
|| (
|
|
788
|
+
response.status >= 300
|
|
789
|
+
&& response.status < 400
|
|
790
|
+
&& response.status !== 304
|
|
791
|
+
)
|
|
792
|
+
) {
|
|
793
|
+
throw new GatewayMediaCatalogError("catalog-unavailable");
|
|
794
|
+
}
|
|
795
|
+
const responseValidators = parseValidators({
|
|
796
|
+
...(response.headers.get("etag") === null
|
|
797
|
+
? {}
|
|
798
|
+
: { etag: response.headers.get("etag")! }),
|
|
799
|
+
...(response.headers.get("last-modified") === null
|
|
800
|
+
? {}
|
|
801
|
+
: { lastModified: response.headers.get("last-modified")! }),
|
|
802
|
+
});
|
|
803
|
+
if (response.status === 304) {
|
|
804
|
+
return {
|
|
805
|
+
status: "not-modified",
|
|
806
|
+
...(responseValidators === undefined
|
|
807
|
+
? {}
|
|
808
|
+
: { validators: responseValidators }),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
if (response.status !== 200) {
|
|
812
|
+
throw new GatewayMediaCatalogError("catalog-unavailable");
|
|
813
|
+
}
|
|
814
|
+
const bytes = await readBoundedResponse(response, CATALOG_RESPONSE_MAX_BYTES);
|
|
815
|
+
let payload: unknown;
|
|
816
|
+
try {
|
|
817
|
+
payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown;
|
|
818
|
+
} catch {
|
|
819
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
payload,
|
|
823
|
+
status: "modified",
|
|
824
|
+
...(responseValidators === undefined
|
|
825
|
+
? {}
|
|
826
|
+
: { validators: responseValidators }),
|
|
827
|
+
};
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (error instanceof GatewayMediaCatalogError) throw error;
|
|
830
|
+
throw new GatewayMediaCatalogError("catalog-unavailable");
|
|
831
|
+
} finally {
|
|
832
|
+
combined.dispose();
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function errno(error: unknown, code: string): boolean {
|
|
839
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function syncDirectory(path: string): Promise<void> {
|
|
843
|
+
const handle = await open(
|
|
844
|
+
path,
|
|
845
|
+
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0),
|
|
846
|
+
);
|
|
847
|
+
try {
|
|
848
|
+
await handle.sync();
|
|
849
|
+
} finally {
|
|
850
|
+
await handle.close();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
export function createFileGatewayMediaCatalogSnapshotStore(
|
|
855
|
+
path: string,
|
|
856
|
+
): GatewayMediaCatalogSnapshotStore {
|
|
857
|
+
if (!isAbsolute(path)) throw new GatewayMediaCatalogError("invalid-request");
|
|
858
|
+
return {
|
|
859
|
+
read: async () => {
|
|
860
|
+
let handle: Awaited<ReturnType<typeof open>> | null = null;
|
|
861
|
+
try {
|
|
862
|
+
handle = await open(
|
|
863
|
+
path,
|
|
864
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
|
|
865
|
+
);
|
|
866
|
+
const details = await handle.stat();
|
|
867
|
+
if (!details.isFile() || details.size > CATALOG_STORE_MAX_BYTES) {
|
|
868
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
869
|
+
}
|
|
870
|
+
const source = await handle.readFile({ encoding: "utf8" });
|
|
871
|
+
try {
|
|
872
|
+
return JSON.parse(source) as unknown;
|
|
873
|
+
} catch {
|
|
874
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
875
|
+
}
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (errno(error, "ENOENT")) return null;
|
|
878
|
+
if (error instanceof GatewayMediaCatalogError) throw error;
|
|
879
|
+
throw new GatewayMediaCatalogError("catalog-unavailable");
|
|
880
|
+
} finally {
|
|
881
|
+
await handle?.close().catch(() => undefined);
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
write: async (snapshot) => {
|
|
885
|
+
const parent = dirname(path);
|
|
886
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
887
|
+
let handle: Awaited<ReturnType<typeof open>> | null = null;
|
|
888
|
+
try {
|
|
889
|
+
await mkdir(parent, { mode: 0o700, recursive: true });
|
|
890
|
+
const parentDetails = await lstat(parent);
|
|
891
|
+
if (parentDetails.isSymbolicLink() || !parentDetails.isDirectory()) {
|
|
892
|
+
throw new GatewayMediaCatalogError("catalog-unavailable");
|
|
893
|
+
}
|
|
894
|
+
await chmod(parent, 0o700);
|
|
895
|
+
const source = `${canonicalJson(snapshot)}\n`;
|
|
896
|
+
if (new TextEncoder().encode(source).byteLength > CATALOG_STORE_MAX_BYTES) {
|
|
897
|
+
throw new GatewayMediaCatalogError("catalog-invalid");
|
|
898
|
+
}
|
|
899
|
+
handle = await open(
|
|
900
|
+
temporaryPath,
|
|
901
|
+
constants.O_CREAT
|
|
902
|
+
| constants.O_EXCL
|
|
903
|
+
| constants.O_WRONLY
|
|
904
|
+
| (constants.O_NOFOLLOW ?? 0),
|
|
905
|
+
0o600,
|
|
906
|
+
);
|
|
907
|
+
await handle.writeFile(source, { encoding: "utf8" });
|
|
908
|
+
await handle.sync();
|
|
909
|
+
await handle.close();
|
|
910
|
+
handle = null;
|
|
911
|
+
await rename(temporaryPath, path);
|
|
912
|
+
await syncDirectory(parent);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
if (error instanceof GatewayMediaCatalogError) throw error;
|
|
915
|
+
throw new GatewayMediaCatalogError("catalog-unavailable");
|
|
916
|
+
} finally {
|
|
917
|
+
await handle?.close().catch(() => undefined);
|
|
918
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
}
|