@pellux/goodvibes-tui 1.27.0 → 2.0.1
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/CHANGELOG.md +125 -2
- package/README.md +7 -9
- package/bin/goodvibes +2 -2
- package/bin/launcher-support.js +8 -4
- package/docs/README.md +11 -2
- package/docs/foundation-artifacts/operator-contract.json +6403 -2347
- package/package.json +14 -25
- package/scripts/postinstall.js +35 -35
- package/src/audio/spoken-turn-model-routing.ts +1 -1
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completions/generate.ts +1 -1
- package/src/cli/doctor.ts +3 -3
- package/src/cli/entrypoint.ts +12 -7
- package/src/cli/help.ts +4 -5
- package/src/cli/hooks-command.ts +1 -1
- package/src/cli/index.ts +1 -4
- package/src/cli/launch-auto-update.ts +59 -7
- package/src/cli/management-commands.ts +6 -6
- package/src/cli/management-utils.ts +5 -5
- package/src/cli/management.ts +7 -7
- package/src/cli/network-posture.ts +1 -1
- package/src/cli/package-verification.ts +7 -6
- package/src/cli/plugin-bundles-command.ts +1 -1
- package/src/cli/plugin-command.ts +2 -2
- package/src/cli/service-command.ts +2 -2
- package/src/cli/service-posture.ts +4 -4
- package/src/cli/status.ts +9 -8
- package/src/cli/surface-command.ts +4 -4
- package/src/cli/tui-startup.ts +12 -3
- package/src/cli-flags.ts +3 -3
- package/src/config/goodvibes-home-audit.ts +0 -2
- package/src/config/secret-config.ts +37 -5
- package/src/config/tui-extension-settings.ts +1 -1
- package/src/core/approval-alert.ts +2 -2
- package/src/core/budget-breach-notifier.ts +3 -3
- package/src/core/conversation-compaction-render.ts +1 -1
- package/src/core/conversation-line-cache.ts +1 -1
- package/src/core/conversation-render-context.ts +1 -1
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +10 -10
- package/src/core/long-task-notifier.ts +2 -2
- package/src/core/memory-diagnostics-gateway.ts +1 -1
- package/src/core/scriptable-statusline.ts +1 -1
- package/src/core/session-auto-titler.ts +1 -1
- package/src/core/session-resume-core.ts +2 -2
- package/src/core/stream-event-wiring.ts +24 -44
- package/src/core/terminal-notifier.ts +3 -3
- package/src/core/turn-cancellation.ts +1 -1
- package/src/core/turn-event-wiring.ts +5 -5
- package/src/input/command-registry.ts +6 -6
- package/src/input/commands/channel-pairing.ts +1 -1
- package/src/input/commands/cluster-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +1 -1
- package/src/input/commands/hosted-runtime.ts +330 -0
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup-transfer.ts +2 -2
- package/src/input/commands/local-setup.ts +3 -3
- package/src/input/commands/managed-runtime.ts +2 -2
- package/src/input/commands/operator-rpc.ts +24 -110
- package/src/input/commands/payment-card-intake.ts +1 -1
- package/src/input/commands/permissions-provenance.ts +2 -2
- package/src/input/commands/plugin-runtime.ts +1 -1
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/relay-runtime.ts +18 -9
- package/src/input/commands/rewind-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +1 -1
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +145 -55
- package/src/input/commands/update-runtime.ts +31 -28
- package/src/input/commands/work-plan-runtime.ts +2 -2
- package/src/input/commands/workstream-attempts.ts +1 -1
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands.ts +2 -0
- package/src/input/feature-unit-layout.ts +1 -1
- package/src/input/feed-context-factory.ts +3 -3
- package/src/input/handler-content-actions.ts +2 -2
- package/src/input/handler-feed-routes.ts +1 -1
- package/src/input/handler-feed.ts +3 -3
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +3 -2
- package/src/input/handler-onboarding.ts +1 -1
- package/src/input/handler-types.ts +2 -2
- package/src/input/handler-ui-state.ts +1 -1
- package/src/input/handler.ts +4 -4
- package/src/input/input-history.ts +1 -1
- package/src/input/onboarding/onboarding-feature-units.ts +2 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +15 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +17 -8
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +5 -3
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-types.ts +1 -1
- package/src/input/payments-config.ts +6 -6
- package/src/input/search.ts +1 -1
- package/src/input/session-picker-modal.ts +46 -0
- package/src/input/settings-modal-activation.ts +1 -1
- package/src/input/settings-modal-behavior.ts +4 -4
- package/src/input/settings-modal-commit.ts +11 -0
- package/src/input/settings-modal-data.ts +13 -80
- package/src/input/settings-modal-mutations.ts +33 -0
- package/src/input/settings-modal-secrets.ts +34 -0
- package/src/input/settings-modal-subscriptions.ts +1 -1
- package/src/input/settings-modal-types.ts +15 -1
- package/src/input/settings-modal.ts +28 -3
- package/src/input/worktree-setup-config.ts +1 -1
- package/src/main.ts +11 -9
- package/src/panels/agent-inspector-shared.ts +2 -2
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/operations.ts +6 -8
- package/src/panels/builtin/session.ts +17 -0
- package/src/panels/builtin/shared.ts +5 -5
- package/src/panels/builtin-modals.ts +3 -3
- package/src/panels/confirm-state.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +4 -4
- package/src/panels/diff-panel.ts +2 -2
- package/src/panels/diff-review-panel.ts +2 -2
- package/src/panels/expandable-list-panel.ts +2 -2
- package/src/panels/fleet-acts.ts +1 -1
- package/src/panels/fleet-observed-render.ts +1 -1
- package/src/panels/fleet-panel-format.ts +1 -1
- package/src/panels/fleet-panel.ts +1 -1
- package/src/panels/fleet-spawn.ts +1 -1
- package/src/panels/fleet-steer.ts +1 -1
- package/src/panels/fleet-transcript.ts +1 -1
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/hosted-session-feed.ts +352 -0
- package/src/panels/hosted-session-panel.ts +221 -0
- package/src/panels/local-auth-panel.ts +3 -3
- package/src/panels/modals/devices-modal.ts +1 -1
- package/src/panels/modals/local-auth-modal.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +1 -1
- package/src/panels/modals/services-modal.ts +1 -1
- package/src/panels/modals/settings-sync-modal.ts +1 -1
- package/src/panels/modals/subscription-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +1 -1
- package/src/panels/notifications-panel.ts +1 -1
- package/src/panels/panel-confirm-overlay.ts +1 -1
- package/src/panels/plugins-panel.ts +2 -2
- package/src/panels/polish-core.ts +2 -2
- package/src/panels/polish-tables.ts +1 -1
- package/src/panels/polish.ts +2 -2
- package/src/panels/scrollable-list-panel.ts +3 -3
- package/src/panels/skills-panel.ts +3 -3
- package/src/panels/token-budget-panel.ts +5 -5
- package/src/panels/types.ts +1 -2
- package/src/permissions/prompt.ts +1 -1
- package/src/renderer/autocomplete-overlay.ts +1 -1
- package/src/renderer/block-actions-overlay.ts +1 -1
- package/src/renderer/bookmark-modal.ts +1 -1
- package/src/renderer/buffer.ts +1 -1
- package/src/renderer/code-block.ts +1 -1
- package/src/renderer/composer-fragments.ts +1 -1
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/context-inspector.ts +1 -1
- package/src/renderer/conversation-layout.ts +2 -2
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/conversation-surface.ts +2 -2
- package/src/renderer/diff-view.ts +1 -1
- package/src/renderer/diff.ts +1 -1
- package/src/renderer/file-picker-overlay.ts +1 -1
- package/src/renderer/fleet-tab-strip.ts +1 -1
- package/src/renderer/fullscreen-primitives.ts +2 -2
- package/src/renderer/fullscreen-workspace.ts +1 -1
- package/src/renderer/header-line.ts +1 -1
- package/src/renderer/help-overlay.ts +1 -1
- package/src/renderer/history-search-overlay.ts +2 -2
- package/src/renderer/layout.ts +5 -8
- package/src/renderer/markdown-inline.ts +1 -1
- package/src/renderer/markdown-table.ts +1 -1
- package/src/renderer/markdown.ts +1 -1
- package/src/renderer/mcp-workspace.ts +1 -1
- package/src/renderer/modal-factory.ts +2 -2
- package/src/renderer/model-workspace.ts +2 -2
- package/src/renderer/onboarding/onboarding-wizard.ts +1 -1
- package/src/renderer/overlay-box.ts +1 -1
- package/src/renderer/panel-composite.ts +1 -1
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/profile-picker-modal.ts +1 -1
- package/src/renderer/search-overlay.ts +2 -2
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +124 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +2 -2
- package/src/renderer/shell-surface.ts +2 -2
- package/src/renderer/system-message.ts +1 -1
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/theme-mode-config.ts +1 -1
- package/src/renderer/theme.ts +11 -12
- package/src/renderer/thinking.ts +1 -1
- package/src/renderer/tool-call.ts +2 -2
- package/src/renderer/tool-result-expanded-lines.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -8
- package/src/renderer/ui-factory.ts +3 -3
- package/src/runtime/bootstrap-command-context.ts +4 -4
- package/src/runtime/bootstrap-command-parts.ts +5 -5
- package/src/runtime/bootstrap-core.ts +30 -30
- package/src/runtime/bootstrap-shell.ts +4 -4
- package/src/runtime/bootstrap.ts +128 -157
- package/src/runtime/client/approval-updates.ts +82 -0
- package/src/runtime/client/build-floors.ts +167 -0
- package/src/runtime/client/daemon-attach-handshake.ts +115 -0
- package/src/runtime/client/fleet-union.ts +147 -0
- package/src/runtime/client/host-service-status.ts +71 -0
- package/src/runtime/client/hosted-exit.ts +86 -0
- package/src/runtime/client/hosted-roster.ts +110 -0
- package/src/runtime/client/hosted-session-stream.ts +178 -0
- package/src/runtime/client/hosted-sessions.ts +187 -0
- package/src/runtime/client/operator-endpoint.ts +213 -0
- package/src/runtime/client/spine-adoption.ts +99 -0
- package/src/runtime/cloudflare-control-plane.ts +1 -1
- package/src/runtime/context-accounting-source.ts +1 -1
- package/src/runtime/context.ts +2 -2
- package/src/runtime/conversation-rewind-port.ts +42 -14
- package/src/runtime/daemon-attach-notices.ts +55 -19
- package/src/runtime/diagnostics/panels/ops.ts +1 -1
- package/src/runtime/disposal-wiring.ts +4 -24
- package/src/runtime/index.ts +119 -105
- package/src/runtime/install-self-check-startup.ts +2 -2
- package/src/runtime/interaction-seams.ts +1 -1
- package/src/runtime/knowledge-services.ts +8 -2
- package/src/runtime/legacy-daemon-migration.ts +15 -3
- package/src/runtime/legacy-daemon-reconcile.ts +3 -2
- package/src/runtime/memory-spine-transport.ts +37 -267
- package/src/runtime/notification-dispatch.ts +2 -2
- package/src/runtime/onboarding/apply.ts +2 -2
- package/src/runtime/onboarding/derivation.ts +2 -2
- package/src/runtime/onboarding/markers.ts +2 -2
- package/src/runtime/onboarding/progress.ts +2 -2
- package/src/runtime/onboarding/snapshot.ts +1 -1
- package/src/runtime/onboarding/state.ts +2 -2
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/orchestrator-core-services.ts +1 -1
- package/src/runtime/path-shadow-startup.ts +28 -221
- package/src/runtime/power-keepawake-remote.ts +3 -2
- package/src/runtime/process-lifecycle.ts +2 -2
- package/src/runtime/provider-fallback.ts +10 -66
- package/src/runtime/recovery-autosave.ts +1 -1
- package/src/runtime/recovery-offer-wiring.ts +39 -23
- package/src/runtime/recovery-prompt.ts +31 -17
- package/src/runtime/relay-reachability-bridge.ts +53 -42
- package/src/runtime/release-artifacts.ts +2 -1
- package/src/runtime/runtime-services-types.ts +82 -35
- package/src/runtime/services.ts +305 -300
- package/src/runtime/session-resume-liveness-confirm.ts +1 -1
- package/src/runtime/ui-services.ts +7 -6
- package/src/runtime/update-check.ts +23 -43
- package/src/runtime/workspace-checkpointing.ts +1 -1
- package/src/runtime/wrfc-persistence.ts +2 -2
- package/src/shell/ui-openers.ts +30 -7
- package/src/utils/clipboard.ts +1 -1
- package/src/verification/verification-ledger.ts +10 -8
- package/src/version.ts +1 -1
- package/bin/goodvibes-daemon +0 -90
- package/src/cli/config-overrides.ts +0 -267
- package/src/cli/endpoints.ts +0 -109
- package/src/cli/parser.ts +0 -395
- package/src/cli/redaction.ts +0 -170
- package/src/cli/types.ts +0 -84
- package/src/cluster/commands.ts +0 -382
- package/src/cluster/remote-daemon-target.ts +0 -225
- package/src/cluster/render.ts +0 -268
- package/src/config/atomic-write.ts +0 -70
- package/src/config/credential-availability.ts +0 -122
- package/src/config/goodvibes-home.ts +0 -149
- package/src/config/index.ts +0 -88
- package/src/config/provider-model.ts +0 -23
- package/src/config/read-versioned.ts +0 -228
- package/src/core/alert-gating.ts +0 -67
- package/src/core/consolidation-receipt.ts +0 -41
- package/src/core/focus-tracker.ts +0 -41
- package/src/core/history.ts +0 -45
- package/src/core/memory-status.ts +0 -118
- package/src/core/orchestrator.ts +0 -7
- package/src/core/pairing-devices.ts +0 -49
- package/src/core/pairing-handoff.ts +0 -101
- package/src/core/pairing-offers.ts +0 -83
- package/src/core/pairing-origin.ts +0 -78
- package/src/core/rewind-turn-anchors.ts +0 -311
- package/src/core/session-recovery.ts +0 -210
- package/src/core/stable-host.ts +0 -171
- package/src/core/transcript-journal.ts +0 -503
- package/src/daemon/cli.ts +0 -641
- package/src/daemon/handlers/context.ts +0 -29
- package/src/daemon/handlers/contracts.ts +0 -77
- package/src/daemon/handlers/credentials.ts +0 -139
- package/src/daemon/handlers/drafts/draft-store.ts +0 -427
- package/src/daemon/handlers/drafts/index.ts +0 -17
- package/src/daemon/handlers/drafts/register.ts +0 -331
- package/src/daemon/handlers/errors.ts +0 -18
- package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
- package/src/daemon/handlers/inbox/index.ts +0 -284
- package/src/daemon/handlers/inbox/mapping.ts +0 -192
- package/src/daemon/handlers/inbox/poller.ts +0 -212
- package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
- package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
- package/src/daemon/handlers/inbox/providers/email.ts +0 -152
- package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
- package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
- package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
- package/src/daemon/handlers/index.ts +0 -117
- package/src/daemon/handlers/register.ts +0 -168
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
- package/src/daemon/handlers/remote/backends/docker.ts +0 -79
- package/src/daemon/handlers/remote/backends/index.ts +0 -40
- package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
- package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
- package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
- package/src/daemon/handlers/remote/backends/types.ts +0 -97
- package/src/daemon/handlers/remote/dispatcher.ts +0 -181
- package/src/daemon/handlers/remote/index.ts +0 -119
- package/src/daemon/handlers/remote/peer-registry.ts +0 -357
- package/src/daemon/handlers/remote/service.ts +0 -191
- package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
- package/src/daemon/handlers/routing/index.ts +0 -261
- package/src/daemon/handlers/routing/route-store.ts +0 -319
- package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
- package/src/daemon/handlers/sqlite-store.ts +0 -278
- package/src/daemon/handlers/triage/index.ts +0 -57
- package/src/daemon/handlers/triage/integration.ts +0 -213
- package/src/daemon/handlers/triage/pipeline.ts +0 -274
- package/src/daemon/handlers/triage/scorer.ts +0 -287
- package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
- package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
- package/src/daemon/handlers/triage/tagger/index.ts +0 -184
- package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
- package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
- package/src/daemon/handlers/triage/types.ts +0 -50
- package/src/daemon/lifecycle.ts +0 -41
- package/src/daemon/provision-wake-model.ts +0 -81
- package/src/daemon/safe-serve.ts +0 -61
- package/src/daemon/send/channels.ts +0 -200
- package/src/daemon/send/command.ts +0 -332
- package/src/daemon/send/composition.ts +0 -99
- package/src/daemon/send/failure-text.ts +0 -93
- package/src/daemon/send/inert-text.ts +0 -225
- package/src/daemon/send/stdin.ts +0 -24
- package/src/daemon/service-commands.ts +0 -329
- package/src/export/cost-utils.ts +0 -319
- package/src/input/selection.ts +0 -135
- package/src/plugins/loader.ts +0 -15
- package/src/renderer/bottom-bar.ts +0 -58
- package/src/renderer/conversation-tree.ts +0 -228
- package/src/runtime/agent-graph-composition.ts +0 -78
- package/src/runtime/channel-composition.ts +0 -46
- package/src/runtime/cluster-composition.ts +0 -124
- package/src/runtime/cluster-group-composition.ts +0 -284
- package/src/runtime/code-index-services.ts +0 -167
- package/src/runtime/daemon-handler-composition.ts +0 -76
- package/src/runtime/device-posture-composition.ts +0 -110
- package/src/runtime/durability-housekeeping.ts +0 -183
- package/src/runtime/durability-services.ts +0 -155
- package/src/runtime/embedded-service-factories.ts +0 -58
- package/src/runtime/feature-settings.ts +0 -154
- package/src/runtime/fleet-needs-input-push.ts +0 -70
- package/src/runtime/fleet-services.ts +0 -41
- package/src/runtime/idle-power-services.ts +0 -85
- package/src/runtime/mail-composition.ts +0 -65
- package/src/runtime/memory-fold.ts +0 -55
- package/src/runtime/operator-token-cleanup.ts +0 -109
- package/src/runtime/path-shadow.ts +0 -418
- package/src/runtime/perf/panel-health-monitor.ts +0 -18
- package/src/runtime/provider-optimizer-wiring.ts +0 -55
- package/src/runtime/reachability-notice.ts +0 -100
- package/src/runtime/remote-execution-composition.ts +0 -62
- package/src/runtime/session-liveness-marker.ts +0 -257
- package/src/runtime/session-pointer-surface.ts +0 -22
- package/src/runtime/session-storage-services.ts +0 -41
- package/src/runtime/surface-feature-flags.ts +0 -96
- package/src/runtime/terminal-output-guard.ts +0 -235
- package/src/runtime/trust/workspace-registration.ts +0 -192
- package/src/runtime/trust/workspace-trust.ts +0 -229
- package/src/runtime/ui/index.ts +0 -39
- package/src/runtime/ui/model-picker/data-provider.ts +0 -182
- package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
- package/src/runtime/ui/model-picker/index.ts +0 -59
- package/src/runtime/ui/model-picker/types.ts +0 -153
- package/src/runtime/ui/provider-health/data-provider.ts +0 -244
- package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
- package/src/runtime/ui/provider-health/index.ts +0 -46
- package/src/runtime/ui/provider-health/types.ts +0 -146
- package/src/runtime/ui-events.ts +0 -1
- package/src/runtime/ui-read-model-helpers.ts +0 -1
- package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
- package/src/runtime/ui-read-models-observability-options.ts +0 -1
- package/src/runtime/ui-read-models-observability-remote.ts +0 -1
- package/src/runtime/ui-read-models-observability-security.ts +0 -1
- package/src/runtime/ui-read-models-observability-system.ts +0 -1
- package/src/runtime/ui-read-models-observability.ts +0 -1
- package/src/runtime/ui-service-queries.ts +0 -1
- package/src/runtime/voice-setup-services.ts +0 -98
- package/src/runtime/workstream-attempts-validation.ts +0 -77
- package/src/runtime/workstream-draft-edits.ts +0 -95
- package/src/runtime/workstream-draft-store.ts +0 -274
- package/src/runtime/workstream-draft-types.ts +0 -42
- package/src/runtime/workstream-services.ts +0 -373
- package/src/tools/index.ts +0 -1
- package/src/types/grid.ts +0 -48
- package/src/types/sql-js.d.ts +0 -15
- package/src/work-plans/work-plan-store.ts +0 -702
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single SDK-contract import seam for the daemon handler layer.
|
|
3
|
-
*
|
|
4
|
-
* Every other module under `src/daemon/handlers/` imports SDK contract
|
|
5
|
-
* identifiers from HERE and nowhere else. Concentrating the SDK imports in one
|
|
6
|
-
* concrete-submodule module keeps the rest of the layer free of barrel cycles
|
|
7
|
-
* and guarantees the host NEVER re-declares an SDK id, descriptor, or schema —
|
|
8
|
-
* it only attaches handlers to the descriptors the SDK already registered.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// Catalog + invocation contract types (concrete control-plane subpath, not a project barrel).
|
|
12
|
-
export type {
|
|
13
|
-
GatewayMethodCatalog,
|
|
14
|
-
GatewayMethodDescriptor,
|
|
15
|
-
GatewayMethodInvocation,
|
|
16
|
-
GatewayMethodInvocationContext,
|
|
17
|
-
GatewayMethodHandler,
|
|
18
|
-
} from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
19
|
-
|
|
20
|
-
// Channel domain types reused in handler signatures (read-only SDK interfaces; never re-declared).
|
|
21
|
-
export type {
|
|
22
|
-
ChannelIdentity,
|
|
23
|
-
ChannelResolvedTarget,
|
|
24
|
-
ChannelAccountRecord,
|
|
25
|
-
} from '@pellux/goodvibes-sdk/platform/channels';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Per-peer authentication envelope passed to peer-scoped remote routes.
|
|
29
|
-
*
|
|
30
|
-
* The daemon-sdk ships `RemotePeerAuth` only as an UNEXPORTED local alias
|
|
31
|
-
* inside `@pellux/goodvibes-daemon-sdk/remote-routes` (it is `unknown` there),
|
|
32
|
-
* so it cannot be re-exported. We mirror that exact shape here for the host
|
|
33
|
-
* implementation. This is an implementable runtime contract, not a method
|
|
34
|
-
* descriptor or schema — declaring it does not violate the no-re-declaration
|
|
35
|
-
* rule for catalog methods.
|
|
36
|
-
*/
|
|
37
|
-
export type RemotePeerAuth = unknown;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Remote distributed-runtime route service the HOST must implement and supply
|
|
41
|
-
* as `RuntimeServices.distributedRuntime`. The SDK facade injects the instance
|
|
42
|
-
* into `DaemonRemoteRouteContext.distributedRuntime` so the published
|
|
43
|
-
* `remote.peers.*` HTTP routes can dispatch to it.
|
|
44
|
-
*
|
|
45
|
-
* The daemon-sdk declares this interface locally in
|
|
46
|
-
* `@pellux/goodvibes-daemon-sdk/remote-routes` but does NOT export it (the
|
|
47
|
-
* module ends with `export {}`), so it cannot be imported. This declaration
|
|
48
|
-
* mirrors the SDK's exact structural shape (17 methods, verbatim signatures)
|
|
49
|
-
* so a host implementation is assignable to the SDK's context field. The SDK
|
|
50
|
-
* ships no docker/ssh/cloud backend — the host owns the implementation.
|
|
51
|
-
*/
|
|
52
|
-
export interface DistributedRuntimeRouteService {
|
|
53
|
-
listPairRequests(): unknown;
|
|
54
|
-
approvePairRequest(requestId: string, input: Record<string, unknown>): Promise<unknown | null>;
|
|
55
|
-
rejectPairRequest(requestId: string, input: Record<string, unknown>): Promise<unknown | null>;
|
|
56
|
-
listPeers(): unknown;
|
|
57
|
-
rotatePeerToken(peerId: string, input: Record<string, unknown>): Promise<unknown | null>;
|
|
58
|
-
revokePeerToken(peerId: string, input: Record<string, unknown>): Promise<unknown | null>;
|
|
59
|
-
disconnectPeer(peerId: string, input: Record<string, unknown>): Promise<unknown | null>;
|
|
60
|
-
listWork(): unknown;
|
|
61
|
-
invokePeer(input: Record<string, unknown>): Promise<unknown>;
|
|
62
|
-
cancelWork(workId: string, input: Record<string, unknown>): Promise<unknown | null>;
|
|
63
|
-
getNodeHostContract(): unknown;
|
|
64
|
-
requestPairing(input: Record<string, unknown>): Promise<unknown>;
|
|
65
|
-
verifyPairRequest(
|
|
66
|
-
requestId: string,
|
|
67
|
-
challenge: string,
|
|
68
|
-
input: Record<string, unknown>,
|
|
69
|
-
): Promise<unknown | null>;
|
|
70
|
-
heartbeatPeer(auth: RemotePeerAuth, input: Record<string, unknown>): Promise<unknown>;
|
|
71
|
-
claimWork(auth: RemotePeerAuth, input: Record<string, unknown>): Promise<unknown>;
|
|
72
|
-
completeWork(
|
|
73
|
-
auth: RemotePeerAuth,
|
|
74
|
-
workId: string,
|
|
75
|
-
input: Record<string, unknown>,
|
|
76
|
-
): Promise<unknown | null>;
|
|
77
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
2
|
-
import type { SecretsManager, SecretScope } from '../../config/secrets.ts';
|
|
3
|
-
import {
|
|
4
|
-
buildGoodVibesSecretKey,
|
|
5
|
-
isSecretReferenceValue,
|
|
6
|
-
} from '../../config/secret-config.ts';
|
|
7
|
-
|
|
8
|
-
export interface DaemonCredentialStore {
|
|
9
|
-
/**
|
|
10
|
-
* Resolve a goodvibes://secrets/ reference OR a raw secret key to its
|
|
11
|
-
* plaintext value (daemon-internal only).
|
|
12
|
-
*/
|
|
13
|
-
resolveRef(ref: string): Promise<string | null>;
|
|
14
|
-
/**
|
|
15
|
-
* Resolve a config-key-derived secret
|
|
16
|
-
* (e.g. 'surfaces.slack.botToken' → GOODVIBES_SURFACES_SLACK_BOT_TOKEN).
|
|
17
|
-
*/
|
|
18
|
-
resolveConfigSecret(configKey: string): Promise<string | null>;
|
|
19
|
-
/**
|
|
20
|
-
* Store a daemon-owned credential. scope defaults 'daemon'.
|
|
21
|
-
*
|
|
22
|
-
* This is the DAEMON's own credential store: every caller is daemon code, and
|
|
23
|
-
* every value written through it is one the daemon itself has to read back —
|
|
24
|
-
* including its own generated draft encryption key (createAtRestCipher below,
|
|
25
|
-
* which calls put() with no scope at all). Defaulting to 'user' filed those
|
|
26
|
-
* in a surface-scoped tier, so the daemon's own material lived somewhere it
|
|
27
|
-
* only found by accident, and a value written on one surface's behalf was
|
|
28
|
-
* invisible to the next.
|
|
29
|
-
*/
|
|
30
|
-
put(
|
|
31
|
-
secretKey: string,
|
|
32
|
-
value: string,
|
|
33
|
-
opts?: { scope?: SecretScope; medium?: 'plaintext' | 'secure' },
|
|
34
|
-
): Promise<void>;
|
|
35
|
-
has(secretKey: string): Promise<boolean>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Extract the trailing secret-key segment from a goodvibes://secrets/ reference. */
|
|
39
|
-
function secretKeyFromReference(ref: string): string {
|
|
40
|
-
const normalized = ref.trim();
|
|
41
|
-
const segments = normalized.split('/');
|
|
42
|
-
const last = segments[segments.length - 1] ?? '';
|
|
43
|
-
return decodeURIComponent(last);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function createDaemonCredentialStore(secrets: SecretsManager): DaemonCredentialStore {
|
|
47
|
-
return {
|
|
48
|
-
async resolveRef(ref: string): Promise<string | null> {
|
|
49
|
-
const key = isSecretReferenceValue(ref) ? secretKeyFromReference(ref) : ref;
|
|
50
|
-
return secrets.get(key);
|
|
51
|
-
},
|
|
52
|
-
async resolveConfigSecret(configKey: string): Promise<string | null> {
|
|
53
|
-
return secrets.get(buildGoodVibesSecretKey(configKey));
|
|
54
|
-
},
|
|
55
|
-
async put(
|
|
56
|
-
secretKey: string,
|
|
57
|
-
value: string,
|
|
58
|
-
opts?: { scope?: SecretScope; medium?: 'plaintext' | 'secure' },
|
|
59
|
-
): Promise<void> {
|
|
60
|
-
await secrets.set(secretKey, value, {
|
|
61
|
-
scope: opts?.scope ?? 'daemon',
|
|
62
|
-
medium: opts?.medium ?? 'secure',
|
|
63
|
-
});
|
|
64
|
-
},
|
|
65
|
-
async has(secretKey: string): Promise<boolean> {
|
|
66
|
-
const value = await secrets.get(secretKey);
|
|
67
|
-
return value !== null && value.length > 0;
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
// At-rest encryption helper for drafts (draft body must be encrypted at rest).
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
export interface AtRestCipher {
|
|
77
|
-
encrypt(plaintext: string): Promise<string>;
|
|
78
|
-
decrypt(ciphertext: string): Promise<string>;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const DEFAULT_DRAFT_KEY_NAME = 'GOODVIBES_DAEMON_DRAFT_AESKEY';
|
|
82
|
-
const AES_KEY_BYTES = 32; // AES-256
|
|
83
|
-
const GCM_IV_BYTES = 12;
|
|
84
|
-
const GCM_TAG_BYTES = 16;
|
|
85
|
-
|
|
86
|
-
async function loadOrCreateKey(
|
|
87
|
-
store: DaemonCredentialStore,
|
|
88
|
-
keyName: string,
|
|
89
|
-
): Promise<Buffer> {
|
|
90
|
-
const existing = await store.resolveRef(keyName);
|
|
91
|
-
if (existing && existing.length > 0) {
|
|
92
|
-
const buf = Buffer.from(existing, 'base64');
|
|
93
|
-
if (buf.length === AES_KEY_BYTES) return buf;
|
|
94
|
-
}
|
|
95
|
-
const generated = randomBytes(AES_KEY_BYTES);
|
|
96
|
-
await store.put(keyName, generated.toString('base64'), { medium: 'secure' });
|
|
97
|
-
return generated;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Create an AES-256-GCM at-rest cipher backed by a daemon-owned key.
|
|
102
|
-
* encrypt() returns base64(iv | tag | ciphertext). Uses node:crypto (Bun-compatible).
|
|
103
|
-
* NEVER log resolved keys or plaintext.
|
|
104
|
-
*/
|
|
105
|
-
export function createAtRestCipher(
|
|
106
|
-
store: DaemonCredentialStore,
|
|
107
|
-
keyName: string = DEFAULT_DRAFT_KEY_NAME,
|
|
108
|
-
): AtRestCipher {
|
|
109
|
-
let keyPromise: Promise<Buffer> | null = null;
|
|
110
|
-
const getKey = (): Promise<Buffer> => {
|
|
111
|
-
if (!keyPromise) keyPromise = loadOrCreateKey(store, keyName);
|
|
112
|
-
return keyPromise;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
async encrypt(plaintext: string): Promise<string> {
|
|
117
|
-
const key = await getKey();
|
|
118
|
-
const iv = randomBytes(GCM_IV_BYTES);
|
|
119
|
-
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
120
|
-
const encrypted = Buffer.concat([
|
|
121
|
-
cipher.update(plaintext, 'utf-8'),
|
|
122
|
-
cipher.final(),
|
|
123
|
-
]);
|
|
124
|
-
const tag = cipher.getAuthTag();
|
|
125
|
-
return Buffer.concat([iv, tag, encrypted]).toString('base64');
|
|
126
|
-
},
|
|
127
|
-
async decrypt(ciphertext: string): Promise<string> {
|
|
128
|
-
const key = await getKey();
|
|
129
|
-
const raw = Buffer.from(ciphertext, 'base64');
|
|
130
|
-
const iv = raw.subarray(0, GCM_IV_BYTES);
|
|
131
|
-
const tag = raw.subarray(GCM_IV_BYTES, GCM_IV_BYTES + GCM_TAG_BYTES);
|
|
132
|
-
const data = raw.subarray(GCM_IV_BYTES + GCM_TAG_BYTES);
|
|
133
|
-
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
134
|
-
decipher.setAuthTag(tag);
|
|
135
|
-
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
136
|
-
return decrypted.toString('utf-8');
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
}
|
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import type { AtRestCipher } from '../credentials.ts';
|
|
3
|
-
import { HandlerSqliteStore } from '../sqlite-store.ts';
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Draft Sync Backend store (rebased onto the daemon handler foundation).
|
|
7
|
-
//
|
|
8
|
-
// Mirrors the agent's local draft store (channels/drafts.json) server-side so
|
|
9
|
-
// drafts are visible across surfaces. The agent's local file remains the source
|
|
10
|
-
// of truth; this store is a sync mirror. Conflict resolution (most-recent
|
|
11
|
-
// updatedAt wins) is the integrator's concern — this store performs a plain
|
|
12
|
-
// upsert and records the supplied/derived updatedAt.
|
|
13
|
-
//
|
|
14
|
-
// SECURITY POSTURE:
|
|
15
|
-
// - The plaintext message body is NEVER persisted. It is encrypted at rest
|
|
16
|
-
// via the daemon at-rest cipher (AES-256-GCM) as `bodyEnc`.
|
|
17
|
-
// - The webhook URL is encrypted at rest (`webhookEnc`) and is ALWAYS
|
|
18
|
-
// redacted ('[redacted]') in every list/get response — the raw URL never
|
|
19
|
-
// leaves the store.
|
|
20
|
-
// - `messageDigest` (sha256First(body, 12)) is computed at save time and
|
|
21
|
-
// stored alongside, so reads never need to decrypt the body. The required
|
|
22
|
-
// `message` field of every emitted record carries the DIGEST, never the
|
|
23
|
-
// plaintext body.
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
/** SHA-256 of `input`, truncated to the first `hexChars` hex characters. */
|
|
27
|
-
export function sha256First(input: string, hexChars: number): string {
|
|
28
|
-
const digest = createHash('sha256').update(input, 'utf-8').digest('hex');
|
|
29
|
-
return digest.slice(0, Math.max(0, hexChars));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Redact a stored webhook for responses: present → '[redacted]', else undefined. */
|
|
33
|
-
export function redactWebhook(value?: string | null): string | undefined {
|
|
34
|
-
return value === undefined || value === null || value === '' ? undefined : '[redacted]';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type DraftStatus = 'draft' | 'queued' | 'sent' | 'failed';
|
|
38
|
-
|
|
39
|
-
/** Statuses callers are permitted to write via save(). */
|
|
40
|
-
export const WRITABLE_DRAFT_STATUSES: readonly DraftStatus[] = ['draft', 'queued'];
|
|
41
|
-
|
|
42
|
-
/** All known statuses (writable + terminal states set by the send pipeline). */
|
|
43
|
-
export const ALL_DRAFT_STATUSES: readonly DraftStatus[] = [
|
|
44
|
-
'draft',
|
|
45
|
-
'queued',
|
|
46
|
-
'sent',
|
|
47
|
-
'failed',
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
export const DRAFT_MESSAGE_DIGEST_HEX = 12;
|
|
51
|
-
export const DEFAULT_DRAFT_VERSION = 1;
|
|
52
|
-
export const DEFAULT_DRAFT_LIST_LIMIT = 50;
|
|
53
|
-
export const MAX_DRAFT_LIST_LIMIT = 200;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Public draft record returned by list/get/save. Matches the SDK
|
|
57
|
-
* CHANNEL_DRAFT_SCHEMA: the required `message` field carries the message
|
|
58
|
-
* DIGEST (never the plaintext body). `webhook` is always '[redacted]' when set.
|
|
59
|
-
* `messageDigest` is emitted ONLY by `get` (whose output schema permits
|
|
60
|
-
* additional properties); strict list/save records omit it.
|
|
61
|
-
*/
|
|
62
|
-
export interface DraftRecord {
|
|
63
|
-
version: number;
|
|
64
|
-
id: string;
|
|
65
|
-
createdAt: string; // ISO-8601
|
|
66
|
-
updatedAt: string; // ISO-8601
|
|
67
|
-
status: DraftStatus;
|
|
68
|
-
message: string; // sha256First(body, 12) digest — plaintext body is NEVER transmitted
|
|
69
|
-
title?: string;
|
|
70
|
-
channel?: string;
|
|
71
|
-
route?: string;
|
|
72
|
-
webhook?: string; // '[redacted]' when a webhook is stored, else omitted
|
|
73
|
-
link?: string;
|
|
74
|
-
tags?: string[];
|
|
75
|
-
sentResponseId?: string;
|
|
76
|
-
sendError?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Normalized input accepted by upsert(). `message` is the FULL plaintext body. */
|
|
80
|
-
export interface DraftSaveInput {
|
|
81
|
-
id?: string;
|
|
82
|
-
version?: number;
|
|
83
|
-
title?: string;
|
|
84
|
-
message: string;
|
|
85
|
-
channel?: string;
|
|
86
|
-
route?: string;
|
|
87
|
-
webhook?: string;
|
|
88
|
-
link?: string;
|
|
89
|
-
tags?: string[];
|
|
90
|
-
status?: DraftStatus;
|
|
91
|
-
createdAt?: string;
|
|
92
|
-
/**
|
|
93
|
-
* Caller-supplied last-modified timestamp (ISO-8601). When provided it is
|
|
94
|
-
* persisted verbatim, enabling the sync contract's 'most recent updatedAt
|
|
95
|
-
* wins' conflict model; when omitted the store stamps updatedAt = now().
|
|
96
|
-
*/
|
|
97
|
-
updatedAt?: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface DraftListQuery {
|
|
101
|
-
status?: DraftStatus;
|
|
102
|
-
limit?: number;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export interface DraftSaveResult {
|
|
106
|
-
draft: DraftRecord;
|
|
107
|
-
created: boolean;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface DraftSyncStoreOptions {
|
|
111
|
-
workingDirectory: string;
|
|
112
|
-
cipher: AtRestCipher;
|
|
113
|
-
/** Override the sqlite filename (tests). Defaults to 'drafts.sqlite'. */
|
|
114
|
-
fileName?: string;
|
|
115
|
-
/** UUID generator injection point (tests). Defaults to crypto.randomUUID. */
|
|
116
|
-
generateId?: () => string;
|
|
117
|
-
/** Clock injection point (tests). Defaults to () => new Date().toISOString(). */
|
|
118
|
-
now?: () => string;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const SCHEMA: string[] = [
|
|
122
|
-
`CREATE TABLE IF NOT EXISTS drafts (
|
|
123
|
-
id TEXT PRIMARY KEY,
|
|
124
|
-
version INTEGER NOT NULL,
|
|
125
|
-
createdAt TEXT NOT NULL,
|
|
126
|
-
updatedAt TEXT NOT NULL,
|
|
127
|
-
status TEXT NOT NULL,
|
|
128
|
-
title TEXT,
|
|
129
|
-
bodyEnc TEXT NOT NULL,
|
|
130
|
-
messageDigest TEXT NOT NULL,
|
|
131
|
-
channel TEXT,
|
|
132
|
-
route TEXT,
|
|
133
|
-
webhookEnc TEXT,
|
|
134
|
-
link TEXT,
|
|
135
|
-
tags TEXT,
|
|
136
|
-
sentResponseId TEXT,
|
|
137
|
-
sendError TEXT
|
|
138
|
-
)`,
|
|
139
|
-
'CREATE INDEX IF NOT EXISTS idx_drafts_status ON drafts (status)',
|
|
140
|
-
'CREATE INDEX IF NOT EXISTS idx_drafts_updatedAt ON drafts (updatedAt)',
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
interface DraftRow {
|
|
144
|
-
id: string;
|
|
145
|
-
version: number;
|
|
146
|
-
createdAt: string;
|
|
147
|
-
updatedAt: string;
|
|
148
|
-
status: string;
|
|
149
|
-
title: string | null;
|
|
150
|
-
messageDigest: string;
|
|
151
|
-
channel: string | null;
|
|
152
|
-
route: string | null;
|
|
153
|
-
webhookEnc: string | null;
|
|
154
|
-
link: string | null;
|
|
155
|
-
tags: string | null;
|
|
156
|
-
sentResponseId: string | null;
|
|
157
|
-
sendError: string | null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function nullable(value: string | undefined): string | null {
|
|
161
|
-
return value === undefined ? null : value;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function parseTags(raw: string | null): string[] | undefined {
|
|
165
|
-
if (raw === null || raw === '') return undefined;
|
|
166
|
-
try {
|
|
167
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
168
|
-
if (Array.isArray(parsed)) {
|
|
169
|
-
const tags = parsed.filter((t): t is string => typeof t === 'string');
|
|
170
|
-
return tags.length > 0 ? tags : undefined;
|
|
171
|
-
}
|
|
172
|
-
} catch {
|
|
173
|
-
// Corrupt tags column — treat as absent rather than throwing on read.
|
|
174
|
-
}
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function normalizeStatus(status: DraftStatus | undefined): DraftStatus {
|
|
179
|
-
return status ?? 'draft';
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function normalizeVersion(version: number | undefined): number {
|
|
183
|
-
if (version === undefined || !Number.isFinite(version)) return DEFAULT_DRAFT_VERSION;
|
|
184
|
-
const floored = Math.floor(version);
|
|
185
|
-
return floored < 1 ? DEFAULT_DRAFT_VERSION : floored;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Server-side mirror of agent drafts. Wraps HandlerSqliteStore; all body and
|
|
190
|
-
* webhook material is encrypted at rest via the injected AtRestCipher.
|
|
191
|
-
*/
|
|
192
|
-
export class DraftSyncStore {
|
|
193
|
-
private readonly store: HandlerSqliteStore;
|
|
194
|
-
private readonly cipher: AtRestCipher;
|
|
195
|
-
private readonly generateId: () => string;
|
|
196
|
-
private readonly now: () => string;
|
|
197
|
-
private initialized = false;
|
|
198
|
-
|
|
199
|
-
constructor(options: DraftSyncStoreOptions) {
|
|
200
|
-
this.store = new HandlerSqliteStore({
|
|
201
|
-
workingDirectory: options.workingDirectory,
|
|
202
|
-
fileName: options.fileName ?? 'drafts.sqlite',
|
|
203
|
-
schema: SCHEMA,
|
|
204
|
-
});
|
|
205
|
-
this.cipher = options.cipher;
|
|
206
|
-
this.generateId = options.generateId ?? (() => crypto.randomUUID());
|
|
207
|
-
this.now = options.now ?? (() => new Date().toISOString());
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
get dbPath(): string {
|
|
211
|
-
return this.store.dbPath;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async init(): Promise<void> {
|
|
215
|
-
if (this.initialized) return;
|
|
216
|
-
await this.store.init();
|
|
217
|
-
this.initialized = true;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Persist the in-memory db to disk (atomic tmp+rename via the base store). */
|
|
221
|
-
async save(): Promise<void> {
|
|
222
|
-
await this.store.save();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
close(): void {
|
|
226
|
-
this.store.close();
|
|
227
|
-
this.initialized = false;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Create (no id) or update (id present) a draft. Encrypts the body and
|
|
232
|
-
* webhook at rest, computes messageDigest, preserves createdAt on update, and
|
|
233
|
-
* sets updatedAt to input.updatedAt when present (enabling 'most recent
|
|
234
|
-
* updatedAt wins') or now() otherwise.
|
|
235
|
-
*
|
|
236
|
-
* FULL-REPLACE SEMANTICS (intentional, per the full-snapshot sync contract):
|
|
237
|
-
* every caller-supplied field is a complete snapshot, NOT a partial patch.
|
|
238
|
-
* The ON CONFLICT UPDATE SET clause assigns each column from the incoming row,
|
|
239
|
-
* so an OMITTED optional field is CLEARED on update (webhookEnc/tags/title/
|
|
240
|
-
* channel/route/link → NULL). The ONLY exceptions are sentResponseId/
|
|
241
|
-
* sendError, which are owned by the send pipeline and survive a save().
|
|
242
|
-
*/
|
|
243
|
-
async upsert(input: DraftSaveInput): Promise<DraftSaveResult> {
|
|
244
|
-
const existing = input.id !== undefined ? this.getRow(input.id) : null;
|
|
245
|
-
const created = existing === null;
|
|
246
|
-
const id = existing?.id ?? input.id ?? this.generateId();
|
|
247
|
-
const timestamp = this.now();
|
|
248
|
-
const createdAt = existing?.createdAt ?? input.createdAt ?? timestamp;
|
|
249
|
-
const updatedAt = input.updatedAt ?? timestamp;
|
|
250
|
-
const status = normalizeStatus(input.status);
|
|
251
|
-
const version = normalizeVersion(input.version);
|
|
252
|
-
|
|
253
|
-
const bodyEnc = await this.cipher.encrypt(input.message);
|
|
254
|
-
const messageDigest = sha256First(input.message, DRAFT_MESSAGE_DIGEST_HEX);
|
|
255
|
-
const webhookEnc =
|
|
256
|
-
input.webhook !== undefined && input.webhook !== ''
|
|
257
|
-
? await this.cipher.encrypt(input.webhook)
|
|
258
|
-
: null;
|
|
259
|
-
const tagsJson =
|
|
260
|
-
input.tags !== undefined && input.tags.length > 0
|
|
261
|
-
? JSON.stringify(input.tags)
|
|
262
|
-
: null;
|
|
263
|
-
|
|
264
|
-
this.store.run(
|
|
265
|
-
`INSERT INTO drafts (
|
|
266
|
-
id, version, createdAt, updatedAt, status, title, bodyEnc, messageDigest,
|
|
267
|
-
channel, route, webhookEnc, link, tags, sentResponseId, sendError
|
|
268
|
-
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
269
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
270
|
-
version = excluded.version,
|
|
271
|
-
updatedAt = excluded.updatedAt,
|
|
272
|
-
status = excluded.status,
|
|
273
|
-
title = excluded.title,
|
|
274
|
-
bodyEnc = excluded.bodyEnc,
|
|
275
|
-
messageDigest = excluded.messageDigest,
|
|
276
|
-
channel = excluded.channel,
|
|
277
|
-
route = excluded.route,
|
|
278
|
-
webhookEnc = excluded.webhookEnc,
|
|
279
|
-
link = excluded.link,
|
|
280
|
-
tags = excluded.tags`,
|
|
281
|
-
[
|
|
282
|
-
id,
|
|
283
|
-
version,
|
|
284
|
-
createdAt,
|
|
285
|
-
updatedAt,
|
|
286
|
-
status,
|
|
287
|
-
nullable(input.title),
|
|
288
|
-
bodyEnc,
|
|
289
|
-
messageDigest,
|
|
290
|
-
nullable(input.channel),
|
|
291
|
-
nullable(input.route),
|
|
292
|
-
webhookEnc,
|
|
293
|
-
nullable(input.link),
|
|
294
|
-
tagsJson,
|
|
295
|
-
// sentResponseId / sendError are owned by the send pipeline, not save().
|
|
296
|
-
// On update they are preserved (omitted from UPDATE SET); on insert NULL.
|
|
297
|
-
null,
|
|
298
|
-
null,
|
|
299
|
-
],
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
const row = this.getRow(id);
|
|
303
|
-
return { draft: toRecord(row ?? this.fallbackRow(id, version, createdAt, updatedAt, status, messageDigest, webhookEnc, input)), created };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** List drafts, newest-updated first, optionally filtered by status. */
|
|
307
|
-
list(query: DraftListQuery = {}): DraftRecord[] {
|
|
308
|
-
const limit = clampLimit(query.limit);
|
|
309
|
-
const rows =
|
|
310
|
-
query.status !== undefined
|
|
311
|
-
? this.store.all<DraftRow>(
|
|
312
|
-
`SELECT id, version, createdAt, updatedAt, status, title, messageDigest,
|
|
313
|
-
channel, route, webhookEnc, link, tags, sentResponseId, sendError
|
|
314
|
-
FROM drafts WHERE status = ? ORDER BY updatedAt DESC, id ASC LIMIT ?`,
|
|
315
|
-
[query.status, limit],
|
|
316
|
-
)
|
|
317
|
-
: this.store.all<DraftRow>(
|
|
318
|
-
`SELECT id, version, createdAt, updatedAt, status, title, messageDigest,
|
|
319
|
-
channel, route, webhookEnc, link, tags, sentResponseId, sendError
|
|
320
|
-
FROM drafts ORDER BY updatedAt DESC, id ASC LIMIT ?`,
|
|
321
|
-
[limit],
|
|
322
|
-
);
|
|
323
|
-
return rows.map((row) => toRecord(row));
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/** Total number of stored drafts (optionally status-filtered). */
|
|
327
|
-
count(status?: DraftStatus): number {
|
|
328
|
-
const row =
|
|
329
|
-
status !== undefined
|
|
330
|
-
? this.store.get<{ n: number }>('SELECT COUNT(*) AS n FROM drafts WHERE status = ?', [status])
|
|
331
|
-
: this.store.get<{ n: number }>('SELECT COUNT(*) AS n FROM drafts');
|
|
332
|
-
return row?.n ?? 0;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** Fetch a single draft as a redacted record, or null when absent. */
|
|
336
|
-
get(id: string): DraftRecord | null {
|
|
337
|
-
const row = this.getRow(id);
|
|
338
|
-
return row === null ? null : toRecord(row);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** Delete a draft. Returns true when a row was removed. */
|
|
342
|
-
delete(id: string): boolean {
|
|
343
|
-
const existed = this.getRow(id) !== null;
|
|
344
|
-
if (existed) {
|
|
345
|
-
this.store.run('DELETE FROM drafts WHERE id = ?', [id]);
|
|
346
|
-
}
|
|
347
|
-
return existed;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
private getRow(id: string): DraftRow | null {
|
|
351
|
-
return this.store.get<DraftRow>(
|
|
352
|
-
`SELECT id, version, createdAt, updatedAt, status, title, messageDigest,
|
|
353
|
-
channel, route, webhookEnc, link, tags, sentResponseId, sendError
|
|
354
|
-
FROM drafts WHERE id = ?`,
|
|
355
|
-
[id],
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Defensive synthesis if a read-back ever returns null (should not happen for
|
|
360
|
-
// the row we just wrote); keeps upsert total even under a degraded backend.
|
|
361
|
-
private fallbackRow(
|
|
362
|
-
id: string,
|
|
363
|
-
version: number,
|
|
364
|
-
createdAt: string,
|
|
365
|
-
updatedAt: string,
|
|
366
|
-
status: DraftStatus,
|
|
367
|
-
messageDigest: string,
|
|
368
|
-
webhookEnc: string | null,
|
|
369
|
-
input: DraftSaveInput,
|
|
370
|
-
): DraftRow {
|
|
371
|
-
return {
|
|
372
|
-
id,
|
|
373
|
-
version,
|
|
374
|
-
createdAt,
|
|
375
|
-
updatedAt,
|
|
376
|
-
status,
|
|
377
|
-
title: nullable(input.title),
|
|
378
|
-
messageDigest,
|
|
379
|
-
channel: nullable(input.channel),
|
|
380
|
-
route: nullable(input.route),
|
|
381
|
-
webhookEnc,
|
|
382
|
-
link: nullable(input.link),
|
|
383
|
-
tags:
|
|
384
|
-
input.tags !== undefined && input.tags.length > 0
|
|
385
|
-
? JSON.stringify(input.tags)
|
|
386
|
-
: null,
|
|
387
|
-
sentResponseId: null,
|
|
388
|
-
sendError: null,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function clampLimit(limit: number | undefined): number {
|
|
394
|
-
if (limit === undefined || !Number.isFinite(limit)) return DEFAULT_DRAFT_LIST_LIMIT;
|
|
395
|
-
const floored = Math.floor(limit);
|
|
396
|
-
if (floored < 1) return 1;
|
|
397
|
-
if (floored > MAX_DRAFT_LIST_LIMIT) return MAX_DRAFT_LIST_LIMIT;
|
|
398
|
-
return floored;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Map a stored row to a wire record. The required `message` field carries the
|
|
403
|
-
* DIGEST (never plaintext); the webhook is redacted; tags are parsed.
|
|
404
|
-
*/
|
|
405
|
-
export function toRecord(row: DraftRow): DraftRecord {
|
|
406
|
-
const record: DraftRecord = {
|
|
407
|
-
version: row.version,
|
|
408
|
-
id: row.id,
|
|
409
|
-
createdAt: row.createdAt,
|
|
410
|
-
updatedAt: row.updatedAt,
|
|
411
|
-
status: row.status as DraftStatus,
|
|
412
|
-
message: row.messageDigest,
|
|
413
|
-
};
|
|
414
|
-
if (row.title !== null) record.title = row.title;
|
|
415
|
-
if (row.channel !== null) record.channel = row.channel;
|
|
416
|
-
if (row.route !== null) record.route = row.route;
|
|
417
|
-
// Webhook is encrypted at rest; reads ALWAYS redact. Presence of webhookEnc
|
|
418
|
-
// means a webhook exists → emit '[redacted]'; the raw URL never leaves here.
|
|
419
|
-
const redacted = redactWebhook(row.webhookEnc);
|
|
420
|
-
if (redacted !== undefined) record.webhook = redacted;
|
|
421
|
-
if (row.link !== null) record.link = row.link;
|
|
422
|
-
const tags = parseTags(row.tags);
|
|
423
|
-
if (tags !== undefined) record.tags = tags;
|
|
424
|
-
if (row.sentResponseId !== null) record.sentResponseId = row.sentResponseId;
|
|
425
|
-
if (row.sendError !== null) record.sendError = row.sendError;
|
|
426
|
-
return record;
|
|
427
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* channels.drafts.* handler surface barrel.
|
|
3
|
-
*
|
|
4
|
-
* Re-exports only the concrete register entrypoint and the store/types other
|
|
5
|
-
* code may need. It imports concrete submodules (no project index barrels), so
|
|
6
|
-
* it introduces no import cycle.
|
|
7
|
-
*/
|
|
8
|
-
export { registerDraftMethods } from './register.ts';
|
|
9
|
-
export type { RegisterDraftsOptions } from './register.ts';
|
|
10
|
-
export { DraftSyncStore, sha256First, redactWebhook } from './draft-store.ts';
|
|
11
|
-
export type {
|
|
12
|
-
DraftListQuery,
|
|
13
|
-
DraftRecord,
|
|
14
|
-
DraftSaveInput,
|
|
15
|
-
DraftSaveResult,
|
|
16
|
-
DraftStatus,
|
|
17
|
-
} from './draft-store.ts';
|