@readymadeit/mcp 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -2
- package/bundle/LICENSE +201 -0
- package/bundle/boards/aggregator-demo-board.json +54 -0
- package/bundle/boards/alpaca-board.json +316 -0
- package/bundle/boards/animate-board.json +159 -0
- package/bundle/boards/ascii-art-demo-board.json +33 -0
- package/bundle/boards/ascii-cam-board.json +184 -0
- package/bundle/boards/asset-demo-board.json +54 -0
- package/bundle/boards/audio-encode-demo-board.json +153 -0
- package/bundle/boards/audiolize-board.json +152 -0
- package/bundle/boards/batcher-demo-board.json +43 -0
- package/bundle/boards/buffer-demo-board.json +53 -0
- package/bundle/boards/cache-demo-board.json +59 -0
- package/bundle/boards/camera-demo-board.json +28 -0
- package/bundle/boards/canvas-demo-board.json +55 -0
- package/bundle/boards/changes-demo-board.json +60 -0
- package/bundle/boards/court-booking-browser-demo-board.json +221 -0
- package/bundle/boards/court-booking-demo-board.json +218 -0
- package/bundle/boards/current-time-board.json +32 -0
- package/bundle/boards/debounce-demo-board.json +43 -0
- package/bundle/boards/decrypt-demo-board.json +47 -0
- package/bundle/boards/delay-demo-board.json +28 -0
- package/bundle/boards/detect-demo-board.json +64 -0
- package/bundle/boards/document-extract-demo-board.json +55 -0
- package/bundle/boards/dropitapp-board.json +138 -0
- package/bundle/boards/encode-demo-board.json +39 -0
- package/bundle/boards/encrypt-board.json +185 -0
- package/bundle/boards/encrypt-demo-board.json +39 -0
- package/bundle/boards/face-alert-board.json +181 -0
- package/bundle/boards/fetcher-demo-board.json +40 -0
- package/bundle/boards/fft-demo-board.json +28 -0
- package/bundle/boards/filter-demo-board.json +55 -0
- package/bundle/boards/flat-map-demo-board.json +42 -0
- package/bundle/boards/frame-limit-test-board.json +130 -0
- package/bundle/boards/game-board.json +413 -0
- package/bundle/boards/game-of-life-board.json +139 -0
- package/bundle/boards/game-of-life-demo-board.json +34 -0
- package/bundle/boards/gather-demo-board.json +62 -0
- package/bundle/boards/group-by-demo-board.json +62 -0
- package/bundle/boards/hacker-demo-board.json +45 -0
- package/bundle/boards/hacker-news-board.json +741 -0
- package/bundle/boards/hash-demo-board.json +40 -0
- package/bundle/boards/health-log-browser-demo-board.json +537 -0
- package/bundle/boards/health-log-demo-board.json +498 -0
- package/bundle/boards/helloworld-board.json +57 -0
- package/bundle/boards/hold-demo-board.json +67 -0
- package/bundle/boards/hold-scopes-demo-board.json +141 -0
- package/bundle/boards/http-client-browser-demo-board.json +379 -0
- package/bundle/boards/http-client-demo-board.json +380 -0
- package/bundle/boards/image-picker-demo-board.json +22 -0
- package/bundle/boards/imap-email-demo-board.json +35 -0
- package/bundle/boards/injector-demo-board.json +55 -0
- package/bundle/boards/iterator-demo-board.json +55 -0
- package/bundle/boards/limit-demo-board.json +66 -0
- package/bundle/boards/link-debugger.json +32 -0
- package/bundle/boards/live-location-demo-board.json +93 -0
- package/bundle/boards/live-radio-cloud-demo-board.json +216 -0
- package/bundle/boards/live-radio-cloud-direct-demo-board.json +311 -0
- package/bundle/boards/live-radio-demo-board.json +211 -0
- package/bundle/boards/live-radio-members-demo-board.json +314 -0
- package/bundle/boards/local-storage-demo-board.json +30 -0
- package/bundle/boards/lz-compress-demo-board.json +52 -0
- package/bundle/boards/map-demo-board.json +46 -0
- package/bundle/boards/match-filter-demo-board.json +43 -0
- package/bundle/boards/meeting-poll-browser-demo-board.json +649 -0
- package/bundle/boards/meeting-poll-demo-board.json +650 -0
- package/bundle/boards/microphone-monitor-demo-board.json +28 -0
- package/bundle/boards/microphone-speaker-demo-board.json +86 -0
- package/bundle/boards/monitor-demo-board.json +52 -0
- package/bundle/boards/mounted-endpoint-demo-board-hkp-rt.json +84 -0
- package/bundle/boards/mounted-endpoint-demo-board.json +84 -0
- package/bundle/boards/nested-rhythm-demo-board.json +1160 -0
- package/bundle/boards/next-bus-board.json +52 -0
- package/bundle/boards/next-bus-switch-board.json +79 -0
- package/bundle/boards/night-groove-demo-board.json +742 -0
- package/bundle/boards/noise-alert-board.json +146 -0
- package/bundle/boards/offer-followup-demo-board.json +291 -0
- package/bundle/boards/offer-intake-demo-board.json +289 -0
- package/bundle/boards/offer-intake-hetzner-demo-board.json +286 -0
- package/bundle/boards/offer-intake-imap-demo-board.json +289 -0
- package/bundle/boards/offer-intake-imap-hetzner-demo-board.json +354 -0
- package/bundle/boards/p2p-receiver-demo-board.json +65 -0
- package/bundle/boards/p2p-sender-demo-board.json +94 -0
- package/bundle/boards/pad-groove-demo-board.json +644 -0
- package/bundle/boards/peer-chat-board.json +109 -0
- package/bundle/boards/peer-chat-node-board.json +97 -0
- package/bundle/boards/qr-code-demo-board.json +22 -0
- package/bundle/boards/queue-demo-board.json +239 -0
- package/bundle/boards/reduce-demo-board.json +54 -0
- package/bundle/boards/remote-named-demo-board-hkp-rt.json +61 -0
- package/bundle/boards/remote-named-demo-board.json +61 -0
- package/bundle/boards/rss-demo-board.json +530 -0
- package/bundle/boards/rss-radio-board.json +515 -0
- package/bundle/boards/rss-radio-brief-one-board.json +237 -0
- package/bundle/boards/rss-radio-editor-unit-board.json +555 -0
- package/bundle/boards/rss-radio-library-unit-board.json +293 -0
- package/bundle/boards/rss-radio-unit-board.json +809 -0
- package/bundle/boards/rss-radio-voice-unit-board.json +146 -0
- package/bundle/boards/runtime-configs/hold-example.json +41 -0
- package/bundle/boards/runtime-configs/injector-example.json +23 -0
- package/bundle/boards/runtime-configs/speech-to-text-example.json +35 -0
- package/bundle/boards/runtime-configs/text-generation-example.json +31 -0
- package/bundle/boards/runtime-configs/text-to-speech-example.json +28 -0
- package/bundle/boards/runtime-configs/text-to-speech-inflect-example.json +28 -0
- package/bundle/boards/runtime-configs/ws-audio-playback-example.json +23 -0
- package/bundle/boards/select-demo-board.json +32 -0
- package/bundle/boards/send-ntfy-board.json +52 -0
- package/bundle/boards/shared-costs-demo-board.json +361 -0
- package/bundle/boards/sign-demo-board.json +41 -0
- package/bundle/boards/skill-router-demo-board.json +210 -0
- package/bundle/boards/smooth-demo-board.json +91 -0
- package/bundle/boards/smtp-email-demo-board.json +34 -0
- package/bundle/boards/sort-demo-board.json +55 -0
- package/bundle/boards/sound-demo-board.json +65 -0
- package/bundle/boards/spectral-modifier-board.json +98 -0
- package/bundle/boards/speech-synth-demo-board.json +37 -0
- package/bundle/boards/speech-to-text-demo-board.json +146 -0
- package/bundle/boards/spotify-board.json +75 -0
- package/bundle/boards/spread-demo-board.json +35 -0
- package/bundle/boards/sql-demo-board.json +56 -0
- package/bundle/boards/sql-explorer-board.json +447 -0
- package/bundle/boards/sql-explorer-node-board.json +474 -0
- package/bundle/boards/sql-import-board.json +156 -0
- package/bundle/boards/state-demo-board.json +65 -0
- package/bundle/boards/stopper-demo-board.json +40 -0
- package/bundle/boards/storage-demo-board.json +193 -0
- package/bundle/boards/store-demo-board.json +63 -0
- package/bundle/boards/swimlanes-demo-board.json +197 -0
- package/bundle/boards/switch-demo-board.json +51 -0
- package/bundle/boards/syn-board.json +19 -0
- package/bundle/boards/syn-booking-unit-board.json +998 -0
- package/bundle/boards/syn-conversations-anthropic-demo-board.json +925 -0
- package/bundle/boards/syn-conversations-demo-board.json +925 -0
- package/bundle/boards/syn-hotels-unit-board.json +287 -0
- package/bundle/boards/syn-mail-desk-board.json +25 -0
- package/bundle/boards/syn-mail-desk-unit-board.json +254 -0
- package/bundle/boards/telegram-listener-demo-board.json +31 -0
- package/bundle/boards/text-generation-anthropic-demo-board.json +66 -0
- package/bundle/boards/text-generation-demo-board.json +51 -0
- package/bundle/boards/text-generation-local-demo-board.json +80 -0
- package/bundle/boards/text-to-speech-demo-board.json +58 -0
- package/bundle/boards/timeline-demo-board.json +477 -0
- package/bundle/boards/timer-demo-board.json +35 -0
- package/bundle/boards/tracks-demo-board.json +95 -0
- package/bundle/boards/trigger-pad-demo-board.json +47 -0
- package/bundle/boards/triggerpad-board.json +31 -0
- package/bundle/boards/triggerpad-recorder-board.json +56 -0
- package/bundle/boards/uuid-generator-demo-board.json +166 -0
- package/bundle/boards/voice-assistant-demo-board.json +230 -0
- package/bundle/boards/voice-assistant-hkp-rt-demo-board.json +239 -0
- package/bundle/boards/voice-assistant-local-demo-board.json +232 -0
- package/bundle/boards/voice-assistant-skills-demo-board.json +316 -0
- package/bundle/boards/voice-assistant-skills-local-demo-board.json +316 -0
- package/bundle/boards/xy-pad-demo-board.json +36 -0
- package/bundle/docs/content/board-json.md +277 -0
- package/bundle/docs/content/boards/alpaca-board.md +56 -0
- package/bundle/docs/content/boards/animate-board.md +40 -0
- package/bundle/docs/content/boards/ascii-cam-board.md +37 -0
- package/bundle/docs/content/boards/audiolize-board.md +47 -0
- package/bundle/docs/content/boards/court-booking-browser-demo-board.md +43 -0
- package/bundle/docs/content/boards/court-booking-demo-board.md +120 -0
- package/bundle/docs/content/boards/current-time-board.md +32 -0
- package/bundle/docs/content/boards/dropitapp-board.md +57 -0
- package/bundle/docs/content/boards/encrypt-board.md +36 -0
- package/bundle/docs/content/boards/face-alert-board.md +50 -0
- package/bundle/docs/content/boards/game-board.md +69 -0
- package/bundle/docs/content/boards/game-of-life-board.md +47 -0
- package/bundle/docs/content/boards/hacker-news-board.md +76 -0
- package/bundle/docs/content/boards/health-log-browser-demo-board.md +56 -0
- package/bundle/docs/content/boards/health-log-demo-board.md +138 -0
- package/bundle/docs/content/boards/helloworld-board.md +40 -0
- package/bundle/docs/content/boards/link-debugger.md +31 -0
- package/bundle/docs/content/boards/live-radio-cloud-demo-board.md +70 -0
- package/bundle/docs/content/boards/live-radio-cloud-direct-demo-board.md +89 -0
- package/bundle/docs/content/boards/live-radio-demo-board.md +102 -0
- package/bundle/docs/content/boards/live-radio-members-demo-board.md +91 -0
- package/bundle/docs/content/boards/meeting-poll-browser-demo-board.md +45 -0
- package/bundle/docs/content/boards/meeting-poll-demo-board.md +121 -0
- package/bundle/docs/content/boards/mounted-endpoint-demo-board-hkp-rt.md +50 -0
- package/bundle/docs/content/boards/nested-rhythm-demo-board.md +165 -0
- package/bundle/docs/content/boards/next-bus-board.md +38 -0
- package/bundle/docs/content/boards/next-bus-switch-board.md +44 -0
- package/bundle/docs/content/boards/night-groove-demo-board.md +110 -0
- package/bundle/docs/content/boards/noise-alert-board.md +44 -0
- package/bundle/docs/content/boards/pad-groove-demo-board.md +93 -0
- package/bundle/docs/content/boards/peer-chat-board.md +49 -0
- package/bundle/docs/content/boards/peer-chat-node-board.md +47 -0
- package/bundle/docs/content/boards/rss-radio-board.md +221 -0
- package/bundle/docs/content/boards/send-ntfy-board.md +33 -0
- package/bundle/docs/content/boards/shared-costs-demo-board.md +65 -0
- package/bundle/docs/content/boards/spectral-modifier-board.md +55 -0
- package/bundle/docs/content/boards/spotify-board.md +46 -0
- package/bundle/docs/content/boards/sql-explorer-board.md +70 -0
- package/bundle/docs/content/boards/sql-explorer-node-board.md +76 -0
- package/bundle/docs/content/boards/sql-import-board.md +37 -0
- package/bundle/docs/content/boards/swimlanes-demo-board.md +148 -0
- package/bundle/docs/content/boards/syn-mail-desk-board.md +52 -0
- package/bundle/docs/content/boards/timeline-demo-board.md +115 -0
- package/bundle/docs/content/boards/triggerpad-board.md +32 -0
- package/bundle/docs/content/boards/triggerpad-recorder-board.md +52 -0
- package/bundle/docs/content/boards/voice-assistant-demo-board.md +51 -0
- package/bundle/docs/content/concepts/assets.md +245 -0
- package/bundle/docs/content/concepts/blocks.md +300 -0
- package/bundle/docs/content/concepts/board-assistant.md +307 -0
- package/bundle/docs/content/concepts/board.md +265 -0
- package/bundle/docs/content/concepts/cloud-boards.md +803 -0
- package/bundle/docs/content/concepts/coordinator.md +297 -0
- package/bundle/docs/content/concepts/logging.md +293 -0
- package/bundle/docs/content/concepts/mounts.md +314 -0
- package/bundle/docs/content/concepts/presets.md +360 -0
- package/bundle/docs/content/concepts/remotes.md +423 -0
- package/bundle/docs/content/concepts/runtime.md +664 -0
- package/bundle/docs/content/concepts/scopes.md +256 -0
- package/bundle/docs/content/concepts/service.md +227 -0
- package/bundle/docs/content/concepts/units.md +321 -0
- package/bundle/docs/content/introduction.md +163 -0
- package/bundle/docs/content/llm/circle-text-board.json +55 -0
- package/bundle/docs/content/llm/howto-workflow.md +369 -0
- package/bundle/docs/content/repository.md +161 -0
- package/bundle/docs/content/services/aggregator.md +76 -0
- package/bundle/docs/content/services/analyzer.md +44 -0
- package/bundle/docs/content/services/ascii-art.md +58 -0
- package/bundle/docs/content/services/asset.md +88 -0
- package/bundle/docs/content/services/audio-editor.md +57 -0
- package/bundle/docs/content/services/audio-encode.md +115 -0
- package/bundle/docs/content/services/audio-io.md +173 -0
- package/bundle/docs/content/services/batcher.md +49 -0
- package/bundle/docs/content/services/board-service.md +48 -0
- package/bundle/docs/content/services/browser-sub-service.md +75 -0
- package/bundle/docs/content/services/buffer.md +70 -0
- package/bundle/docs/content/services/cache.md +92 -0
- package/bundle/docs/content/services/camera.md +85 -0
- package/bundle/docs/content/services/canvas.md +196 -0
- package/bundle/docs/content/services/changes.md +87 -0
- package/bundle/docs/content/services/chunked-file-provider.md +63 -0
- package/bundle/docs/content/services/communication-dispatcher.md +190 -0
- package/bundle/docs/content/services/configurator.md +66 -0
- package/bundle/docs/content/services/conversations.md +244 -0
- package/bundle/docs/content/services/debounce.md +45 -0
- package/bundle/docs/content/services/decrypt.md +53 -0
- package/bundle/docs/content/services/delay.md +55 -0
- package/bundle/docs/content/services/detect.md +101 -0
- package/bundle/docs/content/services/document-extract.md +174 -0
- package/bundle/docs/content/services/download.md +67 -0
- package/bundle/docs/content/services/encode.md +63 -0
- package/bundle/docs/content/services/encrypt.md +53 -0
- package/bundle/docs/content/services/fetcher.md +122 -0
- package/bundle/docs/content/services/fft.md +96 -0
- package/bundle/docs/content/services/file-source.md +67 -0
- package/bundle/docs/content/services/filesystem.md +157 -0
- package/bundle/docs/content/services/filter.md +133 -0
- package/bundle/docs/content/services/flat-map.md +62 -0
- package/bundle/docs/content/services/game-of-life.md +93 -0
- package/bundle/docs/content/services/gather.md +135 -0
- package/bundle/docs/content/services/gif-encoder.md +56 -0
- package/bundle/docs/content/services/github.md +85 -0
- package/bundle/docs/content/services/group-by.md +54 -0
- package/bundle/docs/content/services/hacker.md +79 -0
- package/bundle/docs/content/services/hash.md +59 -0
- package/bundle/docs/content/services/hold.md +182 -0
- package/bundle/docs/content/services/http-relay-client.md +48 -0
- package/bundle/docs/content/services/http-uploader.md +62 -0
- package/bundle/docs/content/services/http.md +424 -0
- package/bundle/docs/content/services/if.md +79 -0
- package/bundle/docs/content/services/ifft.md +46 -0
- package/bundle/docs/content/services/image-picker.md +50 -0
- package/bundle/docs/content/services/imap-email.md +104 -0
- package/bundle/docs/content/services/injector.md +129 -0
- package/bundle/docs/content/services/input.md +90 -0
- package/bundle/docs/content/services/iterator.md +186 -0
- package/bundle/docs/content/services/join.md +153 -0
- package/bundle/docs/content/services/limit.md +47 -0
- package/bundle/docs/content/services/local-storage.md +47 -0
- package/bundle/docs/content/services/looper.md +62 -0
- package/bundle/docs/content/services/lz-compress.md +52 -0
- package/bundle/docs/content/services/map.md +257 -0
- package/bundle/docs/content/services/match-filter.md +38 -0
- package/bundle/docs/content/services/microphone-monitor.md +66 -0
- package/bundle/docs/content/services/monitor.md +67 -0
- package/bundle/docs/content/services/ollama.md +116 -0
- package/bundle/docs/content/services/openai-prompt.md +74 -0
- package/bundle/docs/content/services/output.md +107 -0
- package/bundle/docs/content/services/peer-socket.md +61 -0
- package/bundle/docs/content/services/qr-code.md +48 -0
- package/bundle/docs/content/services/queue.md +201 -0
- package/bundle/docs/content/services/reduce.md +51 -0
- package/bundle/docs/content/services/rss.md +232 -0
- package/bundle/docs/content/services/select.md +47 -0
- package/bundle/docs/content/services/sign.md +53 -0
- package/bundle/docs/content/services/skill-router.md +154 -0
- package/bundle/docs/content/services/smooth.md +59 -0
- package/bundle/docs/content/services/smtp-email.md +129 -0
- package/bundle/docs/content/services/sort.md +48 -0
- package/bundle/docs/content/services/sound.md +87 -0
- package/bundle/docs/content/services/speech-synth.md +46 -0
- package/bundle/docs/content/services/speech-to-text.md +172 -0
- package/bundle/docs/content/services/spotify.md +77 -0
- package/bundle/docs/content/services/spread.md +111 -0
- package/bundle/docs/content/services/sql.md +491 -0
- package/bundle/docs/content/services/state.md +57 -0
- package/bundle/docs/content/services/static.md +47 -0
- package/bundle/docs/content/services/stopper.md +55 -0
- package/bundle/docs/content/services/storage.md +155 -0
- package/bundle/docs/content/services/store.md +255 -0
- package/bundle/docs/content/services/sub-service.md +152 -0
- package/bundle/docs/content/services/switch.md +67 -0
- package/bundle/docs/content/services/telegram-listener.md +76 -0
- package/bundle/docs/content/services/text-generation.md +390 -0
- package/bundle/docs/content/services/text-to-speech.md +130 -0
- package/bundle/docs/content/services/timeline.md +333 -0
- package/bundle/docs/content/services/timer.md +113 -0
- package/bundle/docs/content/services/tracks.md +207 -0
- package/bundle/docs/content/services/trigger-pad.md +68 -0
- package/bundle/docs/content/services/uuid-generator.md +48 -0
- package/bundle/docs/content/services/wav-reader.md +66 -0
- package/bundle/docs/content/services/websocket.md +189 -0
- package/bundle/docs/content/services/workflow-board-builder.md +65 -0
- package/bundle/docs/content/services/xy-pad.md +60 -0
- package/bundle/docs/content/targets.md +434 -0
- package/bundle/docs/content/testing.md +263 -0
- package/bundle/docs/content/vocabulary.md +171 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRegistry.tsx +141 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntime.tsx +202 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeApi.ts +306 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeApp.ts +310 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeScope.ts +500 -0
- package/bundle/hkp-frontend/src/runtime/browser/UIRegistry.ts +150 -0
- package/bundle/hkp-frontend/src/runtime/browser/assetResolver.ts +123 -0
- package/bundle/hkp-frontend/src/runtime/browser/pipelineNames.ts +65 -0
- package/bundle/hkp-frontend/src/runtime/browser/registry/Default.ts +237 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Aggregator.ts +127 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AggregatorUI.tsx +83 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Analyzer.tsx +43 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/Bars.tsx +33 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/HorizontalSpectrogram.tsx +53 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/VerticalSpectogram.tsx +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/Waveform.tsx +109 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/index.tsx +99 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AsciiArt.ts +119 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AsciiArtUI.tsx +131 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Asset.ts +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AssetUI.tsx +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioDeviceSettings.tsx +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioEditor.ts +129 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioEditorUI.tsx +549 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioInput.ts +298 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioInputUI.tsx +257 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioOutput.ts +189 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioOutputUI.tsx +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Batcher.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BoardService.tsx +86 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BoardServiceUI.tsx +410 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserGather.ts +311 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserIterator.ts +167 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserSubService.tsx +583 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserSubServiceUI.tsx +187 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserTracks.tsx +477 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Buffer.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Cache.ts +134 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CacheUI.tsx +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Camera.ts +90 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CameraUI.tsx +321 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Canvas.ts +139 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/canvasDraw.ts +543 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/canvasUtils.ts +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/index.tsx +287 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/canvasDraw.test.ts +355 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/canvasUtils.test.ts +89 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/fullscreen.test.tsx +26 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Changes.ts +201 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ChangesUI.tsx +85 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ChunkedFileProvider.tsx +183 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Configurator.ts +258 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ConfiguratorUI.tsx +83 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ConsideredHacker.tsx +28 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Debounce.ts +75 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DebounceUI.tsx +70 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Decrypt.ts +127 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DecryptUI.tsx +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Delay.tsx +110 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Detect.ts +214 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DetectUI.tsx +86 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Download.ts +120 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DownloadUI.tsx +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Encode.ts +138 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/EncodeUI.tsx +52 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Encrypt.ts +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/EncryptUI.tsx +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FFT.tsx +238 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FFTUI.tsx +112 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FeedbackService.ts +247 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FeedbackServiceUI.tsx +35 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Fetcher.tsx +232 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FetcherUI.tsx +154 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FileSource.tsx +171 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Filter.tsx +18 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FilterUI.tsx +89 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FlatMap.ts +68 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FlatMapUI.tsx +38 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLife.ts +210 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeRenderer.ts +182 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeRendererUI.tsx +48 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeUI.tsx +58 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GatherUI.tsx +84 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GifEncoder.tsx +651 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubAPI.ts +294 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubComponents.tsx +389 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSink.tsx +109 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSinkUI.tsx +143 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSource.tsx +86 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSourceUI.tsx +162 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GroupBy.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HTTPUploader.ts +102 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Hacker.ts +73 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HackerUI.tsx +134 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Hash.ts +134 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HashUI.tsx +45 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Hold.ts +288 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HoldUI.tsx +43 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HttpClient.ts +558 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HttpClientUI.tsx +212 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HttpUploaderTokenInjector.ts +149 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/IfService.ts +231 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/IfServiceUI.tsx +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ImagePicker.tsx +118 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Injector.ts +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/InjectorUI.tsx +389 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Input.ts +214 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/InputUI.tsx +71 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/IteratorUI.tsx +59 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LZCompress.ts +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Limit.ts +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LimitUI.tsx +41 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LocalStorage.ts +65 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LocalStorageUI.tsx +72 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Map.ts +14 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MapUI.tsx +334 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MapUIModeAssist.tsx +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MatchFilter.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MicrophoneMonitor.ts +190 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MicrophoneMonitorUI.tsx +94 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Monitor.tsx +15 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MonitorUI.tsx +281 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPrompt.ts +237 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/BusyIndicator.tsx +18 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/PromptInput.tsx +71 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/StreamingPreview.tsx +24 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/index.tsx +156 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OpenAIPrompt.ts +254 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OpenAIPromptUI.tsx +189 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Output.tsx +145 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OutputUI.tsx +118 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerChatTemplate.json +43 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerConnection.ts +260 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocket.tsx +268 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocketReceiverTemplate.json +29 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocketUI.tsx +341 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PlaceholderUI.tsx +72 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/QrCode.ts +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/QrCodeUI.tsx +197 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Select.ts +40 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SelectUI.tsx +41 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ServiceBase.ts +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SetRuntimeVariable.ts +65 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SetRuntimeVariableUI.tsx +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sign.ts +182 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SignUI.tsx +55 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Smooth.ts +101 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SmoothUI.tsx +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sort.ts +59 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SortUI.tsx +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sound.ts +541 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SoundUI.tsx +183 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpeechSynth.ts +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpeechSynthUI.tsx +108 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Spotify.tsx +111 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyAPI.ts +113 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyComponents.tsx +104 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyUI.tsx +186 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Spread.ts +117 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpreadUI.tsx +38 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sql.ts +448 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SqlInspector.tsx +332 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SqlUI.tsx +226 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/State.ts +149 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Stopper.ts +29 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Switch.tsx +419 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SwitchUI.tsx +171 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TickRuleDelta.ts +101 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TickRuleDeltaUI.tsx +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Timeline.ts +709 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/TimelinePanel.tsx +79 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/TimelineRows.tsx +574 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/Transport.tsx +141 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/index.tsx +119 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/model.ts +425 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/parts.tsx +708 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Timer.ts +15 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimerUI.tsx +213 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TracksUI.tsx +81 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TriggerPad.ts +108 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TriggerPadUI.tsx +160 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/UuidGenerator.ts +74 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/UuidGeneratorUI.tsx +59 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WaveformDisplay.tsx +104 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WebsocketClient.ts +114 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WebsocketClientUI.tsx +42 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WorkflowBoardBuilder.ts +499 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WorkflowBoardBuilderUI.tsx +521 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/XYPad.ts +68 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/XYPadUI.tsx +274 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/audio/audioHelper.ts +36 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Batcher.ts +76 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Buffer.ts +142 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Filter.ts +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/GroupBy.ts +101 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Map.ts +543 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/MatchFilter.ts +49 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Monitor.ts +75 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Reduce.ts +74 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Timer.ts +342 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/bundle.js +7375 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/eval.ts +208 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/expressionEval.ts +223 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/helpers.ts +10 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/index.ts +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/camera/videoCapture.ts +78 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/changes-modes.ts +9 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/colorpalette.ts +26 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/DetectionPreview.tsx +76 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/mediapipe.ts +34 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/models/README.md +14 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/models/blaze_face_short_range.tflite +0 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect-modes.ts +7 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/encode-modes.ts +10 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/helpers.ts +300 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/http-client-methods.ts +10 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-database.ts +594 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-dump.ts +192 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-inspect.ts +130 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-inspector.css +155 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-modes.ts +27 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/TESTING_APPROACH.md +169 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/__snapshots__/service-ui.future-enhancements.test.tsx.snap +5 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/audio-input-permissions.test.ts +42 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/audiolizeLanes.test.ts +70 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/browser-scopes.test.ts +305 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/cache-service.test.ts +53 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/camera-video-capture.test.ts +24 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/changes-service.test.ts +133 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/crypto-secrets.test.ts +196 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/detect-service.test.ts +119 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/download.test.ts +100 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/expressionEval.test.ts +117 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/fetcher-panel.test.tsx +35 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/fetcher-service.test.ts +80 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/filter-service.test.ts +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/github-secrets.test.ts +108 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/group-by-board.test.ts +42 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/hold-service.test.ts +199 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/http-client-service.test.ts +474 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/http-uploader-token-injector.test.ts +152 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/iterator-gather-service.test.ts +277 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/map-service.test.ts +1047 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/map-ui.test.tsx +70 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/nested-rhythm-board.test.ts +311 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/night-groove-board.test.ts +290 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/pad-groove-board.test.ts +230 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/peer-host-discovery.test.ts +77 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/peer-socket-discovery.test.ts +69 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/pipeline-names.test.ts +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/secret-canary.test.ts +121 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-modules.contract.test.ts +229 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.advanced.test.tsx +670 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.future-enhancements.test.tsx +219 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.integration.test.tsx +432 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.smoke.test.tsx +140 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sound-service.test.ts +293 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/spread-service.test.ts +137 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-dump.test.ts +172 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-inspect.test.ts +163 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-inspector.test.tsx +121 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql.test.ts +702 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sub-service-in-block.test.tsx +84 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/switch-service.test.ts +254 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-board.test.ts +218 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-service.test.ts +1002 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-ui-model.test.ts +230 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-ui.test.tsx +371 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timer-service.test.ts +902 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/tracks-service.test.ts +275 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/tracks-ui.test.tsx +126 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/triggerpad-recorder-board.test.ts +100 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/uuid-generator-service.test.ts +214 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/timeline-core.ts +410 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/DefaultWorkflowBoard.ts +90 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/RefinementSession.ts +121 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/SystemPromptCatalog.ts +245 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/catalog/browser-service-specs.json +110 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/catalog/runtime-capabilities.json +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/asset-service.test.ts +73 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/board-persistence.integration.test.tsx +391 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/browser-runtime.identity-binding.test.tsx +81 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/demo-boards.regression.test.tsx +386 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/failed-call-lifecycle.test.ts +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/http-client.integration.test.ts +146 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/nested-run.integration.test.ts +415 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/replay-next.test.ts +391 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/rerun.test.ts +71 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/run-no-input.test.ts +72 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-add.integration.test.tsx +256 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-operations.integration.test.tsx +310 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-processing.integration.test.ts +311 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-removal.integration.test.tsx +454 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-update.identity.integration.test.tsx +145 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/service-operations.integration.test.tsx +511 -0
- package/bundle/hkp-node/src/address.ts +72 -0
- package/bundle/hkp-node/src/assets.ts +582 -0
- package/bundle/hkp-node/src/auth.ts +369 -0
- package/bundle/hkp-node/src/callReport.ts +281 -0
- package/bundle/hkp-node/src/coordinator/auth.ts +85 -0
- package/bundle/hkp-node/src/coordinator/binaryFrame.ts +189 -0
- package/bundle/hkp-node/src/coordinator/boardStore.ts +0 -0
- package/bundle/hkp-node/src/coordinator/bridge.ts +242 -0
- package/bundle/hkp-node/src/coordinator/bridgeProtocol.ts +200 -0
- package/bundle/hkp-node/src/coordinator/coordinator.ts +594 -0
- package/bundle/hkp-node/src/coordinator/facadeAccess.ts +426 -0
- package/bundle/hkp-node/src/coordinator/fileBoardStore.ts +159 -0
- package/bundle/hkp-node/src/coordinator/frameLimits.ts +41 -0
- package/bundle/hkp-node/src/coordinator/index.ts +12 -0
- package/bundle/hkp-node/src/coordinator/join.ts +62 -0
- package/bundle/hkp-node/src/coordinator/logStore.ts +335 -0
- package/bundle/hkp-node/src/coordinator/members.ts +133 -0
- package/bundle/hkp-node/src/coordinator/mount.ts +124 -0
- package/bundle/hkp-node/src/coordinator/participantProtocol.ts +169 -0
- package/bundle/hkp-node/src/coordinator/participants.ts +666 -0
- package/bundle/hkp-node/src/coordinator/router.ts +416 -0
- package/bundle/hkp-node/src/coordinator/session.ts +1610 -0
- package/bundle/hkp-node/src/coordinator/types.ts +95 -0
- package/bundle/hkp-node/src/coordinatorLinks.ts +712 -0
- package/bundle/hkp-node/src/credentialedFetch.ts +122 -0
- package/bundle/hkp-node/src/egress.ts +344 -0
- package/bundle/hkp-node/src/expressionEval.ts +223 -0
- package/bundle/hkp-node/src/index.ts +470 -0
- package/bundle/hkp-node/src/instance.ts +172 -0
- package/bundle/hkp-node/src/mounts.ts +296 -0
- package/bundle/hkp-node/src/origins.ts +201 -0
- package/bundle/hkp-node/src/reset.ts +187 -0
- package/bundle/hkp-node/src/runtime.ts +1345 -0
- package/bundle/hkp-node/src/secrets.ts +292 -0
- package/bundle/hkp-node/src/server.ts +2120 -0
- package/bundle/hkp-node/src/services/asset.ts +150 -0
- package/bundle/hkp-node/src/services/claudeModels.ts +370 -0
- package/bundle/hkp-node/src/services/communication-dispatcher.ts +735 -0
- package/bundle/hkp-node/src/services/conversations.ts +914 -0
- package/bundle/hkp-node/src/services/database.ts +386 -0
- package/bundle/hkp-node/src/services/document-extract.ts +575 -0
- package/bundle/hkp-node/src/services/expression.ts +417 -0
- package/bundle/hkp-node/src/services/fileStore.ts +369 -0
- package/bundle/hkp-node/src/services/filesystem.ts +280 -0
- package/bundle/hkp-node/src/services/gather.ts +335 -0
- package/bundle/hkp-node/src/services/hold.ts +275 -0
- package/bundle/hkp-node/src/services/http-client.ts +525 -0
- package/bundle/hkp-node/src/services/http-server.ts +1222 -0
- package/bundle/hkp-node/src/services/imap-email.ts +627 -0
- package/bundle/hkp-node/src/services/injector.ts +135 -0
- package/bundle/hkp-node/src/services/iterator.ts +203 -0
- package/bundle/hkp-node/src/services/join.ts +226 -0
- package/bundle/hkp-node/src/services/map.ts +632 -0
- package/bundle/hkp-node/src/services/monitor.ts +128 -0
- package/bundle/hkp-node/src/services/nested-pipeline.ts +405 -0
- package/bundle/hkp-node/src/services/peer-server.ts +268 -0
- package/bundle/hkp-node/src/services/queue-store.ts +301 -0
- package/bundle/hkp-node/src/services/queue.ts +316 -0
- package/bundle/hkp-node/src/services/recordStore.ts +341 -0
- package/bundle/hkp-node/src/services/rss-parse.ts +281 -0
- package/bundle/hkp-node/src/services/rss.ts +392 -0
- package/bundle/hkp-node/src/services/smtp-email.ts +398 -0
- package/bundle/hkp-node/src/services/spread.ts +153 -0
- package/bundle/hkp-node/src/services/sql-dump.ts +192 -0
- package/bundle/hkp-node/src/services/sql.ts +429 -0
- package/bundle/hkp-node/src/services/stopper.ts +69 -0
- package/bundle/hkp-node/src/services/storage.ts +332 -0
- package/bundle/hkp-node/src/services/store.ts +609 -0
- package/bundle/hkp-node/src/services/stream.ts +367 -0
- package/bundle/hkp-node/src/services/sub-service.ts +600 -0
- package/bundle/hkp-node/src/services/telegram-listener.ts +263 -0
- package/bundle/hkp-node/src/services/telegram-sender.ts +136 -0
- package/bundle/hkp-node/src/services/text-generation.ts +1249 -0
- package/bundle/hkp-node/src/services/timer.ts +273 -0
- package/bundle/hkp-node/src/services/tracks.ts +381 -0
- package/bundle/hkp-node/src/services/websocket-reader.ts +364 -0
- package/bundle/hkp-node/src/types.ts +465 -0
- package/bundle/hkp-node/src/wsTickets.ts +77 -0
- package/bundle/hkp-node/src/yas.ts +95 -0
- package/bundle/hkp-python/src/hkp/__init__.py +1 -0
- package/bundle/hkp-python/src/hkp/__main__.py +241 -0
- package/bundle/hkp-python/src/hkp/address.py +56 -0
- package/bundle/hkp-python/src/hkp/assets.py +439 -0
- package/bundle/hkp-python/src/hkp/auth.py +252 -0
- package/bundle/hkp-python/src/hkp/binary_frame.py +100 -0
- package/bundle/hkp-python/src/hkp/call_report.py +201 -0
- package/bundle/hkp-python/src/hkp/coordinator_links.py +667 -0
- package/bundle/hkp-python/src/hkp/data.py +118 -0
- package/bundle/hkp-python/src/hkp/egress.py +310 -0
- package/bundle/hkp-python/src/hkp/mount.py +46 -0
- package/bundle/hkp-python/src/hkp/mounts.py +288 -0
- package/bundle/hkp-python/src/hkp/origins.py +169 -0
- package/bundle/hkp-python/src/hkp/runtime.py +1131 -0
- package/bundle/hkp-python/src/hkp/secrets.py +261 -0
- package/bundle/hkp-python/src/hkp/server.py +1714 -0
- package/bundle/hkp-python/src/hkp/services/__init__.py +0 -0
- package/bundle/hkp-python/src/hkp/services/asset.py +123 -0
- package/bundle/hkp-python/src/hkp/services/audio_encode.py +190 -0
- package/bundle/hkp-python/src/hkp/services/expression.py +942 -0
- package/bundle/hkp-python/src/hkp/services/gather.py +266 -0
- package/bundle/hkp-python/src/hkp/services/hold.py +246 -0
- package/bundle/hkp-python/src/hkp/services/http_client.py +444 -0
- package/bundle/hkp-python/src/hkp/services/http_server.py +790 -0
- package/bundle/hkp-python/src/hkp/services/iterator.py +141 -0
- package/bundle/hkp-python/src/hkp/services/join.py +173 -0
- package/bundle/hkp-python/src/hkp/services/map_service.py +452 -0
- package/bundle/hkp-python/src/hkp/services/monitor.py +99 -0
- package/bundle/hkp-python/src/hkp/services/nested_pipeline.py +278 -0
- package/bundle/hkp-python/src/hkp/services/skill_router.py +388 -0
- package/bundle/hkp-python/src/hkp/services/speech_to_text.py +186 -0
- package/bundle/hkp-python/src/hkp/services/spread.py +114 -0
- package/bundle/hkp-python/src/hkp/services/stopper.py +59 -0
- package/bundle/hkp-python/src/hkp/services/sub_service.py +417 -0
- package/bundle/hkp-python/src/hkp/services/text_generation.py +498 -0
- package/bundle/hkp-python/src/hkp/services/text_to_speech.py +237 -0
- package/bundle/hkp-python/src/hkp/services/timer.py +267 -0
- package/bundle/hkp-python/src/hkp/services/tracks.py +251 -0
- package/bundle/hkp-python/src/hkp/types.py +412 -0
- package/bundle/hkp-python/src/hkp/ws_tickets.py +54 -0
- package/bundle/hkp-python/src/hkp/yas.py +192 -0
- package/bundle/hkp-rt/lib/CMakeLists.txt +193 -0
- package/bundle/hkp-rt/lib/hkp-bundles/CMakeLists.txt +55 -0
- package/bundle/hkp-rt/lib/hkp-bundles/plugin_register.cpp +14 -0
- package/bundle/hkp-rt/lib/include/app.h +201 -0
- package/bundle/hkp-rt/lib/include/assets.h +239 -0
- package/bundle/hkp-rt/lib/include/auth.h +167 -0
- package/bundle/hkp-rt/lib/include/call_report.h +197 -0
- package/bundle/hkp-rt/lib/include/coordinator_links.h +127 -0
- package/bundle/hkp-rt/lib/include/discovery_hooks.h +29 -0
- package/bundle/hkp-rt/lib/include/egress.h +379 -0
- package/bundle/hkp-rt/lib/include/files.h +275 -0
- package/bundle/hkp-rt/lib/include/log_entry.h +131 -0
- package/bundle/hkp-rt/lib/include/mounts.h +139 -0
- package/bundle/hkp-rt/lib/include/origins.h +363 -0
- package/bundle/hkp-rt/lib/include/registry.h +119 -0
- package/bundle/hkp-rt/lib/include/secrets.h +414 -0
- package/bundle/hkp-rt/lib/include/server.h +122 -0
- package/bundle/hkp-rt/lib/include/service.h +190 -0
- package/bundle/hkp-rt/lib/include/slot_store.h +41 -0
- package/bundle/hkp-rt/lib/include/standalone_config.h +253 -0
- package/bundle/hkp-rt/lib/include/types/audio.h +3 -0
- package/bundle/hkp-rt/lib/include/types/data.h +152 -0
- package/bundle/hkp-rt/lib/include/types/message.h +56 -0
- package/bundle/hkp-rt/lib/include/types/models.h +154 -0
- package/bundle/hkp-rt/lib/include/types/owns_me.h +19 -0
- package/bundle/hkp-rt/lib/include/types/ringbuffer.h +86 -0
- package/bundle/hkp-rt/lib/include/types/types.h +33 -0
- package/bundle/hkp-rt/lib/include/types/validation.h +205 -0
- package/bundle/hkp-rt/lib/src/address.h +88 -0
- package/bundle/hkp-rt/lib/src/app.cpp +591 -0
- package/bundle/hkp-rt/lib/src/assets.cpp +686 -0
- package/bundle/hkp-rt/lib/src/binary_frame.h +154 -0
- package/bundle/hkp-rt/lib/src/common/expression.h +1481 -0
- package/bundle/hkp-rt/lib/src/common/inja.h +36 -0
- package/bundle/hkp-rt/lib/src/common/link_socket.cpp +437 -0
- package/bundle/hkp-rt/lib/src/common/link_socket.h +97 -0
- package/bundle/hkp-rt/lib/src/common/realtime_wakeup.h +120 -0
- package/bundle/hkp-rt/lib/src/common/stream_broadcast.h +250 -0
- package/bundle/hkp-rt/lib/src/common/string_util.h +31 -0
- package/bundle/hkp-rt/lib/src/common/websocket_client_session.cpp +217 -0
- package/bundle/hkp-rt/lib/src/common/websocket_client_session.h +37 -0
- package/bundle/hkp-rt/lib/src/common/websocket_config.h +119 -0
- package/bundle/hkp-rt/lib/src/common/websocket_protocol.h +32 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server.cpp +82 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server.h +57 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_listener.cpp +115 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_listener.h +35 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_session.cpp +172 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_session.h +53 -0
- package/bundle/hkp-rt/lib/src/common.h +13 -0
- package/bundle/hkp-rt/lib/src/coordinator_links.cpp +1142 -0
- package/bundle/hkp-rt/lib/src/discovery/discovery.cpp +707 -0
- package/bundle/hkp-rt/lib/src/discovery/discovery.h +89 -0
- package/bundle/hkp-rt/lib/src/http/auth.cpp +569 -0
- package/bundle/hkp-rt/lib/src/http/front_door.cpp +518 -0
- package/bundle/hkp-rt/lib/src/http/front_door.h +131 -0
- package/bundle/hkp-rt/lib/src/http/server.cpp +1539 -0
- package/bundle/hkp-rt/lib/src/http/ws_tickets.h +125 -0
- package/bundle/hkp-rt/lib/src/mount.h +63 -0
- package/bundle/hkp-rt/lib/src/process_context.h +335 -0
- package/bundle/hkp-rt/lib/src/process_depth.h +20 -0
- package/bundle/hkp-rt/lib/src/registry.cpp +233 -0
- package/bundle/hkp-rt/lib/src/runtime.cpp +986 -0
- package/bundle/hkp-rt/lib/src/runtime.h +225 -0
- package/bundle/hkp-rt/lib/src/runtime_host.h +180 -0
- package/bundle/hkp-rt/lib/src/service.cpp +315 -0
- package/bundle/hkp-rt/lib/src/services/asset.h +185 -0
- package/bundle/hkp-rt/lib/src/services/audio_encode.cpp +372 -0
- package/bundle/hkp-rt/lib/src/services/audio_encode.h +97 -0
- package/bundle/hkp-rt/lib/src/services/cache.h +362 -0
- package/bundle/hkp-rt/lib/src/services/cache_subservices.h +463 -0
- package/bundle/hkp-rt/lib/src/services/core_audio.cpp +384 -0
- package/bundle/hkp-rt/lib/src/services/core_audio.h +22 -0
- package/bundle/hkp-rt/lib/src/services/core_input.h +337 -0
- package/bundle/hkp-rt/lib/src/services/core_output.h +370 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/detail.cpp +749 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/detail.h +29 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/ffmpeg.cpp +193 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/ffmpeg.h +40 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/types.h +120 -0
- package/bundle/hkp-rt/lib/src/services/fft_service.h +190 -0
- package/bundle/hkp-rt/lib/src/services/filesystem.h +309 -0
- package/bundle/hkp-rt/lib/src/services/filter.h +208 -0
- package/bundle/hkp-rt/lib/src/services/gather.h +272 -0
- package/bundle/hkp-rt/lib/src/services/hold.h +304 -0
- package/bundle/hkp-rt/lib/src/services/http_client.cpp +347 -0
- package/bundle/hkp-rt/lib/src/services/http_client.h +67 -0
- package/bundle/hkp-rt/lib/src/services/http_client_impl.h +45 -0
- package/bundle/hkp-rt/lib/src/services/http_client_template_examples.md +202 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_listener.cpp +130 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_listener.h +33 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server.cpp +351 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server.h +79 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_impl.cpp +230 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_impl.h +64 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_subservices.cpp +950 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_subservices.h +253 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_session.cpp +495 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_session.h +169 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_stream_listener.cpp +270 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_stream_listener.h +104 -0
- package/bundle/hkp-rt/lib/src/services/http_server/request_decode.h +164 -0
- package/bundle/hkp-rt/lib/src/services/if_service.h +210 -0
- package/bundle/hkp-rt/lib/src/services/ifft_service.h +198 -0
- package/bundle/hkp-rt/lib/src/services/injector.h +162 -0
- package/bundle/hkp-rt/lib/src/services/iterator.h +201 -0
- package/bundle/hkp-rt/lib/src/services/map.h +641 -0
- package/bundle/hkp-rt/lib/src/services/monitor.h +161 -0
- package/bundle/hkp-rt/lib/src/services/mp3_frames.h +119 -0
- package/bundle/hkp-rt/lib/src/services/mp4_to_wav.cpp +281 -0
- package/bundle/hkp-rt/lib/src/services/mp4_to_wav.h +263 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_registry.cpp +88 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_registry.h +43 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server.h +163 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_listener.cpp +133 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_listener.h +46 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_session.cpp +165 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_session.h +44 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_ws_session.cpp +184 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_ws_session.h +55 -0
- package/bundle/hkp-rt/lib/src/services/receiver_session.h +172 -0
- package/bundle/hkp-rt/lib/src/services/root_certificates.h +3959 -0
- package/bundle/hkp-rt/lib/src/services/sha256_openssl.h +49 -0
- package/bundle/hkp-rt/lib/src/services/speech_http.h +306 -0
- package/bundle/hkp-rt/lib/src/services/speech_to_text.cpp +595 -0
- package/bundle/hkp-rt/lib/src/services/speech_to_text.h +69 -0
- package/bundle/hkp-rt/lib/src/services/static.cpp +45 -0
- package/bundle/hkp-rt/lib/src/services/static.h +41 -0
- package/bundle/hkp-rt/lib/src/services/stopper.h +72 -0
- package/bundle/hkp-rt/lib/src/services/sub_service.h +286 -0
- package/bundle/hkp-rt/lib/src/services/text_generation.cpp +1132 -0
- package/bundle/hkp-rt/lib/src/services/text_generation.h +71 -0
- package/bundle/hkp-rt/lib/src/services/text_to_speech.cpp +853 -0
- package/bundle/hkp-rt/lib/src/services/text_to_speech.h +97 -0
- package/bundle/hkp-rt/lib/src/services/timer.h +300 -0
- package/bundle/hkp-rt/lib/src/services/tracks.h +397 -0
- package/bundle/hkp-rt/lib/src/services/tts_inflect.cpp +149 -0
- package/bundle/hkp-rt/lib/src/services/tts_inflect.h +82 -0
- package/bundle/hkp-rt/lib/src/services/wav_reader.h +213 -0
- package/bundle/hkp-rt/lib/src/services/websocket_client_service.h +182 -0
- package/bundle/hkp-rt/lib/src/services/websocket_reader.h +148 -0
- package/bundle/hkp-rt/lib/src/services/websocket_server_service.h +196 -0
- package/bundle/hkp-rt/lib/src/services/websocket_writer.cpp +641 -0
- package/bundle/hkp-rt/lib/src/services/websocket_writer.h +117 -0
- package/bundle/hkp-rt/lib/src/sub_runtime.cpp +234 -0
- package/bundle/hkp-rt/lib/src/sub_runtime.h +160 -0
- package/bundle/hkp-rt/lib/src/types/data.cpp +237 -0
- package/bundle/hkp-rt/lib/src/types/message.cpp +148 -0
- package/bundle/hkp-rt/lib/src/types/models.cpp +83 -0
- package/bundle/hkp-rt/lib/src/types/ringbuffer.cpp +336 -0
- package/bundle/hkp-rt/lib/src/uuid.h +12 -0
- package/bundle/readymade-mcp-commits.json +5 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +79 -0
- package/dist/git.d.ts +8 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +53 -0
- package/dist/http.d.ts +17 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +130 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/repository.d.ts +49 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +452 -0
- package/dist/schemas.d.ts +156 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +43 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +83 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/validation.d.ts +9 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +101 -0
- package/package.json +52 -4
|
@@ -0,0 +1,2120 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { AddressInfo } from "node:net";
|
|
3
|
+
import { Duplex } from "node:stream";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import cors from "cors";
|
|
7
|
+
import express, { NextFunction, Request, Response } from "express";
|
|
8
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
9
|
+
|
|
10
|
+
import { MapService, mapDescriptor } from "./services/map";
|
|
11
|
+
import { MonitorService, monitorDescriptor } from "./services/monitor";
|
|
12
|
+
import { SubService, subServiceDescriptor } from "./services/sub-service";
|
|
13
|
+
import { IteratorService, iteratorDescriptor } from "./services/iterator";
|
|
14
|
+
import { GatherService, gatherDescriptor } from "./services/gather";
|
|
15
|
+
import { SpreadService, spreadDescriptor } from "./services/spread";
|
|
16
|
+
import { TracksService, tracksDescriptor } from "./services/tracks";
|
|
17
|
+
import {
|
|
18
|
+
CommunicationDispatcherService,
|
|
19
|
+
communicationDispatcherDescriptor,
|
|
20
|
+
} from "./services/communication-dispatcher";
|
|
21
|
+
import { JoinService, joinDescriptor } from "./services/join";
|
|
22
|
+
import {
|
|
23
|
+
HttpServerSubservicesService,
|
|
24
|
+
httpServerSubservicesDescriptor,
|
|
25
|
+
} from "./services/http-server";
|
|
26
|
+
import {
|
|
27
|
+
HttpClientService,
|
|
28
|
+
httpClientDescriptor,
|
|
29
|
+
} from "./services/http-client";
|
|
30
|
+
import { StopperService, stopperDescriptor } from "./services/stopper";
|
|
31
|
+
import { TimerService, timerDescriptor } from "./services/timer";
|
|
32
|
+
import {
|
|
33
|
+
PeerServerService,
|
|
34
|
+
peerServerDescriptor,
|
|
35
|
+
} from "./services/peer-server";
|
|
36
|
+
import {
|
|
37
|
+
WebsocketReaderService,
|
|
38
|
+
websocketReaderDescriptor,
|
|
39
|
+
} from "./services/websocket-reader";
|
|
40
|
+
import {
|
|
41
|
+
ImapEmailService,
|
|
42
|
+
imapEmailDescriptor,
|
|
43
|
+
} from "./services/imap-email";
|
|
44
|
+
import {
|
|
45
|
+
TelegramListenerService,
|
|
46
|
+
telegramListenerDescriptor,
|
|
47
|
+
} from "./services/telegram-listener";
|
|
48
|
+
import {
|
|
49
|
+
TelegramSenderService,
|
|
50
|
+
telegramSenderDescriptor,
|
|
51
|
+
} from "./services/telegram-sender";
|
|
52
|
+
import {
|
|
53
|
+
SmtpEmailService,
|
|
54
|
+
smtpEmailDescriptor,
|
|
55
|
+
} from "./services/smtp-email";
|
|
56
|
+
import { HoldService, holdDescriptor } from "./services/hold";
|
|
57
|
+
import {
|
|
58
|
+
createFileRecordStore,
|
|
59
|
+
createMemoryRecordStore,
|
|
60
|
+
RecordStore,
|
|
61
|
+
} from "./services/recordStore";
|
|
62
|
+
import {
|
|
63
|
+
FileStore,
|
|
64
|
+
checkFilePath,
|
|
65
|
+
checkVolumeName,
|
|
66
|
+
createDiskFileStore,
|
|
67
|
+
createMemoryFileStore,
|
|
68
|
+
} from "./services/fileStore";
|
|
69
|
+
import { FilesystemService, filesystemDescriptor } from "./services/filesystem";
|
|
70
|
+
import { StorageService, storageDescriptor } from "./services/storage";
|
|
71
|
+
import { StoreService, storeDescriptor } from "./services/store";
|
|
72
|
+
import { SqlService, sqlDescriptor } from "./services/sql";
|
|
73
|
+
import { QueueService, queueDescriptor } from "./services/queue";
|
|
74
|
+
import {
|
|
75
|
+
ConversationsService,
|
|
76
|
+
conversationsDescriptor,
|
|
77
|
+
} from "./services/conversations";
|
|
78
|
+
import {
|
|
79
|
+
DatabaseStore,
|
|
80
|
+
createFileDatabaseStore,
|
|
81
|
+
createMemoryDatabaseStore,
|
|
82
|
+
} from "./services/database";
|
|
83
|
+
import {
|
|
84
|
+
DocumentExtractService,
|
|
85
|
+
documentExtractDescriptor,
|
|
86
|
+
} from "./services/document-extract";
|
|
87
|
+
import {
|
|
88
|
+
TextGenerationService,
|
|
89
|
+
textGenerationDescriptor,
|
|
90
|
+
} from "./services/text-generation";
|
|
91
|
+
import { InjectorService, injectorDescriptor } from "./services/injector";
|
|
92
|
+
import { RssService, rssDescriptor } from "./services/rss";
|
|
93
|
+
import { AssetService, assetDescriptor } from "./services/asset";
|
|
94
|
+
import {
|
|
95
|
+
boardSpace,
|
|
96
|
+
contextForClient,
|
|
97
|
+
contextFromLink,
|
|
98
|
+
contextToWire,
|
|
99
|
+
HostedRuntime,
|
|
100
|
+
newRun,
|
|
101
|
+
RuntimeApp,
|
|
102
|
+
TenantRuntimes,
|
|
103
|
+
} from "./runtime";
|
|
104
|
+
import { MountRegistry } from "./mounts";
|
|
105
|
+
import { outgoingPayload } from "./callReport";
|
|
106
|
+
import { MessagePurpose, decodeYasMessage, encodeYasBinary } from "./yas";
|
|
107
|
+
import {
|
|
108
|
+
AllowedOrigins,
|
|
109
|
+
AuthConfig,
|
|
110
|
+
AuthenticatedUser,
|
|
111
|
+
Authenticator,
|
|
112
|
+
AuthenticatorOptions,
|
|
113
|
+
createAuthenticator,
|
|
114
|
+
isOriginAllowed,
|
|
115
|
+
ownerKeyOf,
|
|
116
|
+
} from "./auth";
|
|
117
|
+
import {
|
|
118
|
+
admitsWithoutCredential,
|
|
119
|
+
isForwarded,
|
|
120
|
+
allowsOrigin,
|
|
121
|
+
allowsOriginWithoutCredential,
|
|
122
|
+
} from "./origins";
|
|
123
|
+
import {
|
|
124
|
+
HostedServiceFactory,
|
|
125
|
+
JsonRecord,
|
|
126
|
+
LogEntry,
|
|
127
|
+
LogLevel,
|
|
128
|
+
RuntimeConfiguration,
|
|
129
|
+
RuntimeNotification,
|
|
130
|
+
ServiceConfiguration,
|
|
131
|
+
} from "./types";
|
|
132
|
+
import { readSecretsPayload, referencedSecrets } from "./secrets";
|
|
133
|
+
import { socketCeiling } from "./coordinator/frameLimits";
|
|
134
|
+
import { TICKET_TTL_MS, TicketHolder, WsTickets } from "./wsTickets";
|
|
135
|
+
import {
|
|
136
|
+
AssetDescriptor,
|
|
137
|
+
DEFAULT_MAX_INLINE_ASSET_BYTES,
|
|
138
|
+
readAssetsPayload,
|
|
139
|
+
} from "./assets";
|
|
140
|
+
import {
|
|
141
|
+
CoordinatorLinks,
|
|
142
|
+
CoordinatorLinksOptions,
|
|
143
|
+
LinkedRuntime,
|
|
144
|
+
LinkStore,
|
|
145
|
+
createFileLinkStore,
|
|
146
|
+
createMemoryLinkStore,
|
|
147
|
+
} from "./coordinatorLinks";
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Per-tenant limits. Runtimes, services and timers all consume resources on a
|
|
151
|
+
* shared host, so without a cap one tenant can degrade the server for everyone.
|
|
152
|
+
* Unset (or 0) means unlimited, which is the right default for a single-user
|
|
153
|
+
* instance and for local development.
|
|
154
|
+
*/
|
|
155
|
+
export type Quotas = {
|
|
156
|
+
maxRuntimesPerUser?: number;
|
|
157
|
+
maxServicesPerRuntime?: number;
|
|
158
|
+
minTimerIntervalMs?: number;
|
|
159
|
+
/** Largest accepted request body on a public service endpoint; 0 disables. */
|
|
160
|
+
maxRequestBodyBytes?: number;
|
|
161
|
+
/**
|
|
162
|
+
* Largest content an asset descriptor may carry itself (`text`, `base64`).
|
|
163
|
+
* Unset keeps the default. Content past it is named by URL, which a runtime
|
|
164
|
+
* fetches for itself.
|
|
165
|
+
*/
|
|
166
|
+
maxInlineAssetBytes?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Largest body of a request that carries asset descriptors: creating a
|
|
169
|
+
* runtime, pushing assets to one, deploying a board. In effect how much
|
|
170
|
+
* inline content one runtime — or one deployed board — may be handed at
|
|
171
|
+
* once, counted as it travels: base64 for bytes, JSON-escaped for text.
|
|
172
|
+
* Unset keeps the default.
|
|
173
|
+
*/
|
|
174
|
+
maxAssetRequestBodyBytes?: number;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Service endpoints are reachable without a token, so an unbounded body read is
|
|
179
|
+
* available to anyone holding the URL. Unlike the other quotas this one defaults
|
|
180
|
+
* to a real value rather than "unlimited": leaving it off would make the
|
|
181
|
+
* dangerous choice the automatic one.
|
|
182
|
+
*/
|
|
183
|
+
const DEFAULT_MAX_REQUEST_BODY_BYTES = 25 * 1024 * 1024;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* What a request carrying asset descriptors may weigh when nothing says
|
|
187
|
+
* otherwise. A quota of its own rather than a multiple of the inline limit: a
|
|
188
|
+
* runtime's create payload carries every asset its services reference and a
|
|
189
|
+
* deployed board carries all of its own, so this is what bounds how much
|
|
190
|
+
* inline content a board may hold, however many assets it is spread over.
|
|
191
|
+
*/
|
|
192
|
+
const DEFAULT_MAX_ASSET_REQUEST_BODY_BYTES = 32 * 1024 * 1024;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The largest body a request carrying asset descriptors may have, in bytes.
|
|
196
|
+
* What was configured, as it stands. Left to the default it is never less
|
|
197
|
+
* than one asset at the inline limit takes to travel — base64, four
|
|
198
|
+
* characters for three bytes, and room for the rest of the request — so that
|
|
199
|
+
* raising the inline limit alone does not leave it out of reach.
|
|
200
|
+
*/
|
|
201
|
+
function assetRequestBodyLimit(quotas: Quotas, maxInlineAssetBytes: number): number {
|
|
202
|
+
if (quotas.maxAssetRequestBodyBytes && quotas.maxAssetRequestBodyBytes > 0) {
|
|
203
|
+
return quotas.maxAssetRequestBodyBytes;
|
|
204
|
+
}
|
|
205
|
+
return Math.max(
|
|
206
|
+
DEFAULT_MAX_ASSET_REQUEST_BODY_BYTES,
|
|
207
|
+
Math.ceil((maxInlineAssetBytes * 4) / 3) + 1024 * 1024,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Whether a request is one that carries asset descriptors: provisioning a
|
|
213
|
+
* runtime, pushing to one, and deploying a board to a coordinator.
|
|
214
|
+
*/
|
|
215
|
+
function carriesAssets(req: Request): boolean {
|
|
216
|
+
return (
|
|
217
|
+
req.method === "POST" &&
|
|
218
|
+
(req.path === "/runtimes" ||
|
|
219
|
+
/^\/runtimes\/[^/]+\/assets$/.test(req.path) ||
|
|
220
|
+
/^\/coordinator\/users\/[^/]+\/boards$/.test(req.path))
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Which runtime server this is, reported beside the runtimes so a client can
|
|
226
|
+
* tell remote runtimes apart without reading their address.
|
|
227
|
+
*/
|
|
228
|
+
const RUNTIME_SERVER_KIND = "node";
|
|
229
|
+
|
|
230
|
+
type CreateRuntimeServerOptions = {
|
|
231
|
+
auth?: AuthConfig;
|
|
232
|
+
quotas?: Quotas;
|
|
233
|
+
/**
|
|
234
|
+
* Builds the authenticator, defaulting to a JWKS-backed one for `auth`.
|
|
235
|
+
* Overriding it replaces only how a raw token is verified — the server still
|
|
236
|
+
* resolves its own session tokens first, by passing the resolver it owns into
|
|
237
|
+
* whatever this returns.
|
|
238
|
+
*/
|
|
239
|
+
buildAuthenticator?: (options: AuthenticatorOptions) => Authenticator;
|
|
240
|
+
allowedOrigins?: AllowedOrigins;
|
|
241
|
+
externalHost?: string;
|
|
242
|
+
externalSecure?: boolean;
|
|
243
|
+
host?: string;
|
|
244
|
+
name?: string;
|
|
245
|
+
/**
|
|
246
|
+
* Where `store` keeps what boards remember, or a store to use as given.
|
|
247
|
+
*
|
|
248
|
+
* Absent means memory: a runtime nobody told where to persist keeps records
|
|
249
|
+
* for as long as it runs and no longer, which is the honest default for a
|
|
250
|
+
* server that may be running from a checkout.
|
|
251
|
+
*/
|
|
252
|
+
recordStore?: RecordStore | string;
|
|
253
|
+
/**
|
|
254
|
+
* Where SQL databases live, one file per board. Absent or empty means memory:
|
|
255
|
+
* a runtime nobody told where to persist keeps them for as long as it runs.
|
|
256
|
+
*/
|
|
257
|
+
database?: string;
|
|
258
|
+
/**
|
|
259
|
+
* Where `filesystem` keeps the files boards write, or a store to use as
|
|
260
|
+
* given. Absent or empty means memory, as with the two stores above — bytes
|
|
261
|
+
* a runtime was not told where to put outlive nothing.
|
|
262
|
+
*/
|
|
263
|
+
files?: FileStore | string;
|
|
264
|
+
/**
|
|
265
|
+
* Keys the derivation of public mount addresses; see MountRegistry.
|
|
266
|
+
*
|
|
267
|
+
* Absent draws one per process, so endpoints work but change on restart.
|
|
268
|
+
* `index.ts` persists one so a webhook configured elsewhere keeps working.
|
|
269
|
+
*/
|
|
270
|
+
mountSecret?: string;
|
|
271
|
+
/**
|
|
272
|
+
* Where the tickets this server connects to coordinators with are kept, or a
|
|
273
|
+
* store to use as given. Absent or empty means memory: the links work, and
|
|
274
|
+
* are not re-established after a restart.
|
|
275
|
+
*/
|
|
276
|
+
coordinatorLinks?: LinkStore | string;
|
|
277
|
+
/** Timing of those connections; the defaults suit a real deployment. */
|
|
278
|
+
coordinatorLinkOptions?: CoordinatorLinksOptions;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/** Refused for being over a per-tenant limit; carries what to tell the caller. */
|
|
282
|
+
class QuotaError extends Error {}
|
|
283
|
+
|
|
284
|
+
/** A coordinator session token, bound to the user it was minted for and the
|
|
285
|
+
* runtime it grants access to. */
|
|
286
|
+
type SessionToken = {
|
|
287
|
+
sub: string;
|
|
288
|
+
runtimeId: string;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Runtime ids are unique per tenant, not globally, so anything keyed by runtime
|
|
293
|
+
* outside a tenant view (socket sets, mounts) must be keyed by both. NUL cannot
|
|
294
|
+
* occur in either part, so the join is unambiguous.
|
|
295
|
+
*/
|
|
296
|
+
function tenantKey(owner: string, runtimeId: string): string {
|
|
297
|
+
return `${owner}\u0000${runtimeId}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
type WsInboundMessage = {
|
|
301
|
+
type?: string;
|
|
302
|
+
params?: unknown;
|
|
303
|
+
/** The run this call belongs to, as its caller named it; see ProcessContext. */
|
|
304
|
+
context?: unknown;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* What a runtime socket frame asks for. Text frames are JSON; a binary frame is
|
|
309
|
+
* a YAS message carrying bytes, which JSON cannot — the peer sends one for a
|
|
310
|
+
* pass that is bytes (encoded audio, say), and it runs the pipeline with those
|
|
311
|
+
* bytes as a Buffer. A binary frame has no room for a run context, so its pass
|
|
312
|
+
* begins a run of its own.
|
|
313
|
+
*/
|
|
314
|
+
function readInbound(
|
|
315
|
+
raw: Buffer | ArrayBuffer | Buffer[],
|
|
316
|
+
isBinary: boolean,
|
|
317
|
+
): WsInboundMessage | null {
|
|
318
|
+
const bytes = Array.isArray(raw)
|
|
319
|
+
? Buffer.concat(raw)
|
|
320
|
+
: Buffer.isBuffer(raw)
|
|
321
|
+
? raw
|
|
322
|
+
: Buffer.from(raw);
|
|
323
|
+
if (!isBinary) {
|
|
324
|
+
try {
|
|
325
|
+
return JSON.parse(bytes.toString());
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const message = decodeYasMessage(bytes);
|
|
331
|
+
// A NOTIFICATION frame answers a request this server never makes, and a type
|
|
332
|
+
// this server does not read has nothing a Node service could use.
|
|
333
|
+
if (
|
|
334
|
+
!message ||
|
|
335
|
+
message.purpose === MessagePurpose.NOTIFICATION ||
|
|
336
|
+
message.data === undefined
|
|
337
|
+
) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return { type: "processRuntime", params: message.data };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** A header a client may have sent more than once, as the first it sent. */
|
|
344
|
+
function singleHeader(value: string | string[] | undefined): string | undefined {
|
|
345
|
+
return Array.isArray(value) ? value[0] : value;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function createRuntimeServer(options: CreateRuntimeServerOptions = {}) {
|
|
349
|
+
// Tests and local dev default to no auth; index.ts always resolves an explicit
|
|
350
|
+
// config and fails closed for the published package (see resolveServerAuthConfig).
|
|
351
|
+
const authConfig: AuthConfig = options.auth ?? { mode: "none" };
|
|
352
|
+
const allowedOrigins: AllowedOrigins = options.allowedOrigins ?? "default";
|
|
353
|
+
|
|
354
|
+
// Coordinator session tokens this runtime has issued (see POST .../session-token).
|
|
355
|
+
// Opaque, in-memory, and bound to the minting user — so they resolve back to a
|
|
356
|
+
// real `sub`, not an unscoped superuser. They live only as long as this process:
|
|
357
|
+
// if the runtime dies, the coordinator must re-provision (which needs a live
|
|
358
|
+
// user JWT). That bound is intentional for v1 — see the session-token route.
|
|
359
|
+
const sessionTokens = new Map<string, SessionToken>();
|
|
360
|
+
const buildAuthenticator =
|
|
361
|
+
options.buildAuthenticator ??
|
|
362
|
+
((authOptions: AuthenticatorOptions) =>
|
|
363
|
+
createAuthenticator(authConfig, authOptions));
|
|
364
|
+
const authenticator: Authenticator = buildAuthenticator({
|
|
365
|
+
resolveOpaqueToken: (token) => {
|
|
366
|
+
const session = sessionTokens.get(token);
|
|
367
|
+
return session ? { sub: session.sub } : null;
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
const externalHost = options.externalHost ?? options.host ?? "127.0.0.1";
|
|
371
|
+
const externalSecure = options.externalSecure ?? false;
|
|
372
|
+
const quotas = options.quotas ?? {};
|
|
373
|
+
const maxInlineAssetBytes =
|
|
374
|
+
quotas.maxInlineAssetBytes && quotas.maxInlineAssetBytes > 0
|
|
375
|
+
? quotas.maxInlineAssetBytes
|
|
376
|
+
: DEFAULT_MAX_INLINE_ASSET_BYTES;
|
|
377
|
+
// Databases follow records below: one store for the whole server, scoped per
|
|
378
|
+
// call, and an empty path saying "keep nothing on disk" rather than naming
|
|
379
|
+
// the working directory as a root.
|
|
380
|
+
const databases: DatabaseStore =
|
|
381
|
+
typeof options.database === "string"
|
|
382
|
+
? options.database
|
|
383
|
+
? createFileDatabaseStore(options.database)
|
|
384
|
+
: createMemoryDatabaseStore()
|
|
385
|
+
: createMemoryDatabaseStore();
|
|
386
|
+
|
|
387
|
+
// One store for the whole server; it is the scope handed to each call, not a
|
|
388
|
+
// store per board, that keeps one board's records out of another's.
|
|
389
|
+
// An empty path is how "keep nothing on disk" is said, and must not be read
|
|
390
|
+
// as a root — which would be the working directory.
|
|
391
|
+
const records: RecordStore =
|
|
392
|
+
typeof options.recordStore === "string"
|
|
393
|
+
? options.recordStore
|
|
394
|
+
? createFileRecordStore(options.recordStore)
|
|
395
|
+
: createMemoryRecordStore()
|
|
396
|
+
: (options.recordStore ?? createMemoryRecordStore());
|
|
397
|
+
|
|
398
|
+
// Files follow records and databases: one store for the server, scoped per
|
|
399
|
+
// call, and an empty path saying "keep nothing on disk".
|
|
400
|
+
const files: FileStore =
|
|
401
|
+
typeof options.files === "string"
|
|
402
|
+
? options.files
|
|
403
|
+
? createDiskFileStore(options.files)
|
|
404
|
+
: createMemoryFileStore()
|
|
405
|
+
: (options.files ?? createMemoryFileStore());
|
|
406
|
+
|
|
407
|
+
/** True when adding one more to `count` would pass the limit (0/unset = no limit). */
|
|
408
|
+
function atQuota(count: number, limit: number | undefined): boolean {
|
|
409
|
+
return !!limit && limit > 0 && count >= limit;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** True when `count` items is already more than the limit allows. */
|
|
413
|
+
function exceedsQuota(count: number, limit: number | undefined): boolean {
|
|
414
|
+
return !!limit && limit > 0 && count > limit;
|
|
415
|
+
}
|
|
416
|
+
const factories = new Map<string, HostedServiceFactory>([
|
|
417
|
+
[
|
|
418
|
+
monitorDescriptor.serviceId,
|
|
419
|
+
{
|
|
420
|
+
descriptor: monitorDescriptor,
|
|
421
|
+
create: (config, _createService) => new MonitorService(config),
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
[
|
|
425
|
+
mapDescriptor.serviceId,
|
|
426
|
+
{
|
|
427
|
+
descriptor: mapDescriptor,
|
|
428
|
+
create: (config, _createService) => new MapService(config),
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
[
|
|
432
|
+
subServiceDescriptor.serviceId,
|
|
433
|
+
{
|
|
434
|
+
descriptor: subServiceDescriptor,
|
|
435
|
+
create: (config, createService) =>
|
|
436
|
+
new SubService(config, createService),
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
[
|
|
440
|
+
iteratorDescriptor.serviceId,
|
|
441
|
+
{
|
|
442
|
+
descriptor: iteratorDescriptor,
|
|
443
|
+
create: (config, createService) =>
|
|
444
|
+
new IteratorService(config, createService),
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
[
|
|
448
|
+
gatherDescriptor.serviceId,
|
|
449
|
+
{
|
|
450
|
+
descriptor: gatherDescriptor,
|
|
451
|
+
create: (config, createService) =>
|
|
452
|
+
new GatherService(config, createService),
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
[
|
|
456
|
+
spreadDescriptor.serviceId,
|
|
457
|
+
{
|
|
458
|
+
descriptor: spreadDescriptor,
|
|
459
|
+
create: (config, _createService) => new SpreadService(config),
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
[
|
|
463
|
+
tracksDescriptor.serviceId,
|
|
464
|
+
{
|
|
465
|
+
descriptor: tracksDescriptor,
|
|
466
|
+
create: (config, createService) => new TracksService(config, createService),
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
[
|
|
470
|
+
communicationDispatcherDescriptor.serviceId,
|
|
471
|
+
{
|
|
472
|
+
descriptor: communicationDispatcherDescriptor,
|
|
473
|
+
create: (config, createService) =>
|
|
474
|
+
new CommunicationDispatcherService(config, createService),
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
[
|
|
478
|
+
joinDescriptor.serviceId,
|
|
479
|
+
{
|
|
480
|
+
descriptor: joinDescriptor,
|
|
481
|
+
create: (config, createService) => new JoinService(config, createService),
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
[
|
|
485
|
+
httpServerSubservicesDescriptor.serviceId,
|
|
486
|
+
{
|
|
487
|
+
descriptor: httpServerSubservicesDescriptor,
|
|
488
|
+
create: (config, createService) =>
|
|
489
|
+
new HttpServerSubservicesService(
|
|
490
|
+
config,
|
|
491
|
+
createService,
|
|
492
|
+
options.quotas?.maxRequestBodyBytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES,
|
|
493
|
+
),
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
[
|
|
497
|
+
timerDescriptor.serviceId,
|
|
498
|
+
{
|
|
499
|
+
descriptor: timerDescriptor,
|
|
500
|
+
create: (config, _createService) =>
|
|
501
|
+
new TimerService(config, options.quotas?.minTimerIntervalMs ?? 0),
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
[
|
|
505
|
+
peerServerDescriptor.serviceId,
|
|
506
|
+
{
|
|
507
|
+
descriptor: peerServerDescriptor,
|
|
508
|
+
create: (config, _createService) => new PeerServerService(config),
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
[
|
|
512
|
+
websocketReaderDescriptor.serviceId,
|
|
513
|
+
{
|
|
514
|
+
descriptor: websocketReaderDescriptor,
|
|
515
|
+
create: (config, _createService) => new WebsocketReaderService(config),
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
[
|
|
519
|
+
httpClientDescriptor.serviceId,
|
|
520
|
+
{
|
|
521
|
+
descriptor: httpClientDescriptor,
|
|
522
|
+
create: (config, _createService) => new HttpClientService(config),
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
[
|
|
526
|
+
stopperDescriptor.serviceId,
|
|
527
|
+
{
|
|
528
|
+
descriptor: stopperDescriptor,
|
|
529
|
+
create: (config, _createService) => new StopperService(config),
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
[
|
|
533
|
+
imapEmailDescriptor.serviceId,
|
|
534
|
+
{
|
|
535
|
+
descriptor: imapEmailDescriptor,
|
|
536
|
+
create: (config, _createService) => new ImapEmailService(config),
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
[
|
|
540
|
+
telegramListenerDescriptor.serviceId,
|
|
541
|
+
{
|
|
542
|
+
descriptor: telegramListenerDescriptor,
|
|
543
|
+
create: (config, _createService) =>
|
|
544
|
+
new TelegramListenerService(config),
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
[
|
|
548
|
+
telegramSenderDescriptor.serviceId,
|
|
549
|
+
{
|
|
550
|
+
descriptor: telegramSenderDescriptor,
|
|
551
|
+
create: (config, _createService) => new TelegramSenderService(config),
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
[
|
|
555
|
+
smtpEmailDescriptor.serviceId,
|
|
556
|
+
{
|
|
557
|
+
descriptor: smtpEmailDescriptor,
|
|
558
|
+
create: (config, _createService) => new SmtpEmailService(config),
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
[
|
|
562
|
+
holdDescriptor.serviceId,
|
|
563
|
+
{
|
|
564
|
+
descriptor: holdDescriptor,
|
|
565
|
+
create: (config, _createService) => new HoldService(config),
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
[
|
|
569
|
+
textGenerationDescriptor.serviceId,
|
|
570
|
+
{
|
|
571
|
+
descriptor: textGenerationDescriptor,
|
|
572
|
+
create: (config, _createService) => new TextGenerationService(config),
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
[
|
|
576
|
+
storeDescriptor.serviceId,
|
|
577
|
+
{
|
|
578
|
+
descriptor: storeDescriptor,
|
|
579
|
+
create: (config, _createService) => new StoreService(config, records),
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
[
|
|
583
|
+
sqlDescriptor.serviceId,
|
|
584
|
+
{
|
|
585
|
+
descriptor: sqlDescriptor,
|
|
586
|
+
create: (config, _createService) => new SqlService(config, databases),
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
[
|
|
590
|
+
filesystemDescriptor.serviceId,
|
|
591
|
+
{
|
|
592
|
+
descriptor: filesystemDescriptor,
|
|
593
|
+
create: (config, _createService) => new FilesystemService(config, files),
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
[
|
|
597
|
+
storageDescriptor.serviceId,
|
|
598
|
+
{
|
|
599
|
+
descriptor: storageDescriptor,
|
|
600
|
+
create: (config, createService) => new StorageService(config, createService),
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
[
|
|
604
|
+
conversationsDescriptor.serviceId,
|
|
605
|
+
{
|
|
606
|
+
descriptor: conversationsDescriptor,
|
|
607
|
+
create: (config, _createService) =>
|
|
608
|
+
new ConversationsService(config, databases),
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
[
|
|
612
|
+
queueDescriptor.serviceId,
|
|
613
|
+
{
|
|
614
|
+
descriptor: queueDescriptor,
|
|
615
|
+
create: (config, _createService) => new QueueService(config, databases),
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
[
|
|
619
|
+
documentExtractDescriptor.serviceId,
|
|
620
|
+
{
|
|
621
|
+
descriptor: documentExtractDescriptor,
|
|
622
|
+
create: (config, _createService) => new DocumentExtractService(config),
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
[
|
|
626
|
+
injectorDescriptor.serviceId,
|
|
627
|
+
{
|
|
628
|
+
descriptor: injectorDescriptor,
|
|
629
|
+
create: (config, _createService) => new InjectorService(config),
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
[
|
|
633
|
+
rssDescriptor.serviceId,
|
|
634
|
+
{
|
|
635
|
+
descriptor: rssDescriptor,
|
|
636
|
+
create: (config, _createService) => new RssService(config),
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
[
|
|
640
|
+
assetDescriptor.serviceId,
|
|
641
|
+
{
|
|
642
|
+
descriptor: assetDescriptor,
|
|
643
|
+
create: (config, _createService) => new AssetService(config),
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
]);
|
|
647
|
+
|
|
648
|
+
// Public service endpoints. Declared before the runtime app because runtimes
|
|
649
|
+
// hand mounts to their services as they are created.
|
|
650
|
+
const mounts = new MountRegistry(
|
|
651
|
+
(mountPath) => {
|
|
652
|
+
const address = httpServer.address();
|
|
653
|
+
if (!address || typeof address === "string") {
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
return externalSecure
|
|
657
|
+
? `https://${externalHost}${mountPath}`
|
|
658
|
+
: `http://${externalHost}:${address.port}${mountPath}`;
|
|
659
|
+
},
|
|
660
|
+
options.mountSecret,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
const runtimeApp = new RuntimeApp(
|
|
664
|
+
factories,
|
|
665
|
+
(owner, runtimeId, space) => ({
|
|
666
|
+
mount: (serviceUuid, handlers, options) =>
|
|
667
|
+
mounts.register(owner, runtimeId, serviceUuid, handlers, {
|
|
668
|
+
...options,
|
|
669
|
+
space,
|
|
670
|
+
}),
|
|
671
|
+
}),
|
|
672
|
+
// A `file://` asset is a file in one of the tenant's volumes —
|
|
673
|
+
// `file:///<volume>/<path>` — read through the same store and the same
|
|
674
|
+
// checks as `filesystem`. Nothing outside a volume is ever read: a shared
|
|
675
|
+
// board naming a path on this machine is refused, not served.
|
|
676
|
+
async (scope, url) => {
|
|
677
|
+
const [volume, ...rest] = decodeURIComponent(url.pathname)
|
|
678
|
+
.split("/")
|
|
679
|
+
.filter((segment) => segment !== "");
|
|
680
|
+
if (!volume || url.host || checkVolumeName(volume)) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
const checked = checkFilePath(rest.join("/"));
|
|
684
|
+
if ("error" in checked) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
const found = await files.read({ ...scope, volume }, checked.path);
|
|
688
|
+
return found?.bytes ?? null;
|
|
689
|
+
},
|
|
690
|
+
{ maxInlineBytes: maxInlineAssetBytes },
|
|
691
|
+
);
|
|
692
|
+
// A server without auth lets a request in for where it comes from, so it
|
|
693
|
+
// asks that this is not a foreign page; see origins.ts. With auth a request
|
|
694
|
+
// is let in for the token it carries, and the origin list says only which
|
|
695
|
+
// pages may send one.
|
|
696
|
+
const noAuth = authConfig.mode === "none";
|
|
697
|
+
const ownNames = [externalHost];
|
|
698
|
+
const admitsUnauthenticated = (request: http.IncomingMessage): boolean =>
|
|
699
|
+
admitsWithoutCredential(
|
|
700
|
+
{
|
|
701
|
+
origin: request.headers.origin,
|
|
702
|
+
secFetchSite: singleHeader(request.headers["sec-fetch-site"]),
|
|
703
|
+
host: request.headers.host,
|
|
704
|
+
forwarded: isForwarded(request.headers),
|
|
705
|
+
},
|
|
706
|
+
allowedOrigins,
|
|
707
|
+
ownNames,
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
const expressApp = express();
|
|
711
|
+
expressApp.use(
|
|
712
|
+
cors({
|
|
713
|
+
// Which page may read an answer: the one asking, when it is one this
|
|
714
|
+
// server allows. Without auth there are no credentials, so `*` — any
|
|
715
|
+
// page, with one — allows nobody.
|
|
716
|
+
origin: (origin, callback) =>
|
|
717
|
+
callback(
|
|
718
|
+
null,
|
|
719
|
+
noAuth
|
|
720
|
+
? allowsOriginWithoutCredential(origin, allowedOrigins)
|
|
721
|
+
: allowsOrigin(origin, allowedOrigins),
|
|
722
|
+
),
|
|
723
|
+
// PATCH: the page switching Capture mode on a runtime while it runs
|
|
724
|
+
// (`PATCH /runtimes/:id/state`), which nothing else sends from a browser.
|
|
725
|
+
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
|
726
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
727
|
+
}),
|
|
728
|
+
);
|
|
729
|
+
// Ahead of everything that reads the request. Answered with nothing a page
|
|
730
|
+
// could read — the CORS headers above were not set for it — so to the page
|
|
731
|
+
// that sent it this is what a server that is not there looks like.
|
|
732
|
+
if (noAuth) {
|
|
733
|
+
expressApp.use((req, res, next) => {
|
|
734
|
+
if (admitsUnauthenticated(req)) {
|
|
735
|
+
next();
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
res.status(403).end();
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
// Not strict: a JSON `null` is a payload here, and the one that means "no
|
|
742
|
+
// input". A pipeline is started with it whenever nothing precedes the first
|
|
743
|
+
// service — a panel's Send button, an external trigger — and the strict
|
|
744
|
+
// parser rejects a bare `null` before a route sees it, turning that into a
|
|
745
|
+
// 400. The process routes tell the two apart themselves: no body at all is
|
|
746
|
+
// `undefined` and refused, `null` runs the pipeline with nothing on its
|
|
747
|
+
// input. Every other route already checks the shape it needs.
|
|
748
|
+
//
|
|
749
|
+
// Requests that carry asset descriptors are larger than any other control
|
|
750
|
+
// request, by design: inline content travels in them. They get a limit of
|
|
751
|
+
// their own, derived from the largest inline asset a runtime takes — and
|
|
752
|
+
// only once the caller is known, so that the larger body is not something
|
|
753
|
+
// anyone can make this server read.
|
|
754
|
+
const controlBody = express.json({ strict: false });
|
|
755
|
+
const assetBody = express.json({
|
|
756
|
+
strict: false,
|
|
757
|
+
limit: assetRequestBodyLimit(quotas, maxInlineAssetBytes),
|
|
758
|
+
});
|
|
759
|
+
expressApp.use((req, res, next) =>
|
|
760
|
+
carriesAssets(req) ? next() : controlBody(req, res, next),
|
|
761
|
+
);
|
|
762
|
+
// Routes that authenticate their own callers, as "METHOD /path". Every
|
|
763
|
+
// other request passes the check below, which asks the server's allowlist —
|
|
764
|
+
// and that is who may *own* things here. A route listed says who may call it
|
|
765
|
+
// by something narrower, and is named explicitly, the way `upgradeRoutes`
|
|
766
|
+
// names the join endpoint: nothing gets past this check by its shape.
|
|
767
|
+
const selfAuthenticatedRoutes = new Set<string>();
|
|
768
|
+
expressApp.use((req, res, next) =>
|
|
769
|
+
selfAuthenticatedRoutes.has(`${req.method} ${req.path}`)
|
|
770
|
+
? next()
|
|
771
|
+
: authenticator.middleware(req, res, next),
|
|
772
|
+
);
|
|
773
|
+
expressApp.use((req, res, next) =>
|
|
774
|
+
carriesAssets(req) ? assetBody(req, res, next) : next(),
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// Mounts are matched before Express so they bypass CORS and the auth
|
|
778
|
+
// middleware entirely: they exist to be called by outside parties (webhooks,
|
|
779
|
+
// uploads, PeerJS clients) that hold no token. Their unguessable mount id is
|
|
780
|
+
// what gates access.
|
|
781
|
+
const httpServer = http.createServer((req, res) => {
|
|
782
|
+
if (mounts.handleRequest(req, res)) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
expressApp(req, res);
|
|
786
|
+
});
|
|
787
|
+
const webSocketServer = new WebSocketServer({ noServer: true });
|
|
788
|
+
// Keyed by tenantKey(owner, runtimeId), not runtimeId — see tenantKey.
|
|
789
|
+
const runtimeSockets = new Map<string, Set<WebSocket>>();
|
|
790
|
+
|
|
791
|
+
/** The caller's runtime namespace. Every route resolves runtimes through it. */
|
|
792
|
+
function tenantOf(req: Request): TenantRuntimes {
|
|
793
|
+
return runtimeApp.forOwner(ownerKeyOf(req.authenticatedUser));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function runtimeOutputUrl(runtimeId: string): string | undefined {
|
|
797
|
+
const address = httpServer.address();
|
|
798
|
+
if (!address || typeof address === "string") {
|
|
799
|
+
return undefined;
|
|
800
|
+
}
|
|
801
|
+
if (externalSecure) {
|
|
802
|
+
return `wss://${externalHost}/${runtimeId}`;
|
|
803
|
+
}
|
|
804
|
+
return `ws://${externalHost}:${address.port}/${runtimeId}`;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function serializeRuntime(runtime: HostedRuntime) {
|
|
808
|
+
return runtime.serialize(runtimeOutputUrl(runtime.id));
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Carry a log entry to whoever is collecting this runtime's output.
|
|
813
|
+
*
|
|
814
|
+
* The same socket a notification takes, and for the same reason: it is the
|
|
815
|
+
* connection the board's coordinator already holds, authenticated with a
|
|
816
|
+
* credential minted to outlive the user's session. An entry differs in what
|
|
817
|
+
* it is for — a notification is for whoever is watching, an entry has to
|
|
818
|
+
* survive with nobody attached — but not in how it travels.
|
|
819
|
+
*/
|
|
820
|
+
function sendJsonLog(socketKey: string, entry: LogEntry) {
|
|
821
|
+
const sockets = runtimeSockets.get(socketKey);
|
|
822
|
+
if (!sockets || sockets.size === 0) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const message = JSON.stringify({ type: "log", entry });
|
|
827
|
+
for (const socket of sockets) {
|
|
828
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
829
|
+
socket.send(message);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function sendJsonNotification(
|
|
835
|
+
socketKey: string,
|
|
836
|
+
notification: RuntimeNotification,
|
|
837
|
+
) {
|
|
838
|
+
const sockets = runtimeSockets.get(socketKey);
|
|
839
|
+
if (!sockets || sockets.size === 0) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const message = JSON.stringify({
|
|
844
|
+
type: "notification",
|
|
845
|
+
instanceId: notification.instanceId,
|
|
846
|
+
value: JSON.stringify(notification.payload),
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
for (const socket of sockets) {
|
|
850
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
851
|
+
socket.send(message);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function sendJsonResult(socket: WebSocket, result: unknown) {
|
|
857
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
// Bytes as bytes: JSON would spell each one out as a numbered key.
|
|
861
|
+
if (result instanceof Uint8Array) {
|
|
862
|
+
socket.send(encodeYasBinary(result, MessagePurpose.RESULT));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
socket.send(JSON.stringify({ type: "result", data: result }));
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Resolve a runtime inside the caller's namespace. A runtime owned by another
|
|
870
|
+
* tenant is reported as 404 rather than 403, so runtime ids belonging to other
|
|
871
|
+
* users cannot be probed for existence.
|
|
872
|
+
*/
|
|
873
|
+
function getRuntimeOr404(
|
|
874
|
+
req: Request,
|
|
875
|
+
res: Response,
|
|
876
|
+
runtimeId: string,
|
|
877
|
+
): HostedRuntime | null {
|
|
878
|
+
const runtime = tenantOf(req).getRuntime(runtimeId);
|
|
879
|
+
if (!runtime) {
|
|
880
|
+
res.sendStatus(404);
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
return runtime;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Tear a runtime down and drop any session tokens it issued, so a dead
|
|
887
|
+
// runtime's tokens can't linger as valid credentials.
|
|
888
|
+
/** Removes a board's runtime; nothing a client created is touched. */
|
|
889
|
+
function removeLinkedRuntime(linked: LinkedRuntime): void {
|
|
890
|
+
const space = boardSpace(linked.owner, linked.boardName);
|
|
891
|
+
runtimeApp.removeRuntime(space, linked.runtimeId);
|
|
892
|
+
mounts.releaseRuntime(space, linked.runtimeId);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function linkedRuntime(linked: LinkedRuntime): HostedRuntime | undefined {
|
|
896
|
+
return runtimeApp.getRuntime(
|
|
897
|
+
boardSpace(linked.owner, linked.boardName),
|
|
898
|
+
linked.runtimeId,
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function removeRuntimeAndSessions(owner: string, runtimeId: string): void {
|
|
903
|
+
runtimeApp.removeRuntime(owner, runtimeId);
|
|
904
|
+
mounts.releaseRuntime(owner, runtimeId);
|
|
905
|
+
for (const [token, info] of sessionTokens) {
|
|
906
|
+
if (info.sub === owner && info.runtimeId === runtimeId) {
|
|
907
|
+
sessionTokens.delete(token);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Builds a runtime for a tenant, replacing anything under that id, and wires
|
|
914
|
+
* what it says to whoever is listening.
|
|
915
|
+
*
|
|
916
|
+
* A runtime a client asked for lives in the tenant's own space and speaks to
|
|
917
|
+
* the sockets watching it. One built for a coordinator (`linked`) lives in
|
|
918
|
+
* its board's space and speaks to that coordinator; see `boardSpace`.
|
|
919
|
+
*
|
|
920
|
+
* Provisioning creates. Attaching to a runtime that is already running is a
|
|
921
|
+
* different intent and has its own verb — GET /runtimes/:id, which a client
|
|
922
|
+
* uses before posting when it means "take back over" rather than "build
|
|
923
|
+
* this".
|
|
924
|
+
*
|
|
925
|
+
* Replacing rather than reusing matters most for the flag the config
|
|
926
|
+
* carries: reusing would keep the *old* runtime's lifecycle, so a board
|
|
927
|
+
* deployed to a coordinator could inherit a browser's "clean me up when I
|
|
928
|
+
* disconnect" and vanish when that browser closed.
|
|
929
|
+
*/
|
|
930
|
+
function provisionRuntime(
|
|
931
|
+
owner: string,
|
|
932
|
+
config: RuntimeConfiguration,
|
|
933
|
+
linked?: LinkedRuntime,
|
|
934
|
+
): HostedRuntime {
|
|
935
|
+
const space = linked ? boardSpace(linked.owner, linked.boardName) : owner;
|
|
936
|
+
const replacing = runtimeApp.getRuntime(space, config.id);
|
|
937
|
+
|
|
938
|
+
// Quotas apply only to genuinely new runtimes — replacing one that already
|
|
939
|
+
// exists must never be refused for being over the limit.
|
|
940
|
+
if (
|
|
941
|
+
!replacing &&
|
|
942
|
+
atQuota(runtimeApp.countRuntimes(owner), quotas.maxRuntimesPerUser)
|
|
943
|
+
) {
|
|
944
|
+
throw new QuotaError(
|
|
945
|
+
`Runtime limit reached (${quotas.maxRuntimesPerUser})`,
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
if (exceedsQuota(config.services.length, quotas.maxServicesPerRuntime)) {
|
|
949
|
+
throw new QuotaError(
|
|
950
|
+
`Service limit reached (${quotas.maxServicesPerRuntime})`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const runtime = runtimeApp.createRuntime(owner, config, space);
|
|
955
|
+
if (linked) {
|
|
956
|
+
runtime.registerNotificationTarget((notification) => {
|
|
957
|
+
coordinatorLinks.emit(linked, {
|
|
958
|
+
type: "notification",
|
|
959
|
+
serviceUuid: notification.instanceId,
|
|
960
|
+
// A coordinator always listens: what it is sent of a call is what
|
|
961
|
+
// Capture mode allows, as for any other listener.
|
|
962
|
+
payload: outgoingPayload(notification.payload, runtime.capturesCalls()),
|
|
963
|
+
context: contextToWire(notification.context),
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
runtime.registerLogTarget((entry) => {
|
|
967
|
+
coordinatorLinks.emit(linked, { type: "log", entry });
|
|
968
|
+
});
|
|
969
|
+
runtime.registerResultTarget((result, context) => {
|
|
970
|
+
// With the run it was emitted in, so the coordinator can tell the
|
|
971
|
+
// next runtime which run this continues and who began it.
|
|
972
|
+
coordinatorLinks.emit(linked, {
|
|
973
|
+
type: "result",
|
|
974
|
+
data: result,
|
|
975
|
+
context: contextToWire(context),
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
return runtime;
|
|
979
|
+
}
|
|
980
|
+
const socketKey = tenantKey(owner, runtime.id);
|
|
981
|
+
runtime.registerNotificationTarget((notification) => {
|
|
982
|
+
sendJsonNotification(socketKey, {
|
|
983
|
+
...notification,
|
|
984
|
+
payload: outgoingPayload(notification.payload, runtime.capturesCalls()),
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
runtime.registerLogTarget((entry) => {
|
|
988
|
+
sendJsonLog(socketKey, entry);
|
|
989
|
+
});
|
|
990
|
+
runtime.registerResultTarget((result) => {
|
|
991
|
+
const sockets = runtimeSockets.get(socketKey);
|
|
992
|
+
if (!sockets) return;
|
|
993
|
+
for (const socket of sockets) {
|
|
994
|
+
sendJsonResult(socket, result);
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
return runtime;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* What a coordinator may do here, over a connection this server opened to
|
|
1002
|
+
* it: the operations on one runtime, as the tenant who introduced the link.
|
|
1003
|
+
* The same things the REST routes below do for a caller holding a token.
|
|
1004
|
+
*/
|
|
1005
|
+
const coordinatorLinks = new CoordinatorLinks(
|
|
1006
|
+
{
|
|
1007
|
+
kind: RUNTIME_SERVER_KIND,
|
|
1008
|
+
registry: () => runtimeApp.getRegistry(),
|
|
1009
|
+
runtimeExists: (linked) => !!linkedRuntime(linked),
|
|
1010
|
+
provision: (linked, payload, secrets) => {
|
|
1011
|
+
const config = validateRuntimeConfiguration({
|
|
1012
|
+
id: linked.runtimeId,
|
|
1013
|
+
name: payload.name,
|
|
1014
|
+
boardName: payload.boardName,
|
|
1015
|
+
// The coordinator's until it says otherwise: a deployed board keeps
|
|
1016
|
+
// running with nobody watching.
|
|
1017
|
+
garbageCollected: false,
|
|
1018
|
+
state: payload.state,
|
|
1019
|
+
services: payload.services,
|
|
1020
|
+
assets: payload.assets,
|
|
1021
|
+
});
|
|
1022
|
+
if (!config) {
|
|
1023
|
+
throw new Error("The board's description of this runtime is malformed");
|
|
1024
|
+
}
|
|
1025
|
+
// The values this server was handed for the runtime, by the person's
|
|
1026
|
+
// own client. They do not come from the coordinator and never go to it.
|
|
1027
|
+
config.secrets = secrets;
|
|
1028
|
+
const runtime = provisionRuntime(linked.owner, config, linked);
|
|
1029
|
+
const held = new Set(runtime.secrets().aliases());
|
|
1030
|
+
const missingSecrets = referencedSecrets(
|
|
1031
|
+
config.services.map((service) => service.state),
|
|
1032
|
+
).filter((alias) => !held.has(alias));
|
|
1033
|
+
return {
|
|
1034
|
+
registry: runtimeApp.getRegistry(),
|
|
1035
|
+
services: runtime.listServices(),
|
|
1036
|
+
missingSecrets,
|
|
1037
|
+
};
|
|
1038
|
+
},
|
|
1039
|
+
describe: (linked) => {
|
|
1040
|
+
const runtime = linkedRuntime(linked);
|
|
1041
|
+
return runtime ? { services: runtime.listServices() } : null;
|
|
1042
|
+
},
|
|
1043
|
+
configureService: async (linked, serviceUuid, config, context) => {
|
|
1044
|
+
const runtime = linkedRuntime(linked);
|
|
1045
|
+
if (!runtime) {
|
|
1046
|
+
throw new Error("the runtime is not running");
|
|
1047
|
+
}
|
|
1048
|
+
if (!isJsonRecord(config)) {
|
|
1049
|
+
throw new Error("a service is configured with an object");
|
|
1050
|
+
}
|
|
1051
|
+
const run = contextFromLink(context) ?? newRun();
|
|
1052
|
+
if (!runtime.configureService(serviceUuid, config, run)) {
|
|
1053
|
+
throw new Error(`no service "${serviceUuid}"`);
|
|
1054
|
+
}
|
|
1055
|
+
return waitForServiceActivationState(runtime, serviceUuid);
|
|
1056
|
+
},
|
|
1057
|
+
processService: (linked, serviceUuid, params, context, done) => {
|
|
1058
|
+
const runtime = linkedRuntime(linked);
|
|
1059
|
+
if (!runtime) {
|
|
1060
|
+
throw new Error("the runtime is not running");
|
|
1061
|
+
}
|
|
1062
|
+
if (typeof serviceUuid !== "string" || !runtime.getService(serviceUuid)) {
|
|
1063
|
+
throw new Error(`no service "${serviceUuid}"`);
|
|
1064
|
+
}
|
|
1065
|
+
// As on `process` below: the run and its caller are the coordinator's
|
|
1066
|
+
// to state, over this link and nowhere else.
|
|
1067
|
+
const run = contextFromLink(context) ?? newRun();
|
|
1068
|
+
void runtime
|
|
1069
|
+
.processAt(
|
|
1070
|
+
serviceUuid,
|
|
1071
|
+
params,
|
|
1072
|
+
() => {
|
|
1073
|
+
// Notifications are broadcast through runtime notification targets.
|
|
1074
|
+
},
|
|
1075
|
+
run,
|
|
1076
|
+
)
|
|
1077
|
+
.then(
|
|
1078
|
+
(data) => done({ data, context: contextToWire(run) }),
|
|
1079
|
+
(err) => {
|
|
1080
|
+
console.error(
|
|
1081
|
+
`[coordinator-link] Runtime "${linked.runtimeId}" failed to process at "${serviceUuid}":`,
|
|
1082
|
+
err instanceof Error ? err.message : err,
|
|
1083
|
+
);
|
|
1084
|
+
},
|
|
1085
|
+
);
|
|
1086
|
+
},
|
|
1087
|
+
setState: (linked, state) => {
|
|
1088
|
+
const runtime = linkedRuntime(linked);
|
|
1089
|
+
if (!runtime) {
|
|
1090
|
+
throw new Error("the runtime is not running");
|
|
1091
|
+
}
|
|
1092
|
+
return applyRuntimeState(runtime, state);
|
|
1093
|
+
},
|
|
1094
|
+
remove: (linked) => removeLinkedRuntime(linked),
|
|
1095
|
+
process: async (linked, params, context) => {
|
|
1096
|
+
const runtime = linkedRuntime(linked);
|
|
1097
|
+
if (!runtime) {
|
|
1098
|
+
throw new Error("the runtime is not running");
|
|
1099
|
+
}
|
|
1100
|
+
// The coordinator names the run its call belongs to, so that a board
|
|
1101
|
+
// spanning several runtimes reads as one trace — and who began it,
|
|
1102
|
+
// which is taken as stated on this path and on no other.
|
|
1103
|
+
const run = contextFromLink(context) ?? newRun();
|
|
1104
|
+
const data = await runtime.process(
|
|
1105
|
+
params,
|
|
1106
|
+
() => {
|
|
1107
|
+
// Notifications are broadcast through runtime notification targets.
|
|
1108
|
+
},
|
|
1109
|
+
run,
|
|
1110
|
+
);
|
|
1111
|
+
return { data, context: contextToWire(run) };
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
typeof options.coordinatorLinks === "string"
|
|
1115
|
+
? options.coordinatorLinks
|
|
1116
|
+
? createFileLinkStore(options.coordinatorLinks)
|
|
1117
|
+
: createMemoryLinkStore()
|
|
1118
|
+
: (options.coordinatorLinks ?? createMemoryLinkStore()),
|
|
1119
|
+
options.coordinatorLinkOptions,
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
/** Applies the parts of a runtime's state that can change while it runs. */
|
|
1123
|
+
function applyRuntimeState(runtime: HostedRuntime, state: JsonRecord) {
|
|
1124
|
+
if (typeof state.logging === "boolean") {
|
|
1125
|
+
runtime.setLogging(state.logging);
|
|
1126
|
+
}
|
|
1127
|
+
if (isLogLevel(state.logLevel)) {
|
|
1128
|
+
runtime.setLogLevel(state.logLevel);
|
|
1129
|
+
}
|
|
1130
|
+
if (typeof state.logData === "boolean") {
|
|
1131
|
+
runtime.setLogData(state.logData);
|
|
1132
|
+
}
|
|
1133
|
+
if (typeof state.capture === "boolean") {
|
|
1134
|
+
runtime.setCapture(state.capture);
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
logging: runtime.getLogging(),
|
|
1138
|
+
logData: runtime.getLogData(),
|
|
1139
|
+
logLevel: runtime.getLogLevel(),
|
|
1140
|
+
// Answered, never configured: nothing that saves a runtime's
|
|
1141
|
+
// configuration can keep it.
|
|
1142
|
+
capture: runtime.capturesCalls(),
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Introduces this server to a coordinator, for one runtime of one board.
|
|
1148
|
+
*
|
|
1149
|
+
* Called by the person's own client while it deploys a board: it has asked
|
|
1150
|
+
* the coordinator for a ticket and passes it on, over the same session it
|
|
1151
|
+
* creates runtimes here with. This server then connects to the coordinator —
|
|
1152
|
+
* the coordinator connects to nothing — and keeps the ticket to reconnect
|
|
1153
|
+
* with.
|
|
1154
|
+
*
|
|
1155
|
+
* The address dialled is one the caller chose, and the caller is someone this
|
|
1156
|
+
* server already runs services for; nothing here can be made to reach further
|
|
1157
|
+
* than they could with a service of their own.
|
|
1158
|
+
*
|
|
1159
|
+
* `secrets` are the values for the references that runtime's services carry.
|
|
1160
|
+
* They are handed to the runtime when the coordinator builds it, and are not
|
|
1161
|
+
* sent to the coordinator.
|
|
1162
|
+
*/
|
|
1163
|
+
expressApp.post("/coordinator-links", async (req, res) => {
|
|
1164
|
+
const body = req.body as Record<string, unknown> | undefined;
|
|
1165
|
+
if (
|
|
1166
|
+
!isJsonRecord(body) ||
|
|
1167
|
+
typeof body.coordinatorUrl !== "string" ||
|
|
1168
|
+
typeof body.ticket !== "string" ||
|
|
1169
|
+
typeof body.boardName !== "string" ||
|
|
1170
|
+
typeof body.runtimeId !== "string" ||
|
|
1171
|
+
!body.coordinatorUrl ||
|
|
1172
|
+
!body.ticket ||
|
|
1173
|
+
!body.boardName ||
|
|
1174
|
+
!body.runtimeId
|
|
1175
|
+
) {
|
|
1176
|
+
res.sendStatus(400);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
try {
|
|
1180
|
+
await coordinatorLinks.introduce(
|
|
1181
|
+
{
|
|
1182
|
+
owner: ownerKeyOf(req.authenticatedUser),
|
|
1183
|
+
boardName: body.boardName,
|
|
1184
|
+
runtimeId: body.runtimeId,
|
|
1185
|
+
coordinatorUrl: body.coordinatorUrl,
|
|
1186
|
+
ticket: body.ticket,
|
|
1187
|
+
},
|
|
1188
|
+
readSecretsPayload(body.secrets),
|
|
1189
|
+
);
|
|
1190
|
+
res.status(201).json({ connected: true });
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
res.status(502).json({
|
|
1193
|
+
error: err instanceof Error ? err.message : "Could not connect",
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
/** The caller's links: which runtimes belong to which board, never a ticket. */
|
|
1199
|
+
expressApp.get("/coordinator-links", (req, res) => {
|
|
1200
|
+
res.json({ links: coordinatorLinks.list(ownerKeyOf(req.authenticatedUser)) });
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
/** Leaves a board: drops the link and the runtime it was for. */
|
|
1204
|
+
expressApp.delete("/coordinator-links/:boardName/:runtimeId", (req, res) => {
|
|
1205
|
+
const removed = coordinatorLinks.remove({
|
|
1206
|
+
owner: ownerKeyOf(req.authenticatedUser),
|
|
1207
|
+
boardName: req.params.boardName,
|
|
1208
|
+
runtimeId: req.params.runtimeId,
|
|
1209
|
+
});
|
|
1210
|
+
res.sendStatus(removed ? 200 : 404);
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
// What a browser opens a WebSocket with instead of putting its token in the
|
|
1214
|
+
// address; see wsTickets.ts. Self-authenticated, because a coordinator's
|
|
1215
|
+
// bridge is opened by people the allowlist does not name: the ticket keeps
|
|
1216
|
+
// who the caller is and whether they may own, and the upgrade judges that.
|
|
1217
|
+
const wsTickets = new WsTickets();
|
|
1218
|
+
selfAuthenticatedRoutes.add("POST /ws-tickets");
|
|
1219
|
+
expressApp.post("/ws-tickets", (req, res) => {
|
|
1220
|
+
const header = req.headers.authorization;
|
|
1221
|
+
const token = header?.startsWith("Bearer ") ? header.slice(7) : undefined;
|
|
1222
|
+
void Promise.all([
|
|
1223
|
+
authenticator.identifyToken(token),
|
|
1224
|
+
authenticator.authorizeOwner(token),
|
|
1225
|
+
])
|
|
1226
|
+
.then(([identity, owner]) => {
|
|
1227
|
+
if (!identity) {
|
|
1228
|
+
res.sendStatus(401);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
res.json({
|
|
1232
|
+
ticket: wsTickets.mint({ identity, owner }),
|
|
1233
|
+
expiresInMs: TICKET_TTL_MS,
|
|
1234
|
+
});
|
|
1235
|
+
})
|
|
1236
|
+
.catch(() => res.sendStatus(401));
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
expressApp.get("/runtimes", (req, res) => {
|
|
1240
|
+
res.json({
|
|
1241
|
+
runtimes: tenantOf(req)
|
|
1242
|
+
.getRuntimes()
|
|
1243
|
+
.map((runtime) => serializeRuntime(runtime)),
|
|
1244
|
+
// The service registry is a property of the build, not of a tenant.
|
|
1245
|
+
registry: runtimeApp.getRegistry(),
|
|
1246
|
+
server: RUNTIME_SERVER_KIND,
|
|
1247
|
+
// This server can connect to a coordinator when introduced to one; see
|
|
1248
|
+
// POST /coordinator-links. Said here so a client can tell before it
|
|
1249
|
+
// deploys a board that needs it.
|
|
1250
|
+
coordinatorLinks: true,
|
|
1251
|
+
// A runtime built for a coordinator is kept apart from the ones a client
|
|
1252
|
+
// creates, so a client deleting its own does not delete a deployed
|
|
1253
|
+
// board's. A client checks for this before it deploys: against a server
|
|
1254
|
+
// that keeps them together, leaving the board it deployed would stop it.
|
|
1255
|
+
boardRuntimes: true,
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
expressApp.delete("/runtimes", (req, res) => {
|
|
1260
|
+
const owner = ownerKeyOf(req.authenticatedUser);
|
|
1261
|
+
runtimeApp.removeAllRuntimes(owner);
|
|
1262
|
+
mounts.releaseOwner(owner);
|
|
1263
|
+
for (const [token, info] of sessionTokens) {
|
|
1264
|
+
if (info.sub === owner) {
|
|
1265
|
+
sessionTokens.delete(token);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
res.sendStatus(200);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
expressApp.post("/runtimes", (req, res) => {
|
|
1272
|
+
if (
|
|
1273
|
+
!req.body ||
|
|
1274
|
+
(typeof req.body !== "object" && !Array.isArray(req.body))
|
|
1275
|
+
) {
|
|
1276
|
+
res.sendStatus(400);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const owner = ownerKeyOf(req.authenticatedUser);
|
|
1281
|
+
const payloads = Array.isArray(req.body) ? req.body : [req.body];
|
|
1282
|
+
const runtimes: ReturnType<typeof serializeRuntime>[] = [];
|
|
1283
|
+
|
|
1284
|
+
for (const payload of payloads) {
|
|
1285
|
+
const config = validateRuntimeConfiguration(payload);
|
|
1286
|
+
if (!config) {
|
|
1287
|
+
res.sendStatus(400);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
try {
|
|
1292
|
+
runtimes.push(serializeRuntime(provisionRuntime(owner, config)));
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
if (err instanceof QuotaError) {
|
|
1295
|
+
res.status(429).json({ error: err.message });
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
throw err;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
res.json({
|
|
1303
|
+
runtimes,
|
|
1304
|
+
registry: runtimeApp.getRegistry(),
|
|
1305
|
+
server: RUNTIME_SERVER_KIND,
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
expressApp.get("/runtimes/:runtimeId", (req, res) => {
|
|
1310
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1311
|
+
if (!runtime) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
res.json(serializeRuntime(runtime));
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
expressApp.delete("/runtimes/:runtimeId", (req, res) => {
|
|
1318
|
+
// Always return success — if the runtime was already destroyed (e.g. by a
|
|
1319
|
+
// WebSocket disconnect) the desired state is the same as an explicit delete.
|
|
1320
|
+
// Scoped to the caller, so this can only ever remove their own runtime; an
|
|
1321
|
+
// id owned by another tenant is a no-op that still reports success, matching
|
|
1322
|
+
// the already-destroyed case.
|
|
1323
|
+
removeRuntimeAndSessions(
|
|
1324
|
+
ownerKeyOf(req.authenticatedUser),
|
|
1325
|
+
req.params.runtimeId,
|
|
1326
|
+
);
|
|
1327
|
+
res.json({ id: req.params.runtimeId });
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
// Mint a coordinator session token for a runtime. Gated by the normal auth
|
|
1331
|
+
// middleware, so the caller must present a valid user JWT (the "bootstrap").
|
|
1332
|
+
// The returned opaque token is bound to that user and this runtime, and the
|
|
1333
|
+
// coordinator then uses it for its long-lived machine calls (the result WS,
|
|
1334
|
+
// teardown) without needing a user JWT that would expire.
|
|
1335
|
+
//
|
|
1336
|
+
// Limitation (v1): tokens live only in this process. If the runtime restarts
|
|
1337
|
+
// the token is gone and the coordinator must re-provision — which requires a
|
|
1338
|
+
// live user JWT. Boards therefore don't self-heal across a runtime restart
|
|
1339
|
+
// while the user is offline; persisting these bindings is future work.
|
|
1340
|
+
expressApp.post("/runtimes/:runtimeId/session-token", (req, res) => {
|
|
1341
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1342
|
+
if (!runtime) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
const sub = ownerKeyOf(req.authenticatedUser);
|
|
1346
|
+
const token = randomBytes(32).toString("hex");
|
|
1347
|
+
sessionTokens.set(token, { sub, runtimeId: req.params.runtimeId });
|
|
1348
|
+
res.json({ token });
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Values for the references this runtime's services hold.
|
|
1353
|
+
*
|
|
1354
|
+
* Provisioning carries them already; this is for the moments it cannot cover
|
|
1355
|
+
* — a board being built a service at a time, an entry edited while a board
|
|
1356
|
+
* is running, and a re-push after a restart where the services survived but
|
|
1357
|
+
* the vault did not. It merges, so a client sending one entry does not strip
|
|
1358
|
+
* the rest.
|
|
1359
|
+
*
|
|
1360
|
+
* POST rather than PUT: this merges rather than replaces, and every other
|
|
1361
|
+
* mutation this server takes is a POST — the CORS allowlist says so, and a
|
|
1362
|
+
* lone PUT is a method each runtime implementation would have to remember to
|
|
1363
|
+
* allow separately.
|
|
1364
|
+
*
|
|
1365
|
+
* There is deliberately no GET. The values go one way: in, and then only to
|
|
1366
|
+
* a service resolving a reference for a call it is making. What is held can
|
|
1367
|
+
* be *named* — the response says which aliases the runtime now has — because
|
|
1368
|
+
* a client needs to show whether a credential is configured.
|
|
1369
|
+
*/
|
|
1370
|
+
expressApp.post("/runtimes/:runtimeId/secrets", (req, res) => {
|
|
1371
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1372
|
+
if (!runtime) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const entries = readSecretsPayload(req.body);
|
|
1376
|
+
runtime.setSecrets(entries);
|
|
1377
|
+
res.json({ aliases: runtime.secrets().aliases() });
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Descriptors for the assets this runtime's services reference.
|
|
1382
|
+
*
|
|
1383
|
+
* Provisioning carries them already; this is for a configuration that names
|
|
1384
|
+
* one the runtime was not given, for an asset edited while the board runs —
|
|
1385
|
+
* which is how an edit reaches a service without reconfiguring it — and for a
|
|
1386
|
+
* re-push after a restart. It merges, and `null` removes an asset.
|
|
1387
|
+
*
|
|
1388
|
+
* Answers with the ids held, never content: what a runtime has can be named,
|
|
1389
|
+
* and a client that wants the content has the board.
|
|
1390
|
+
*/
|
|
1391
|
+
expressApp.post("/runtimes/:runtimeId/assets", (req, res) => {
|
|
1392
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1393
|
+
if (!runtime) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
runtime.setAssets(readAssetsPayload(req.body));
|
|
1397
|
+
res.json({ ids: runtime.assets().ids() });
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Whether an asset resolves here, and to what — its media type and size, or
|
|
1402
|
+
* the reason it does not. A check, not a download: the content stays where
|
|
1403
|
+
* it is, and a URL source is fetched from where it will actually be used.
|
|
1404
|
+
*/
|
|
1405
|
+
expressApp.get("/runtimes/:runtimeId/assets/:assetId", async (req, res) => {
|
|
1406
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1407
|
+
if (!runtime) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const { asset, problem } = await runtime
|
|
1411
|
+
.assets()
|
|
1412
|
+
.resolve(`hkp-asset://${req.params.assetId}`);
|
|
1413
|
+
res.json(
|
|
1414
|
+
asset
|
|
1415
|
+
? { ok: true, mediaType: asset.mediaType, size: asset.bytes.length }
|
|
1416
|
+
: { ok: false, problem },
|
|
1417
|
+
);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
expressApp.post("/runtimes/:runtimeId/rearrange", (req, res) => {
|
|
1421
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1422
|
+
if (!runtime) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (
|
|
1426
|
+
!Array.isArray(req.body) ||
|
|
1427
|
+
!req.body.every((entry) => typeof entry === "string")
|
|
1428
|
+
) {
|
|
1429
|
+
res.sendStatus(400);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
if (!runtime.rearrangeServices(req.body)) {
|
|
1433
|
+
res.sendStatus(400);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
res.json(serializeRuntime(runtime));
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
expressApp.post("/runtimes/:runtimeId", async (req, res) => {
|
|
1440
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1441
|
+
if (!runtime) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
if (req.body === undefined) {
|
|
1445
|
+
res.sendStatus(400);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// An external HTTP caller is not continuing a run, it is starting one —
|
|
1450
|
+
// as whoever its token says it is.
|
|
1451
|
+
const result = await runtime.process(
|
|
1452
|
+
req.body,
|
|
1453
|
+
() => {
|
|
1454
|
+
// Notifications are broadcast through runtime notification targets.
|
|
1455
|
+
},
|
|
1456
|
+
contextForClient(undefined, req.authenticatedUser),
|
|
1457
|
+
);
|
|
1458
|
+
res.json(result);
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Change what a running runtime records, without rebuilding it.
|
|
1463
|
+
*
|
|
1464
|
+
* `logData` is a decision a board revisits — switched on to look into
|
|
1465
|
+
* something, off again afterwards — and re-provisioning to carry it would
|
|
1466
|
+
* restart every service in the runtime to change one boolean. Separate from
|
|
1467
|
+
* POST /runtimes/:id, which processes data rather than configuring anything.
|
|
1468
|
+
*/
|
|
1469
|
+
expressApp.patch("/runtimes/:runtimeId/state", (req, res) => {
|
|
1470
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1471
|
+
if (!runtime) {
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (!isJsonRecord(req.body)) {
|
|
1475
|
+
res.sendStatus(400);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
res.json(applyRuntimeState(runtime, req.body));
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
expressApp.get("/runtimes/:runtimeId/inputs", (req, res) => {
|
|
1482
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1483
|
+
if (!runtime) {
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
res.json(serializeRuntime(runtime).inputs);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
expressApp.get("/runtimes/:runtimeId/inputs/:inputId", (req, res) => {
|
|
1490
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1491
|
+
if (!runtime) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const input = serializeRuntime(runtime).inputs.find(
|
|
1495
|
+
(entry) => entry.id === req.params.inputId,
|
|
1496
|
+
);
|
|
1497
|
+
if (!input) {
|
|
1498
|
+
res.sendStatus(404);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
res.json(input);
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
expressApp.get("/runtimes/:runtimeId/services", (req, res) => {
|
|
1505
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1506
|
+
if (!runtime) {
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
res.json(runtime.listServices());
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
expressApp.post("/runtimes/:runtimeId/services", (req, res) => {
|
|
1513
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1514
|
+
if (!runtime) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const config = validateServiceConfiguration(req.body);
|
|
1518
|
+
if (!config) {
|
|
1519
|
+
res.sendStatus(400);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
if (atQuota(runtime.listServices().length, quotas.maxServicesPerRuntime)) {
|
|
1523
|
+
res.status(429).json({
|
|
1524
|
+
error: `Service limit reached (${quotas.maxServicesPerRuntime})`,
|
|
1525
|
+
});
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
try {
|
|
1530
|
+
const state = runtime.addService(config);
|
|
1531
|
+
res.json(state);
|
|
1532
|
+
} catch {
|
|
1533
|
+
res.sendStatus(400);
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
expressApp.delete("/runtimes/:runtimeId/services/:instanceId", (req, res) => {
|
|
1538
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1539
|
+
if (!runtime) {
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (!runtime.removeService(req.params.instanceId)) {
|
|
1543
|
+
res.sendStatus(404);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
res.json(serializeRuntime(runtime));
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
expressApp.post(
|
|
1550
|
+
"/runtimes/:runtimeId/services/:instanceId",
|
|
1551
|
+
async (req, res) => {
|
|
1552
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1553
|
+
if (!runtime) {
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (!isJsonRecord(req.body)) {
|
|
1557
|
+
res.sendStatus(400);
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
let state = runtime.configureService(
|
|
1562
|
+
req.params.instanceId,
|
|
1563
|
+
req.body,
|
|
1564
|
+
contextForClient(undefined, req.authenticatedUser),
|
|
1565
|
+
);
|
|
1566
|
+
if (!state) {
|
|
1567
|
+
res.sendStatus(404);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Some services (for example http-server-subservices with port 0)
|
|
1572
|
+
// transition asynchronously and update state shortly after configure().
|
|
1573
|
+
state = await waitForServiceActivationState(
|
|
1574
|
+
runtime,
|
|
1575
|
+
req.params.instanceId,
|
|
1576
|
+
);
|
|
1577
|
+
|
|
1578
|
+
res.json(state);
|
|
1579
|
+
},
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* Run the pipeline starting at one service, with a given payload.
|
|
1584
|
+
*
|
|
1585
|
+
* Distinct from configuring it: configure says what a service *is*, this says
|
|
1586
|
+
* do your job with this. A facade button had only the former, so anything it
|
|
1587
|
+
* needed to cause had to be smuggled in as a config field that a service read
|
|
1588
|
+
* as a command — which is how `store` ended up releasing records from inside
|
|
1589
|
+
* `configure`.
|
|
1590
|
+
*
|
|
1591
|
+
* The service named here runs; it is not skipped the way `processFrom` skips
|
|
1592
|
+
* the caller that is handing work onward.
|
|
1593
|
+
*/
|
|
1594
|
+
expressApp.post(
|
|
1595
|
+
"/runtimes/:runtimeId/services/:instanceId/process",
|
|
1596
|
+
async (req, res) => {
|
|
1597
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1598
|
+
if (!runtime) {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (req.body === undefined) {
|
|
1602
|
+
res.sendStatus(400);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (!runtime.getService(req.params.instanceId)) {
|
|
1606
|
+
res.sendStatus(404);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// The run is the one the body names, or a new one. The caller is never
|
|
1611
|
+
// the body's to name: it is whoever the token was verified as.
|
|
1612
|
+
const result = await runtime.processAt(
|
|
1613
|
+
req.params.instanceId,
|
|
1614
|
+
req.body,
|
|
1615
|
+
() => {
|
|
1616
|
+
// Notifications are broadcast through runtime notification targets.
|
|
1617
|
+
},
|
|
1618
|
+
contextForClient(
|
|
1619
|
+
(req.body as JsonRecord | undefined)?.__context,
|
|
1620
|
+
req.authenticatedUser,
|
|
1621
|
+
),
|
|
1622
|
+
);
|
|
1623
|
+
res.json(result ?? null);
|
|
1624
|
+
},
|
|
1625
|
+
);
|
|
1626
|
+
|
|
1627
|
+
expressApp.get("/runtimes/:runtimeId/services/:instanceId", (req, res) => {
|
|
1628
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1629
|
+
if (!runtime) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
const service = runtime.getService(req.params.instanceId);
|
|
1633
|
+
if (!service) {
|
|
1634
|
+
res.sendStatus(404);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
res.json(service.getState());
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
expressApp.get(
|
|
1641
|
+
"/runtimes/:runtimeId/services/:instanceId/property/:propertyId",
|
|
1642
|
+
(req, res) => {
|
|
1643
|
+
const runtime = getRuntimeOr404(req, res, req.params.runtimeId);
|
|
1644
|
+
if (!runtime) {
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const service = runtime.getService(req.params.instanceId);
|
|
1648
|
+
if (!service) {
|
|
1649
|
+
res.sendStatus(404);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const state = service.getState();
|
|
1654
|
+
const property = state[req.params.propertyId];
|
|
1655
|
+
if (property === undefined) {
|
|
1656
|
+
res.sendStatus(404);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
res.json(property);
|
|
1660
|
+
},
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
expressApp.use(
|
|
1664
|
+
(err: Error, req: Request, res: Response, _next: NextFunction) => {
|
|
1665
|
+
if (err instanceof SyntaxError) {
|
|
1666
|
+
res.sendStatus(400);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
// Said in words: the caller is a board editor, and "too large" is
|
|
1670
|
+
// something its author can act on.
|
|
1671
|
+
if ((err as { type?: string }).type === "entity.too.large") {
|
|
1672
|
+
const limit = (err as { limit?: number }).limit;
|
|
1673
|
+
res.status(413).json({
|
|
1674
|
+
error: carriesAssets(req)
|
|
1675
|
+
? `Request body is larger than ${limit} bytes, which is what the assets sent at once may weigh here. Name larger content by url.`
|
|
1676
|
+
: `Request body is larger than ${limit} bytes`,
|
|
1677
|
+
});
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
// Don't leak internal error details (paths, stack hints) to clients.
|
|
1681
|
+
console.error("[server] Unhandled request error:", err);
|
|
1682
|
+
res.status(500).json({ error: "Internal Server Error" });
|
|
1683
|
+
},
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
// Above the coordinator's frame limit, as for attachCoordinatorJoin; the
|
|
1687
|
+
// coordinator says which when it takes the bridge (setBridgeUpgradeHandler).
|
|
1688
|
+
let bridgeWsServer = new WebSocketServer({ noServer: true, maxPayload: socketCeiling() });
|
|
1689
|
+
let bridgeUpgradeHandler:
|
|
1690
|
+
| ((
|
|
1691
|
+
ws: WebSocket,
|
|
1692
|
+
user: AuthenticatedUser,
|
|
1693
|
+
mayOwn: boolean,
|
|
1694
|
+
transport?: Duplex,
|
|
1695
|
+
) => void)
|
|
1696
|
+
| undefined;
|
|
1697
|
+
|
|
1698
|
+
function rejectUpgrade(
|
|
1699
|
+
socket: Duplex,
|
|
1700
|
+
status: number,
|
|
1701
|
+
reason: string,
|
|
1702
|
+
): void {
|
|
1703
|
+
socket.write(`HTTP/1.1 ${status} ${reason}\r\n\r\n`);
|
|
1704
|
+
socket.destroy();
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// Upgrade paths that authenticate their own callers, by exact path. A
|
|
1708
|
+
// coordinator's join endpoint is one: what connects there holds a ticket,
|
|
1709
|
+
// not a user's token.
|
|
1710
|
+
const upgradeRoutes = new Map<
|
|
1711
|
+
string,
|
|
1712
|
+
(request: http.IncomingMessage, socket: Duplex, head: Buffer) => void
|
|
1713
|
+
>();
|
|
1714
|
+
|
|
1715
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
1716
|
+
// Mounts are matched first and are not token-authenticated, for the same
|
|
1717
|
+
// reason their HTTP requests are not: the callers are outside parties.
|
|
1718
|
+
if (mounts.handleUpgrade(request, socket, head)) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// Protocol is irrelevant — base is only needed to resolve the relative path.
|
|
1723
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host}`);
|
|
1724
|
+
|
|
1725
|
+
const upgradeRoute = upgradeRoutes.get(url.pathname);
|
|
1726
|
+
if (upgradeRoute) {
|
|
1727
|
+
upgradeRoute(request, socket, head);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Authenticate every upgrade with the same rules as HTTP routes; who is
|
|
1732
|
+
// asking is read from the ticket or token below.
|
|
1733
|
+
// The Origin check blocks cross-site WebSocket hijacking from a page a local
|
|
1734
|
+
// user happens to visit — and without auth, where nothing else stands in
|
|
1735
|
+
// the way, it is the whole rule a request is let in by.
|
|
1736
|
+
if (
|
|
1737
|
+
noAuth
|
|
1738
|
+
? !admitsUnauthenticated(request)
|
|
1739
|
+
: !isOriginAllowed(request.headers.origin, allowedOrigins)
|
|
1740
|
+
) {
|
|
1741
|
+
rejectUpgrade(socket, 403, "Forbidden");
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// Who is asking. A browser cannot set a header on a handshake, so it
|
|
1746
|
+
// presents a ticket it was given for its token (wsTickets.ts); a client
|
|
1747
|
+
// that can — the coordinator — sends the token itself. `?access_token=` is
|
|
1748
|
+
// what browsers sent before tickets, and is still taken from one that
|
|
1749
|
+
// does, though it puts the token where logs keep it.
|
|
1750
|
+
const authHeader = request.headers.authorization;
|
|
1751
|
+
const bearer = authHeader?.startsWith("Bearer ")
|
|
1752
|
+
? authHeader.slice(7)
|
|
1753
|
+
: undefined;
|
|
1754
|
+
const token = bearer ?? url.searchParams.get("access_token");
|
|
1755
|
+
const ticket = url.searchParams.get("ticket");
|
|
1756
|
+
const caller: Promise<TicketHolder> = ticket
|
|
1757
|
+
? Promise.resolve(wsTickets.redeem(ticket) ?? { identity: null, owner: null })
|
|
1758
|
+
: Promise.all([
|
|
1759
|
+
authenticator.identifyToken(token),
|
|
1760
|
+
authenticator.authorizeOwner(token),
|
|
1761
|
+
]).then(([identity, owner]) => ({ identity, owner }));
|
|
1762
|
+
|
|
1763
|
+
// A coordinator's bridge authenticates its own callers, like the join
|
|
1764
|
+
// endpoint above: a deployed board is attached to by its owner and by the
|
|
1765
|
+
// people it is shared with, and the second are not on this server's
|
|
1766
|
+
// allowlist. So the upgrade establishes who is asking and whether they may
|
|
1767
|
+
// own; which board they may attach to, and as what, is the handler's.
|
|
1768
|
+
if (url.pathname === "/coordinator/bridge") {
|
|
1769
|
+
void caller
|
|
1770
|
+
.then(({ identity, owner }) => {
|
|
1771
|
+
if (!identity) {
|
|
1772
|
+
rejectUpgrade(socket, 401, "Unauthorized");
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
// Somebody who may not own and has no verified email can be neither
|
|
1776
|
+
// a board's owner nor on its list, whichever board they go on to
|
|
1777
|
+
// name — so there is nothing to open a socket for.
|
|
1778
|
+
if (!owner && !identity.email) {
|
|
1779
|
+
rejectUpgrade(socket, 401, "Unauthorized");
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (!bridgeUpgradeHandler) {
|
|
1783
|
+
rejectUpgrade(socket, 503, "Service Unavailable");
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
bridgeWsServer.handleUpgrade(request, socket, head, (ws) => {
|
|
1787
|
+
// A frame past the socket ceiling is reported here before the
|
|
1788
|
+
// library closes the socket; the close is what is acted on.
|
|
1789
|
+
// Unlistened, it would end this process.
|
|
1790
|
+
ws.on("error", () => {});
|
|
1791
|
+
bridgeUpgradeHandler!(ws, identity, !!owner, socket);
|
|
1792
|
+
});
|
|
1793
|
+
})
|
|
1794
|
+
.catch(() => {
|
|
1795
|
+
rejectUpgrade(socket, 401, "Unauthorized");
|
|
1796
|
+
});
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
void caller
|
|
1801
|
+
.then(({ owner: user }) => {
|
|
1802
|
+
if (!user) {
|
|
1803
|
+
rejectUpgrade(socket, 401, "Unauthorized");
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// The runtime is resolved in the authenticated user's namespace, so a
|
|
1808
|
+
// token cannot open a socket onto another tenant's runtime; an id owned
|
|
1809
|
+
// by someone else is indistinguishable from one that does not exist.
|
|
1810
|
+
const owner = ownerKeyOf(user);
|
|
1811
|
+
const runtimeId = url.pathname.slice(1);
|
|
1812
|
+
if (!runtimeId || !runtimeApp.getRuntime(owner, runtimeId)) {
|
|
1813
|
+
rejectUpgrade(socket, 404, "Not Found");
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
webSocketServer.handleUpgrade(request, socket, head, (websocket) => {
|
|
1818
|
+
// As for a bridge: an oversized frame is reported, then closed.
|
|
1819
|
+
websocket.on("error", () => {});
|
|
1820
|
+
webSocketServer.emit("connection", websocket, request, {
|
|
1821
|
+
owner,
|
|
1822
|
+
runtimeId,
|
|
1823
|
+
user,
|
|
1824
|
+
});
|
|
1825
|
+
});
|
|
1826
|
+
})
|
|
1827
|
+
.catch(() => {
|
|
1828
|
+
rejectUpgrade(socket, 401, "Unauthorized");
|
|
1829
|
+
});
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
webSocketServer.on(
|
|
1833
|
+
"connection",
|
|
1834
|
+
(
|
|
1835
|
+
socket: WebSocket,
|
|
1836
|
+
_request: http.IncomingMessage,
|
|
1837
|
+
{
|
|
1838
|
+
owner,
|
|
1839
|
+
runtimeId,
|
|
1840
|
+
user,
|
|
1841
|
+
}: { owner: string; runtimeId: string; user: AuthenticatedUser },
|
|
1842
|
+
) => {
|
|
1843
|
+
const socketKey = tenantKey(owner, runtimeId);
|
|
1844
|
+
const sockets = runtimeSockets.get(socketKey) ?? new Set<WebSocket>();
|
|
1845
|
+
sockets.add(socket);
|
|
1846
|
+
runtimeSockets.set(socketKey, sockets);
|
|
1847
|
+
|
|
1848
|
+
socket.on("close", () => {
|
|
1849
|
+
const current = runtimeSockets.get(socketKey);
|
|
1850
|
+
current?.delete(socket);
|
|
1851
|
+
if (current && current.size === 0) {
|
|
1852
|
+
runtimeSockets.delete(socketKey);
|
|
1853
|
+
if (!runtimeApp.forOwner(owner).getRuntime(runtimeId)?.garbageCollected) {
|
|
1854
|
+
// Nobody asked for this one to be cleaned up, so it outlives the
|
|
1855
|
+
// clients that happened to be watching it. A coordinator's board
|
|
1856
|
+
// keeps running with no browser attached; a runtime from a config
|
|
1857
|
+
// file or a script was never anyone's to reap.
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
// Its creator said it should not outlive them — a browser running the
|
|
1861
|
+
// board is the controller, and this was the last one connected. Free
|
|
1862
|
+
// the resources now; provisioning again recreates it cleanly.
|
|
1863
|
+
removeRuntimeAndSessions(owner, runtimeId);
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
// Passes enter the pipeline in the order their frames arrive: nothing
|
|
1868
|
+
// is awaited before process() starts, and the pipeline runs until its
|
|
1869
|
+
// first asynchronous service within that call.
|
|
1870
|
+
socket.on("message", async (raw, isBinary) => {
|
|
1871
|
+
const message = readInbound(raw, isBinary);
|
|
1872
|
+
if (!message) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
if (message.type === "readwrite") {
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
if (message.type === "processRuntime" && message.params !== undefined) {
|
|
1881
|
+
const runtime = runtimeApp.getRuntime(owner, runtimeId);
|
|
1882
|
+
if (!runtime) {
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
const result = await runtime.process(
|
|
1886
|
+
message.params,
|
|
1887
|
+
() => {
|
|
1888
|
+
// Notifications are broadcast through runtime notification targets.
|
|
1889
|
+
},
|
|
1890
|
+
// A peer driving this runtime names the run its call belongs to, so
|
|
1891
|
+
// that a board spanning several runtimes reads as one trace rather
|
|
1892
|
+
// than one per runtime. Who is calling is not the frame's to say:
|
|
1893
|
+
// it is whoever opened this socket.
|
|
1894
|
+
contextForClient(message.context, user),
|
|
1895
|
+
);
|
|
1896
|
+
sendJsonResult(socket, result);
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
},
|
|
1900
|
+
);
|
|
1901
|
+
|
|
1902
|
+
return {
|
|
1903
|
+
expressApp,
|
|
1904
|
+
httpServer,
|
|
1905
|
+
runtimeApp,
|
|
1906
|
+
coordinatorLinks,
|
|
1907
|
+
/** How this server verifies a token, for whatever is mounted on it. */
|
|
1908
|
+
authenticator,
|
|
1909
|
+
/**
|
|
1910
|
+
* Lets one HTTP route past the owner check every request otherwise gets,
|
|
1911
|
+
* because it authenticates its own callers. Exact method and path.
|
|
1912
|
+
*/
|
|
1913
|
+
addSelfAuthenticatedRoute(method: string, pathname: string) {
|
|
1914
|
+
selfAuthenticatedRoutes.add(`${method.toUpperCase()} ${pathname}`);
|
|
1915
|
+
},
|
|
1916
|
+
/** Serves WebSocket upgrades on a path with a handler that does its own
|
|
1917
|
+
* authentication, ahead of the token check every other upgrade gets. */
|
|
1918
|
+
addUpgradeRoute(
|
|
1919
|
+
pathname: string,
|
|
1920
|
+
handler: (
|
|
1921
|
+
request: http.IncomingMessage,
|
|
1922
|
+
socket: Duplex,
|
|
1923
|
+
head: Buffer,
|
|
1924
|
+
) => void,
|
|
1925
|
+
) {
|
|
1926
|
+
upgradeRoutes.set(pathname, handler);
|
|
1927
|
+
},
|
|
1928
|
+
async start(port = 0, host = options.host ?? "127.0.0.1") {
|
|
1929
|
+
await new Promise<void>((resolve, reject) => {
|
|
1930
|
+
httpServer.once("error", reject);
|
|
1931
|
+
httpServer.listen(port, host, () => {
|
|
1932
|
+
httpServer.off("error", reject);
|
|
1933
|
+
resolve();
|
|
1934
|
+
});
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
const address = httpServer.address() as AddressInfo;
|
|
1938
|
+
return {
|
|
1939
|
+
host,
|
|
1940
|
+
port: address.port,
|
|
1941
|
+
baseUrl: `http://${host}:${address.port}`,
|
|
1942
|
+
};
|
|
1943
|
+
},
|
|
1944
|
+
/**
|
|
1945
|
+
* Who handles a browser attaching to a board. It is told who is asking and
|
|
1946
|
+
* whether the server's allowlist lets them own; it decides the rest.
|
|
1947
|
+
*/
|
|
1948
|
+
setBridgeUpgradeHandler(
|
|
1949
|
+
handler: (
|
|
1950
|
+
ws: WebSocket,
|
|
1951
|
+
user: AuthenticatedUser,
|
|
1952
|
+
mayOwn: boolean,
|
|
1953
|
+
transport?: Duplex,
|
|
1954
|
+
) => void,
|
|
1955
|
+
/** The frame limit of the coordinator behind the handler. */
|
|
1956
|
+
maxFrameBytes?: number,
|
|
1957
|
+
) {
|
|
1958
|
+
bridgeUpgradeHandler = handler;
|
|
1959
|
+
if (maxFrameBytes !== undefined) {
|
|
1960
|
+
bridgeWsServer = new WebSocketServer({
|
|
1961
|
+
noServer: true,
|
|
1962
|
+
maxPayload: socketCeiling(maxFrameBytes),
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
},
|
|
1966
|
+
async stop() {
|
|
1967
|
+
coordinatorLinks.stop();
|
|
1968
|
+
for (const sockets of runtimeSockets.values()) {
|
|
1969
|
+
for (const socket of sockets) {
|
|
1970
|
+
socket.close();
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
runtimeSockets.clear();
|
|
1974
|
+
|
|
1975
|
+
await new Promise<void>((resolve, reject) => {
|
|
1976
|
+
httpServer.close((error) => {
|
|
1977
|
+
if (error) {
|
|
1978
|
+
reject(error);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
resolve();
|
|
1982
|
+
});
|
|
1983
|
+
});
|
|
1984
|
+
},
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function isJsonRecord(value: unknown): value is JsonRecord {
|
|
1989
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/** Whether a value is one of the levels a runtime understands. */
|
|
1993
|
+
function isLogLevel(value: unknown): value is LogLevel {
|
|
1994
|
+
return (
|
|
1995
|
+
value === "debug" || value === "info" || value === "warn" || value === "error"
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function validateRuntimeConfiguration(
|
|
2000
|
+
value: unknown,
|
|
2001
|
+
): RuntimeConfiguration | null {
|
|
2002
|
+
if (!isJsonRecord(value)) {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
if (typeof value.id !== "string" || typeof value.name !== "string") {
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
if (!Array.isArray(value.services)) {
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const services: ServiceConfiguration[] = [];
|
|
2013
|
+
for (const entry of value.services) {
|
|
2014
|
+
const config = validateServiceConfiguration(entry);
|
|
2015
|
+
if (!config) {
|
|
2016
|
+
return null;
|
|
2017
|
+
}
|
|
2018
|
+
services.push(config);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
return {
|
|
2022
|
+
id: value.id,
|
|
2023
|
+
name: value.name,
|
|
2024
|
+
boardName:
|
|
2025
|
+
typeof value.boardName === "string" ? value.boardName : undefined,
|
|
2026
|
+
// Absent means persist; see RuntimeConfiguration.garbageCollected.
|
|
2027
|
+
garbageCollected: value.garbageCollected === true,
|
|
2028
|
+
// Both absent mean off; see RuntimeConfiguration.logging / logData.
|
|
2029
|
+
logging: isJsonRecord(value.state) && value.state.logging === true,
|
|
2030
|
+
logLevel:
|
|
2031
|
+
isJsonRecord(value.state) && isLogLevel(value.state.logLevel)
|
|
2032
|
+
? value.state.logLevel
|
|
2033
|
+
: undefined,
|
|
2034
|
+
// Absent means allowed; see RuntimeConfiguration.logData.
|
|
2035
|
+
logData: !(isJsonRecord(value.state) && value.state.logData === false),
|
|
2036
|
+
// Values for the references the services carry. Read out of the payload
|
|
2037
|
+
// here and handed to the runtime's vault; they are never put back into any
|
|
2038
|
+
// service's state, and never appear in a serialized runtime.
|
|
2039
|
+
secrets: readSecretsPayload(value.secrets),
|
|
2040
|
+
// Descriptors for the assets the services reference; a removal means
|
|
2041
|
+
// nothing to a runtime being created, so only descriptors are kept.
|
|
2042
|
+
assets: presentAssets(readAssetsPayload(value.assets)),
|
|
2043
|
+
services,
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function presentAssets(
|
|
2048
|
+
entries: Record<string, AssetDescriptor | null>,
|
|
2049
|
+
): Record<string, AssetDescriptor> {
|
|
2050
|
+
const present: Record<string, AssetDescriptor> = {};
|
|
2051
|
+
for (const [id, entry] of Object.entries(entries)) {
|
|
2052
|
+
if (entry) {
|
|
2053
|
+
present[id] = entry;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return present;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
function validateServiceConfiguration(
|
|
2060
|
+
value: unknown,
|
|
2061
|
+
): ServiceConfiguration | null {
|
|
2062
|
+
if (!isJsonRecord(value)) {
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
if (typeof value.serviceId !== "string" || typeof value.uuid !== "string") {
|
|
2066
|
+
return null;
|
|
2067
|
+
}
|
|
2068
|
+
if (value.state !== undefined && !isJsonRecord(value.state)) {
|
|
2069
|
+
return null;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
return {
|
|
2073
|
+
serviceId: value.serviceId,
|
|
2074
|
+
uuid: value.uuid,
|
|
2075
|
+
name: typeof value.name === "string" ? value.name : undefined,
|
|
2076
|
+
serviceName:
|
|
2077
|
+
typeof value.serviceName === "string" ? value.serviceName : undefined,
|
|
2078
|
+
state: value.state,
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
async function waitForServiceActivationState(
|
|
2083
|
+
runtime: HostedRuntime,
|
|
2084
|
+
instanceId: string,
|
|
2085
|
+
): Promise<JsonRecord> {
|
|
2086
|
+
const maxAttempts = 20;
|
|
2087
|
+
const delayMs = 10;
|
|
2088
|
+
|
|
2089
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
2090
|
+
const service = runtime.getService(instanceId);
|
|
2091
|
+
if (!service) {
|
|
2092
|
+
return {};
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
const state = service.getState();
|
|
2096
|
+
const bypass = state.bypass;
|
|
2097
|
+
const port = state.port;
|
|
2098
|
+
|
|
2099
|
+
if (
|
|
2100
|
+
typeof bypass === "boolean" &&
|
|
2101
|
+
bypass === false &&
|
|
2102
|
+
typeof port === "number" &&
|
|
2103
|
+
port === 0
|
|
2104
|
+
) {
|
|
2105
|
+
await sleep(delayMs);
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
return state;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
const service = runtime.getService(instanceId);
|
|
2113
|
+
return service?.getState() ?? {};
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function sleep(ms: number): Promise<void> {
|
|
2117
|
+
return new Promise((resolve) => {
|
|
2118
|
+
setTimeout(resolve, ms);
|
|
2119
|
+
});
|
|
2120
|
+
}
|