@interactive-inc/claude-funnel 0.7.1 → 0.8.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/README.md +155 -133
- package/dist/bin.js +1344 -0
- package/dist/cli/factory.d.ts +7 -0
- package/dist/cli/router/query-to-cli-args.d.ts +1 -0
- package/dist/cli/router/to-request.d.ts +5 -0
- package/dist/cli/router/validator.d.ts +5 -0
- package/dist/cli/routes/channels.$channel.connectors.$connector.d.ts +42 -0
- package/dist/cli/routes/channels.$channel.connectors.$connector.rename.$newName.d.ts +46 -0
- package/dist/cli/routes/channels.$channel.connectors.$connector.request.d.ts +54 -0
- package/dist/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.d.ts +66 -0
- package/dist/cli/routes/channels.$channel.connectors.$connector.schedules.d.ts +42 -0
- package/dist/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.d.ts +46 -0
- package/dist/cli/routes/channels.$channel.connectors.add.$connector.d.ts +90 -0
- package/dist/cli/routes/channels.$channel.connectors.d.ts +38 -0
- package/dist/cli/routes/channels.$channel.connectors.remove.$connector.d.ts +42 -0
- package/dist/cli/routes/channels.$channel.connectors.set.$connector.d.ts +62 -0
- package/dist/cli/routes/channels.$channel.d.ts +38 -0
- package/dist/cli/routes/channels.$channel.rename.$newName.d.ts +42 -0
- package/dist/cli/routes/channels.$channel.set.delivery.$mode.d.ts +28 -0
- package/dist/cli/routes/channels.add.$channel.d.ts +46 -0
- package/dist/cli/routes/channels.d.ts +16 -0
- package/dist/cli/routes/channels.remove.$channel.d.ts +38 -0
- package/dist/cli/routes/claude.d.ts +32 -0
- package/dist/cli/routes/gateway.d.ts +20 -0
- package/dist/cli/routes/gateway.listeners.d.ts +17 -0
- package/dist/cli/routes/gateway.logs.d.ts +24 -0
- package/dist/cli/routes/gateway.restart.d.ts +24 -0
- package/dist/cli/routes/gateway.run.d.ts +24 -0
- package/dist/cli/routes/gateway.start.d.ts +24 -0
- package/dist/cli/routes/gateway.status.d.ts +13 -0
- package/dist/cli/routes/gateway.stop.d.ts +16 -0
- package/dist/cli/routes/index.d.ts +1222 -0
- package/dist/cli/routes/profiles.$profile.as-default.d.ts +38 -0
- package/dist/cli/routes/profiles.$profile.rename.$newName.d.ts +42 -0
- package/dist/cli/routes/profiles.$profile.run.d.ts +46 -0
- package/dist/cli/routes/profiles.add.$profile.d.ts +54 -0
- package/dist/cli/routes/profiles.d.ts +16 -0
- package/dist/cli/routes/profiles.remove.$profile.d.ts +38 -0
- package/dist/cli/routes/profiles.set.$profile.d.ts +54 -0
- package/dist/cli/routes/status.d.ts +16 -0
- package/dist/cli/routes/update.d.ts +16 -0
- package/dist/connectors/connector-adapter.d.ts +8 -0
- package/dist/connectors/connector-config-schema.d.ts +43 -0
- package/dist/connectors/connector-factory.d.ts +32 -0
- package/dist/connectors/connector-listener.d.ts +17 -0
- package/dist/connectors/discord-adapter.d.ts +14 -0
- package/dist/connectors/discord-connector-schema.d.ts +10 -0
- package/dist/connectors/discord-event-processor.d.ts +26 -0
- package/dist/connectors/discord-listener.d.ts +17 -0
- package/dist/connectors/gh-adapter.d.ts +11 -0
- package/dist/connectors/gh-connector-schema.d.ts +10 -0
- package/dist/connectors/gh-listener.d.ts +26 -0
- package/dist/connectors/match-cron.d.ts +1 -0
- package/dist/connectors/schedule-connector-schema.d.ts +45 -0
- package/dist/connectors/schedule-listener.d.ts +30 -0
- package/dist/connectors/schedule-state-store.d.ts +19 -0
- package/dist/connectors/slack-adapter.d.ts +15 -0
- package/dist/connectors/slack-connector-schema.d.ts +11 -0
- package/dist/connectors/slack-event-processor.d.ts +27 -0
- package/dist/connectors/slack-listener.d.ts +17 -0
- package/dist/engine/channels/channels.d.ts +106 -0
- package/dist/engine/claude/claude.d.ts +49 -0
- package/dist/engine/claude/gateway-controller.d.ts +6 -0
- package/dist/engine/fs/file-system.d.ts +24 -0
- package/dist/engine/fs/memory-file-system.d.ts +31 -0
- package/dist/engine/fs/node-file-system.d.ts +15 -0
- package/dist/engine/http/http-client.d.ts +15 -0
- package/dist/engine/http/memory-http-client.d.ts +12 -0
- package/dist/engine/http/node-http-client.d.ts +5 -0
- package/dist/engine/id/id-generator.d.ts +7 -0
- package/dist/engine/id/memory-id-generator.d.ts +11 -0
- package/dist/engine/id/node-id-generator.d.ts +4 -0
- package/dist/engine/logger/logger.d.ts +11 -0
- package/dist/engine/logger/memory-logger.d.ts +14 -0
- package/dist/engine/logger/node-logger.d.ts +15 -0
- package/dist/engine/logger/noop-logger.d.ts +7 -0
- package/dist/engine/mcp/channel-server.d.ts +1 -0
- package/dist/engine/mcp/mcp.d.ts +22 -0
- package/dist/engine/process/memory-process-runner.d.ts +43 -0
- package/dist/engine/process/node-process-runner.d.ts +9 -0
- package/dist/engine/process/process-runner.d.ts +29 -0
- package/dist/engine/profiles/profile-channel-checker.d.ts +7 -0
- package/dist/engine/profiles/profiles.d.ts +31 -0
- package/dist/engine/settings/mock-settings-reader.d.ts +9 -0
- package/dist/engine/settings/settings-reader.d.ts +5 -0
- package/dist/engine/settings/settings-schema.d.ts +132 -0
- package/dist/engine/settings/settings-store.d.ts +18 -0
- package/dist/engine/time/clock.d.ts +9 -0
- package/dist/engine/time/memory-clock.d.ts +12 -0
- package/dist/engine/time/node-clock.d.ts +4 -0
- package/dist/funnel.d.ts +95 -0
- package/dist/gateway/auth-middleware.d.ts +14 -0
- package/dist/gateway/broadcaster.d.ts +122 -0
- package/dist/gateway/daemon.d.ts +2 -0
- package/dist/gateway/daemon.js +485 -0
- package/dist/gateway/factory.d.ts +7 -0
- package/dist/gateway/funnel-event-store.d.ts +81 -0
- package/dist/gateway/gateway-server.d.ts +94 -0
- package/dist/gateway/gateway-token.d.ts +33 -0
- package/dist/gateway/gateway.d.ts +58 -0
- package/dist/gateway/kill-competing-slack-gateways.d.ts +9 -0
- package/dist/gateway/listener-supervisor.d.ts +85 -0
- package/dist/gateway/listeners-client.d.ts +53 -0
- package/dist/gateway/resolve-daemon-script.d.ts +11 -0
- package/dist/gateway/routes/channels.connectors.call.d.ts +41 -0
- package/dist/gateway/routes/health.d.ts +17 -0
- package/dist/gateway/routes/index.d.ts +209 -0
- package/dist/gateway/routes/listeners.list.d.ts +14 -0
- package/dist/gateway/routes/listeners.restart.d.ts +34 -0
- package/dist/gateway/routes/listeners.start.d.ts +34 -0
- package/dist/gateway/routes/listeners.stop.d.ts +34 -0
- package/dist/gateway/routes/route-deps.d.ts +10 -0
- package/dist/gateway/routes/status.d.ts +30 -0
- package/dist/gateway/routes/validator.d.ts +19 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +3575 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/logger/leuco-human-file-writer.d.ts +33 -0
- package/dist/logger/leuco-human-logger.d.ts +46 -0
- package/dist/logger/leuco-human-record.d.ts +15 -0
- package/dist/logger/leuco-human-stdout-writer.d.ts +20 -0
- package/dist/logger/leuco-human-writer.d.ts +13 -0
- package/dist/logger/leuco-logger-memory-sink.d.ts +33 -0
- package/dist/logger/leuco-logger-record.d.ts +13 -0
- package/dist/logger/leuco-logger-sink.d.ts +34 -0
- package/dist/logger/leuco-logger-sqlite-sink.d.ts +102 -0
- package/dist/logger/leuco-logger.d.ts +56 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/lib/bin.ts +78 -0
- package/lib/{modules → cli}/router/to-request.ts +13 -20
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
- package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
- package/lib/cli/routes/channels.$channel.ts +34 -0
- package/lib/cli/routes/channels.add.$channel.ts +33 -0
- package/lib/cli/routes/channels.remove.$channel.ts +20 -0
- package/lib/cli/routes/channels.ts +39 -0
- package/lib/cli/routes/claude.ts +69 -0
- package/lib/cli/routes/gateway.listeners.ts +41 -0
- package/lib/cli/routes/gateway.logs.ts +123 -0
- package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
- package/lib/cli/routes/gateway.run.ts +41 -0
- package/lib/cli/routes/gateway.start.ts +50 -0
- package/lib/cli/routes/gateway.status.ts +19 -0
- package/lib/cli/routes/gateway.stop.ts +32 -0
- package/lib/cli/routes/gateway.ts +55 -0
- package/lib/cli/routes/index.ts +202 -0
- package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
- package/lib/cli/routes/profiles.$profile.run.ts +36 -0
- package/lib/cli/routes/profiles.add.$profile.ts +46 -0
- package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
- package/lib/cli/routes/profiles.set.$profile.ts +46 -0
- package/lib/cli/routes/profiles.ts +40 -0
- package/lib/cli/routes/status.ts +93 -0
- package/lib/cli/routes/update.ts +27 -0
- package/lib/connectors/connector-config-schema.ts +16 -0
- package/lib/connectors/connector-factory.ts +94 -0
- package/lib/connectors/connector-listener.ts +20 -0
- package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
- package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
- package/lib/connectors/discord-listener.ts +111 -0
- package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
- package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +45 -19
- package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
- package/lib/connectors/schedule-connector-schema.ts +33 -0
- package/lib/connectors/schedule-listener.ts +207 -0
- package/lib/connectors/schedule-state-store.ts +54 -0
- package/lib/connectors/slack-adapter.ts +36 -0
- package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
- package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +33 -14
- package/lib/engine/channels/channels.ts +520 -0
- package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +28 -55
- package/lib/engine/claude/gateway-controller.ts +4 -0
- package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +4 -0
- package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
- package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
- package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
- package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
- package/lib/{modules/id/memory-funnel-id-generator.ts → engine/id/memory-id-generator.ts} +1 -1
- package/lib/{modules/id/node-funnel-id-generator.ts → engine/id/node-id-generator.ts} +1 -1
- package/lib/{modules/logger/memory-funnel-logger.ts → engine/logger/memory-logger.ts} +1 -1
- package/lib/{modules/logger/node-funnel-logger.ts → engine/logger/node-logger.ts} +1 -1
- package/lib/{modules/logger/noop-funnel-logger.ts → engine/logger/noop-logger.ts} +1 -1
- package/lib/engine/mcp/channel-server.ts +204 -0
- package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +24 -10
- package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
- package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
- package/lib/engine/profiles/profile-channel-checker.ts +7 -0
- package/lib/{modules/profiles/funnel-profiles.ts → engine/profiles/profiles.ts} +41 -43
- package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
- package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
- package/lib/engine/settings/settings-schema.ts +46 -0
- package/lib/engine/settings/settings-store.ts +110 -0
- package/lib/{modules/time/memory-funnel-clock.ts → engine/time/memory-clock.ts} +1 -1
- package/lib/{modules/time/node-funnel-clock.ts → engine/time/node-clock.ts} +1 -1
- package/lib/funnel.ts +83 -78
- package/lib/gateway/auth-middleware.ts +44 -0
- package/lib/gateway/broadcaster.ts +319 -0
- package/lib/gateway/daemon.ts +47 -0
- package/lib/gateway/factory.ts +10 -0
- package/lib/gateway/funnel-event-store.ts +155 -0
- package/lib/gateway/gateway-server.ts +414 -0
- package/lib/gateway/gateway-token.ts +79 -0
- package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +27 -13
- package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +4 -4
- package/lib/gateway/listener-supervisor.ts +339 -0
- package/lib/gateway/listeners-client.ts +128 -0
- package/lib/gateway/resolve-daemon-script.ts +26 -0
- package/lib/gateway/routes/channels.connectors.call.ts +39 -0
- package/lib/gateway/routes/health.ts +13 -0
- package/lib/gateway/routes/index.ts +24 -0
- package/lib/gateway/routes/listeners.list.ts +6 -0
- package/lib/gateway/routes/listeners.restart.ts +15 -0
- package/lib/gateway/routes/listeners.start.ts +15 -0
- package/lib/gateway/routes/listeners.stop.ts +15 -0
- package/lib/gateway/routes/route-deps.ts +11 -0
- package/lib/gateway/routes/status.ts +15 -0
- package/lib/gateway/routes/validator.ts +17 -0
- package/lib/index.ts +52 -92
- package/lib/logger/leuco-human-file-writer.ts +65 -0
- package/lib/logger/leuco-human-logger.ts +98 -0
- package/lib/logger/leuco-human-record.ts +16 -0
- package/lib/logger/leuco-human-stdout-writer.ts +26 -0
- package/lib/logger/leuco-human-writer.ts +14 -0
- package/lib/logger/leuco-logger-memory-sink.ts +67 -0
- package/lib/logger/leuco-logger-record.ts +13 -0
- package/lib/logger/leuco-logger-sink.ts +33 -0
- package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
- package/lib/logger/leuco-logger.ts +135 -0
- package/lib/tui/app.tsx +357 -0
- package/lib/tui/components/add-row.tsx +18 -0
- package/lib/tui/components/brand.tsx +27 -0
- package/lib/tui/components/card.tsx +44 -0
- package/lib/tui/components/detail-bar.tsx +46 -0
- package/lib/tui/components/editable-field.tsx +33 -0
- package/lib/tui/components/empty-state.tsx +11 -0
- package/lib/tui/components/gateway-status.tsx +66 -0
- package/lib/tui/components/keymap.tsx +29 -0
- package/lib/tui/components/menu-item.tsx +73 -0
- package/lib/tui/components/menu.tsx +26 -0
- package/lib/tui/components/panel-header.tsx +22 -0
- package/lib/tui/components/readonly-field.tsx +18 -0
- package/lib/tui/components/section-header.tsx +25 -0
- package/lib/tui/components/selection-accent.tsx +32 -0
- package/lib/tui/components/session-item.tsx +33 -0
- package/lib/tui/components/session-list.tsx +33 -0
- package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
- package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
- package/lib/tui/components/ui/hascii/badge.tsx +51 -0
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
- package/lib/tui/components/ui/hascii/button.tsx +194 -0
- package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/card.tsx +27 -0
- package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
- package/lib/tui/components/ui/hascii/command.tsx +159 -0
- package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
- package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
- package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
- package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
- package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
- package/lib/tui/components/ui/hascii/input.tsx +130 -0
- package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
- package/lib/tui/components/ui/hascii/progress.tsx +28 -0
- package/lib/tui/components/ui/hascii/select.tsx +131 -0
- package/lib/tui/components/ui/hascii/separator.tsx +35 -0
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
- package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
- package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
- package/lib/tui/components/ui/hascii/slider.tsx +91 -0
- package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
- package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
- package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
- package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
- package/lib/tui/components/ui/hascii/switch.tsx +66 -0
- package/lib/tui/components/ui/hascii/table.tsx +95 -0
- package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
- package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
- package/lib/tui/components/ui/hascii/tree.tsx +104 -0
- package/lib/tui/components/view-shell.tsx +44 -0
- package/lib/tui/filter-input.tsx +33 -0
- package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
- package/lib/tui/parse-comma-list.ts +14 -0
- package/lib/tui/profile-launcher.tsx +61 -0
- package/lib/tui/scrollbar-options.ts +19 -0
- package/lib/tui/sidebar.tsx +50 -0
- package/lib/tui/theme.ts +40 -0
- package/lib/tui/tui.tsx +20 -0
- package/lib/tui/types.ts +38 -0
- package/lib/tui/unique-name.ts +18 -0
- package/lib/tui/use-event-stream.ts +133 -0
- package/lib/tui/use-snapshot.ts +99 -0
- package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
- package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
- package/lib/tui/utils/hascii/theme-context.tsx +26 -0
- package/lib/tui/utils/hascii/theme.ts +176 -0
- package/lib/tui/views/channels-view.tsx +108 -0
- package/lib/tui/views/connectors-view.tsx +164 -0
- package/lib/tui/views/events-view.tsx +160 -0
- package/lib/tui/views/listeners-view.tsx +80 -0
- package/lib/tui/views/profiles-view.tsx +152 -0
- package/package.json +55 -44
- package/lib/api.ts +0 -54
- package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
- package/lib/modules/channels/funnel-channels.ts +0 -160
- package/lib/modules/connectors/connector-config-schema.ts +0 -16
- package/lib/modules/connectors/connector-existence-checker.ts +0 -3
- package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
- package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
- package/lib/modules/connectors/funnel-connector-stores.ts +0 -52
- package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
- package/lib/modules/connectors/funnel-connectors.ts +0 -151
- package/lib/modules/connectors/funnel-discord-listener.ts +0 -71
- package/lib/modules/connectors/funnel-discord-store.ts +0 -88
- package/lib/modules/connectors/funnel-gh-store.ts +0 -101
- package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
- package/lib/modules/connectors/funnel-schedule-listener.ts +0 -130
- package/lib/modules/connectors/funnel-schedule-store.ts +0 -195
- package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
- package/lib/modules/connectors/funnel-slack-store.ts +0 -90
- package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -81
- package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
- package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
- package/lib/modules/gateway/daemon.ts +0 -74
- package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
- package/lib/modules/gateway/funnel-event-logger.ts +0 -59
- package/lib/modules/gateway/funnel-gateway-server.ts +0 -241
- package/lib/modules/mcp/channel-server.ts +0 -76
- package/lib/modules/profiles/profile-channel-checker.ts +0 -3
- package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
- package/lib/modules/repos/funnel-repositories.ts +0 -112
- package/lib/modules/schedule/funnel-schedule.ts +0 -39
- package/lib/modules/settings/funnel-settings-store.ts +0 -56
- package/lib/modules/settings/settings-schema.ts +0 -33
- package/lib/modules/tui/app.tsx +0 -44
- package/lib/modules/tui/tui.tsx +0 -13
- package/lib/routes/channels/add.help.ts +0 -3
- package/lib/routes/channels/add.ts +0 -21
- package/lib/routes/channels/connectors-attach.help.ts +0 -3
- package/lib/routes/channels/connectors-attach.ts +0 -17
- package/lib/routes/channels/connectors-detach.help.ts +0 -3
- package/lib/routes/channels/connectors-detach.ts +0 -17
- package/lib/routes/channels/group.help.ts +0 -16
- package/lib/routes/channels/group.ts +0 -22
- package/lib/routes/channels/remove.help.ts +0 -3
- package/lib/routes/channels/remove.ts +0 -17
- package/lib/routes/channels/rename.help.ts +0 -5
- package/lib/routes/channels/rename.ts +0 -17
- package/lib/routes/channels/routes.ts +0 -19
- package/lib/routes/channels/show.help.ts +0 -1
- package/lib/routes/channels/show.ts +0 -26
- package/lib/routes/claude/claude.help.ts +0 -16
- package/lib/routes/claude/claude.ts +0 -76
- package/lib/routes/claude/routes.ts +0 -4
- package/lib/routes/connectors/add.help.ts +0 -28
- package/lib/routes/connectors/add.ts +0 -64
- package/lib/routes/connectors/group.help.ts +0 -14
- package/lib/routes/connectors/group.ts +0 -18
- package/lib/routes/connectors/remove.help.ts +0 -3
- package/lib/routes/connectors/remove.ts +0 -17
- package/lib/routes/connectors/rename.help.ts +0 -5
- package/lib/routes/connectors/rename.ts +0 -17
- package/lib/routes/connectors/routes.ts +0 -23
- package/lib/routes/connectors/schedules-add.help.ts +0 -11
- package/lib/routes/connectors/schedules-add.ts +0 -33
- package/lib/routes/connectors/schedules-group.help.ts +0 -1
- package/lib/routes/connectors/schedules-group.ts +0 -38
- package/lib/routes/connectors/schedules-remove.help.ts +0 -3
- package/lib/routes/connectors/schedules-remove.ts +0 -17
- package/lib/routes/connectors/set.help.ts +0 -8
- package/lib/routes/connectors/set.ts +0 -72
- package/lib/routes/connectors/show.help.ts +0 -1
- package/lib/routes/connectors/show.ts +0 -41
- package/lib/routes/gateway/group.help.ts +0 -15
- package/lib/routes/gateway/group.ts +0 -28
- package/lib/routes/gateway/logs.help.ts +0 -13
- package/lib/routes/gateway/logs.ts +0 -102
- package/lib/routes/gateway/restart.help.ts +0 -10
- package/lib/routes/gateway/routes.ts +0 -18
- package/lib/routes/gateway/run.help.ts +0 -12
- package/lib/routes/gateway/run.ts +0 -35
- package/lib/routes/gateway/start.help.ts +0 -15
- package/lib/routes/gateway/start.ts +0 -32
- package/lib/routes/gateway/status.help.ts +0 -9
- package/lib/routes/gateway/status.ts +0 -28
- package/lib/routes/gateway/stop.help.ts +0 -8
- package/lib/routes/gateway/stop.ts +0 -21
- package/lib/routes/profiles/add.help.ts +0 -3
- package/lib/routes/profiles/add.ts +0 -33
- package/lib/routes/profiles/group.help.ts +0 -16
- package/lib/routes/profiles/group.ts +0 -25
- package/lib/routes/profiles/launch.help.ts +0 -4
- package/lib/routes/profiles/launch.ts +0 -36
- package/lib/routes/profiles/remove.help.ts +0 -3
- package/lib/routes/profiles/remove.ts +0 -17
- package/lib/routes/profiles/rename.help.ts +0 -5
- package/lib/routes/profiles/rename.ts +0 -17
- package/lib/routes/profiles/routes.ts +0 -18
- package/lib/routes/profiles/set.help.ts +0 -5
- package/lib/routes/profiles/set.ts +0 -32
- package/lib/routes/repos/add.help.ts +0 -6
- package/lib/routes/repos/add.ts +0 -20
- package/lib/routes/repos/group.help.ts +0 -11
- package/lib/routes/repos/group.ts +0 -18
- package/lib/routes/repos/remove.help.ts +0 -3
- package/lib/routes/repos/remove.ts +0 -17
- package/lib/routes/repos/rename.help.ts +0 -5
- package/lib/routes/repos/rename.ts +0 -17
- package/lib/routes/repos/routes.ts +0 -17
- package/lib/routes/repos/set.help.ts +0 -5
- package/lib/routes/repos/set.ts +0 -21
- package/lib/routes/repos/show.help.ts +0 -1
- package/lib/routes/repos/show.ts +0 -19
- package/lib/routes/request/discord-help.ts +0 -9
- package/lib/routes/request/discord.help.ts +0 -19
- package/lib/routes/request/discord.ts +0 -65
- package/lib/routes/request/group.help.ts +0 -15
- package/lib/routes/request/group.ts +0 -9
- package/lib/routes/request/routes.ts +0 -14
- package/lib/routes/request/slack-help.ts +0 -9
- package/lib/routes/request/slack.help.ts +0 -19
- package/lib/routes/request/slack.ts +0 -61
- package/lib/routes/status/routes.ts +0 -4
- package/lib/routes/status/status.help.ts +0 -6
- package/lib/routes/status/status.ts +0 -77
- package/lib/routes/update/routes.ts +0 -4
- package/lib/routes/update/update.help.ts +0 -5
- package/lib/routes/update/update.ts +0 -21
- package/lib/routes.ts +0 -40
- /package/lib/{factory.ts → cli/factory.ts} +0 -0
- /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
- /package/lib/{modules → cli}/router/validator.ts +0 -0
- /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
- /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
- /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
- /package/lib/{modules/id/funnel-id-generator.ts → engine/id/id-generator.ts} +0 -0
- /package/lib/{modules/logger/funnel-logger.ts → engine/logger/logger.ts} +0 -0
- /package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +0 -0
- /package/lib/{modules/time/funnel-clock.ts → engine/time/clock.ts} +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import type { Server, ServerWebSocket } from "bun"
|
|
4
|
+
import type { Hono } from "hono"
|
|
5
|
+
import type { FunnelChannels } from "@/engine/channels/channels"
|
|
6
|
+
import { constantTimeEqual, requireBearerToken } from "@/gateway/auth-middleware"
|
|
7
|
+
import { type Env, factory } from "@/gateway/factory"
|
|
8
|
+
import { FunnelBroadcaster } from "@/gateway/broadcaster"
|
|
9
|
+
import { FunnelEventStore } from "@/gateway/funnel-event-store"
|
|
10
|
+
import { FunnelListenerSupervisor } from "@/gateway/listener-supervisor"
|
|
11
|
+
import { killCompetingSlackGateways } from "@/gateway/kill-competing-slack-gateways"
|
|
12
|
+
import { gatewayRoutes } from "@/gateway/routes"
|
|
13
|
+
import { FunnelLogger } from "@/engine/logger/logger"
|
|
14
|
+
import { NodeFunnelLogger } from "@/engine/logger/node-logger"
|
|
15
|
+
import type { FunnelProcessRunner } from "@/engine/process/process-runner"
|
|
16
|
+
import type { FunnelSettingsReader } from "@/engine/settings/settings-reader"
|
|
17
|
+
import type { FunnelClock } from "@/engine/time/clock"
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PORT = 9742
|
|
20
|
+
const DEFAULT_LOG_DIR = "/tmp/funnel/events"
|
|
21
|
+
const DB_FILENAME = "events.db"
|
|
22
|
+
|
|
23
|
+
type Deps = {
|
|
24
|
+
channels: FunnelChannels
|
|
25
|
+
settings: FunnelSettingsReader
|
|
26
|
+
port?: number
|
|
27
|
+
/** Directory holding the SQLite event store. The DB file lives at `<logDir>/events.db`. */
|
|
28
|
+
logDir?: string
|
|
29
|
+
process?: FunnelProcessRunner
|
|
30
|
+
clock?: FunnelClock
|
|
31
|
+
logger?: FunnelLogger
|
|
32
|
+
selfPid?: number
|
|
33
|
+
killCompetingSlack?: boolean
|
|
34
|
+
/** Bearer token required for `/listeners*`, `/status`, and `/ws`. Empty string disables auth (tests only). */
|
|
35
|
+
token?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type WsData = {
|
|
39
|
+
/** Stable channel id (uuid) the client subscribed to. "" for tap-all clients. */
|
|
40
|
+
channel: string
|
|
41
|
+
/** Resolved channel name (for log readability). null for tap-all or unknown. */
|
|
42
|
+
channelName: string | null
|
|
43
|
+
/** Connector names belonging to that channel; used by tap-all replay filtering. */
|
|
44
|
+
connectors: string[]
|
|
45
|
+
tapAll?: boolean
|
|
46
|
+
/** Routing mode for this channel; resolved at upgrade time from settings. */
|
|
47
|
+
delivery: "fanout" | "exclusive"
|
|
48
|
+
/** Replay any events with offset strictly greater than this on open, then resume the live stream. */
|
|
49
|
+
since?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const defaultLogger = new NodeFunnelLogger()
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* In-process gateway: runs `Bun.serve` (HTTP + WebSocket /ws), boots connector
|
|
56
|
+
* listeners through `FunnelListenerSupervisor`, fans events out via
|
|
57
|
+
* `FunnelBroadcaster`, and persists them via `FunnelEventStore` (SQLite).
|
|
58
|
+
* System events (gateway lifecycle, connect/disconnect) flow to `FunnelLogger`
|
|
59
|
+
* instead — keeping the SQLite seq space exclusive to broadcaster traffic so
|
|
60
|
+
* the broadcaster's offset counter and `getMaxSeq()` stay aligned without
|
|
61
|
+
* per-event coordination. Exposes `/listeners` HTTP for runtime
|
|
62
|
+
* start/stop/restart of individual connectors.
|
|
63
|
+
*/
|
|
64
|
+
export class FunnelGatewayServer {
|
|
65
|
+
private readonly channels: FunnelChannels
|
|
66
|
+
private readonly settings: FunnelSettingsReader
|
|
67
|
+
private readonly port: number
|
|
68
|
+
private readonly logDir: string
|
|
69
|
+
private readonly process?: FunnelProcessRunner
|
|
70
|
+
private readonly logger: FunnelLogger
|
|
71
|
+
private readonly selfPid: number
|
|
72
|
+
private readonly killCompetingSlack: boolean
|
|
73
|
+
private readonly token: string
|
|
74
|
+
private readonly broadcaster: FunnelBroadcaster
|
|
75
|
+
private readonly eventStore: FunnelEventStore
|
|
76
|
+
private readonly supervisor: FunnelListenerSupervisor
|
|
77
|
+
private readonly nowMs: () => number
|
|
78
|
+
private startedAt: number | null = null
|
|
79
|
+
private server: Server<WsData> | null = null
|
|
80
|
+
|
|
81
|
+
constructor(deps: Deps) {
|
|
82
|
+
this.channels = deps.channels
|
|
83
|
+
this.settings = deps.settings
|
|
84
|
+
this.port = deps.port ?? DEFAULT_PORT
|
|
85
|
+
this.logDir = deps.logDir ?? DEFAULT_LOG_DIR
|
|
86
|
+
this.process = deps.process
|
|
87
|
+
this.logger = deps.logger ?? defaultLogger
|
|
88
|
+
this.selfPid = deps.selfPid ?? globalThis.process.pid
|
|
89
|
+
this.killCompetingSlack = deps.killCompetingSlack ?? true
|
|
90
|
+
this.token = deps.token ?? ""
|
|
91
|
+
const clock = deps.clock
|
|
92
|
+
this.nowMs = clock ? () => clock.millis() : () => Date.now()
|
|
93
|
+
if (!existsSync(this.logDir)) mkdirSync(this.logDir, { recursive: true })
|
|
94
|
+
this.eventStore = new FunnelEventStore({
|
|
95
|
+
path: join(this.logDir, DB_FILENAME),
|
|
96
|
+
now: this.nowMs,
|
|
97
|
+
})
|
|
98
|
+
this.broadcaster = new FunnelBroadcaster({
|
|
99
|
+
logger: this.logger,
|
|
100
|
+
now: this.nowMs,
|
|
101
|
+
persistentReplay: this.eventStore,
|
|
102
|
+
})
|
|
103
|
+
this.broadcaster.seedLatestOffset(this.eventStore.findMaxOffset())
|
|
104
|
+
this.supervisor = new FunnelListenerSupervisor({
|
|
105
|
+
channels: this.channels,
|
|
106
|
+
logger: this.logger,
|
|
107
|
+
notify: (channelName, connectorName, content, meta) =>
|
|
108
|
+
this.notify(channelName, connectorName, content, meta),
|
|
109
|
+
now: this.nowMs,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async start(): Promise<Server<WsData>> {
|
|
114
|
+
if (this.server) return this.server
|
|
115
|
+
|
|
116
|
+
const app = this.buildApp()
|
|
117
|
+
|
|
118
|
+
this.startedAt = this.nowMs()
|
|
119
|
+
this.server = Bun.serve<WsData>({
|
|
120
|
+
port: this.port,
|
|
121
|
+
development: false,
|
|
122
|
+
fetch: (request, server) => this.handleFetch(request, server, app),
|
|
123
|
+
websocket: {
|
|
124
|
+
open: (ws) => this.handleWsOpen(ws),
|
|
125
|
+
close: (ws) => this.handleWsClose(ws),
|
|
126
|
+
message() {
|
|
127
|
+
// required by Bun's websocket interface; no client → gateway messages today
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
this.logServerStarted()
|
|
133
|
+
await this.bootListeners()
|
|
134
|
+
|
|
135
|
+
return this.server
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async stop(): Promise<void> {
|
|
139
|
+
await this.supervisor.stopAll()
|
|
140
|
+
|
|
141
|
+
if (this.server) {
|
|
142
|
+
this.server.stop()
|
|
143
|
+
this.server = null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getStatus(): { clients: number; channels: { channel: string; connectors: string[] }[] } {
|
|
148
|
+
return {
|
|
149
|
+
clients: this.broadcaster.getClientCount(),
|
|
150
|
+
channels: this.broadcaster.listChannels(),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getBroadcaster(): FunnelBroadcaster {
|
|
155
|
+
return this.broadcaster
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getSupervisor(): FunnelListenerSupervisor {
|
|
159
|
+
return this.supervisor
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getEventStore(): FunnelEventStore {
|
|
163
|
+
return this.eventStore
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private handleFetch(
|
|
167
|
+
request: Request,
|
|
168
|
+
server: Server<WsData>,
|
|
169
|
+
app: Hono<Env>,
|
|
170
|
+
): Response | Promise<Response> | undefined {
|
|
171
|
+
const url = new URL(request.url)
|
|
172
|
+
|
|
173
|
+
if (url.pathname === "/ws" && request.headers.get("upgrade") === "websocket") {
|
|
174
|
+
if (this.token && !this.tokenMatchesUpgrade(request)) {
|
|
175
|
+
return new Response("unauthorized", { status: 401 })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const tapAll = url.searchParams.get("tap") === "all"
|
|
179
|
+
const requestedChannel = tapAll ? "" : (url.searchParams.get("channel") ?? "")
|
|
180
|
+
const channel = !tapAll && requestedChannel ? this.resolveChannel(requestedChannel) : null
|
|
181
|
+
const channelId = tapAll ? "" : (channel?.id ?? requestedChannel)
|
|
182
|
+
const channelName = tapAll ? null : (channel?.name ?? null)
|
|
183
|
+
const connectors = channel?.connectors ?? []
|
|
184
|
+
const delivery = channel?.delivery ?? "fanout"
|
|
185
|
+
const sinceRaw = url.searchParams.get("since")
|
|
186
|
+
const sinceParsed = sinceRaw === null ? Number.NaN : Number.parseInt(sinceRaw, 10)
|
|
187
|
+
const since = Number.isFinite(sinceParsed) && sinceParsed >= 0 ? sinceParsed : undefined
|
|
188
|
+
const upgraded = server.upgrade(request, {
|
|
189
|
+
data: {
|
|
190
|
+
channel: channelId,
|
|
191
|
+
channelName,
|
|
192
|
+
connectors,
|
|
193
|
+
tapAll,
|
|
194
|
+
delivery,
|
|
195
|
+
since,
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
if (upgraded) return undefined
|
|
200
|
+
|
|
201
|
+
return new Response("WebSocket upgrade failed", { status: 400 })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return app.fetch(request)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private handleWsOpen(ws: ServerWebSocket<WsData>): void {
|
|
208
|
+
if (typeof ws.data.since === "number") {
|
|
209
|
+
const replay = this.broadcaster.replaySince(ws.data.since, ws.data)
|
|
210
|
+
|
|
211
|
+
for (const event of replay) ws.send(JSON.stringify(event))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.broadcaster.addClient(ws, ws.data)
|
|
215
|
+
|
|
216
|
+
if (ws.data.channelName) {
|
|
217
|
+
const meta: Record<string, string> = {
|
|
218
|
+
event_type: "system",
|
|
219
|
+
action: "channel_connect",
|
|
220
|
+
channel: ws.data.channelName,
|
|
221
|
+
channelId: ws.data.channel,
|
|
222
|
+
connectors: ws.data.connectors.join(","),
|
|
223
|
+
total: String(this.broadcaster.getClientCount()),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.logger.info("channel connected", meta)
|
|
227
|
+
} else {
|
|
228
|
+
this.logger.info("tap-all client connected", {
|
|
229
|
+
event_type: "system",
|
|
230
|
+
action: "tap_connect",
|
|
231
|
+
total: String(this.broadcaster.getClientCount()),
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private handleWsClose(ws: ServerWebSocket<WsData>): void {
|
|
237
|
+
this.broadcaster.removeClient(ws)
|
|
238
|
+
|
|
239
|
+
if (ws.data.channelName) {
|
|
240
|
+
this.logger.info("channel disconnected", {
|
|
241
|
+
event_type: "system",
|
|
242
|
+
action: "channel_disconnect",
|
|
243
|
+
channel: ws.data.channelName,
|
|
244
|
+
channelId: ws.data.channel,
|
|
245
|
+
total: String(this.broadcaster.getClientCount()),
|
|
246
|
+
})
|
|
247
|
+
} else {
|
|
248
|
+
this.logger.info("tap-all client disconnected", {
|
|
249
|
+
event_type: "system",
|
|
250
|
+
action: "tap_disconnect",
|
|
251
|
+
total: String(this.broadcaster.getClientCount()),
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private logServerStarted(): void {
|
|
257
|
+
this.logger.info("gateway started", {
|
|
258
|
+
event_type: "system",
|
|
259
|
+
action: "gateway_start",
|
|
260
|
+
port: String(this.port),
|
|
261
|
+
pid: String(this.selfPid),
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
this.logger.info("funnel gateway listening", {
|
|
265
|
+
url: `http://localhost:${this.port}`,
|
|
266
|
+
websocket: `ws://localhost:${this.port}/ws`,
|
|
267
|
+
health: `http://localhost:${this.port}/health`,
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private buildApp(): Hono<Env> {
|
|
272
|
+
const base = factory.createApp()
|
|
273
|
+
|
|
274
|
+
base.use((c, next) => {
|
|
275
|
+
c.set("deps", {
|
|
276
|
+
selfPid: this.selfPid,
|
|
277
|
+
broadcaster: this.broadcaster,
|
|
278
|
+
supervisor: this.supervisor,
|
|
279
|
+
channels: this.channels,
|
|
280
|
+
uptimeMs: () => (this.startedAt ? this.nowMs() - this.startedAt : 0),
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return next()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
if (this.token) {
|
|
287
|
+
base.use("/listeners/*", requireBearerToken({ expected: this.token }))
|
|
288
|
+
base.use("/status", requireBearerToken({ expected: this.token }))
|
|
289
|
+
base.use("/channels/*", requireBearerToken({ expected: this.token }))
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return base.route("/", gatewayRoutes)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Reads the bearer token from the WebSocket upgrade request. Accepts:
|
|
297
|
+
* - `Sec-WebSocket-Protocol: funnel.token.<value>` (preferred — header, never logged in URLs)
|
|
298
|
+
* - `Authorization: Bearer <value>` (also header-based)
|
|
299
|
+
* Returns true on a constant-time match against the daemon token.
|
|
300
|
+
*/
|
|
301
|
+
private tokenMatchesUpgrade(request: Request): boolean {
|
|
302
|
+
const protocols = (request.headers.get("sec-websocket-protocol") ?? "")
|
|
303
|
+
.split(",")
|
|
304
|
+
.map((p) => p.trim())
|
|
305
|
+
.filter((p) => p.length > 0)
|
|
306
|
+
|
|
307
|
+
for (const proto of protocols) {
|
|
308
|
+
if (
|
|
309
|
+
proto.startsWith("funnel.token.") &&
|
|
310
|
+
constantTimeEqual(proto.slice("funnel.token.".length), this.token)
|
|
311
|
+
) {
|
|
312
|
+
return true
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const auth = request.headers.get("authorization") ?? ""
|
|
317
|
+
const match = auth.match(/^Bearer\s+(.+)$/i)
|
|
318
|
+
|
|
319
|
+
if (match && constantTimeEqual(match[1] ?? "", this.token)) return true
|
|
320
|
+
|
|
321
|
+
return false
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private resolveChannel(
|
|
325
|
+
requested: string,
|
|
326
|
+
): { id: string; name: string; connectors: string[]; delivery: "fanout" | "exclusive" } | null {
|
|
327
|
+
const settings = this.settings.read()
|
|
328
|
+
const channel = settings?.channels.find((c) => c.id === requested || c.name === requested)
|
|
329
|
+
|
|
330
|
+
if (!channel) return null
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
id: channel.id,
|
|
334
|
+
name: channel.name,
|
|
335
|
+
connectors: channel.connectors.map((c) => c.name),
|
|
336
|
+
delivery: channel.delivery,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private async bootListeners(): Promise<void> {
|
|
341
|
+
const allConnectors = this.channels.listAllConnectors()
|
|
342
|
+
|
|
343
|
+
if (this.killCompetingSlack && allConnectors.some((c) => c.type === "slack")) {
|
|
344
|
+
const killed = await killCompetingSlackGateways({
|
|
345
|
+
selfPid: this.selfPid,
|
|
346
|
+
process: this.process,
|
|
347
|
+
logger: this.logger,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
if (killed.length > 0) {
|
|
351
|
+
this.logger.info("killed competing Slack gateway processes", {
|
|
352
|
+
event_type: "system",
|
|
353
|
+
action: "kill_competing",
|
|
354
|
+
pids: killed.join(","),
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await this.supervisor.startAll()
|
|
360
|
+
|
|
361
|
+
for (const entry of this.supervisor.list()) {
|
|
362
|
+
this.logger.info(`${entry.type} listener started: ${entry.name}`, {
|
|
363
|
+
event_type: "system",
|
|
364
|
+
action: `${entry.type}_connect`,
|
|
365
|
+
channel: entry.channelName,
|
|
366
|
+
connector: entry.name,
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this.logger.info(`event store: ${join(this.logDir, DB_FILENAME)}`)
|
|
371
|
+
this.logger.info("funnel gateway running")
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private async notify(
|
|
375
|
+
channelName: string,
|
|
376
|
+
connectorName: string,
|
|
377
|
+
content: string,
|
|
378
|
+
meta?: Record<string, string>,
|
|
379
|
+
): Promise<void> {
|
|
380
|
+
const channelId = this.lookupChannelId(channelName)
|
|
381
|
+
const connectorId = channelId ? this.lookupConnectorId(channelId, connectorName) : null
|
|
382
|
+
const enriched: Record<string, string> = {
|
|
383
|
+
...meta,
|
|
384
|
+
channel: channelName,
|
|
385
|
+
connector: connectorName,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (channelId) enriched.channelId = channelId
|
|
389
|
+
if (connectorId) enriched.connectorId = connectorId
|
|
390
|
+
|
|
391
|
+
const event = this.broadcaster.broadcast(content, enriched)
|
|
392
|
+
|
|
393
|
+
this.eventStore.record({
|
|
394
|
+
content,
|
|
395
|
+
channelId: channelId ?? null,
|
|
396
|
+
connectorId: connectorId ?? null,
|
|
397
|
+
meta: enriched,
|
|
398
|
+
offset: event.offset,
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private lookupChannelId(channelName: string): string | null {
|
|
403
|
+
const channel = this.settings.read().channels.find((c) => c.name === channelName)
|
|
404
|
+
|
|
405
|
+
return channel?.id ?? null
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private lookupConnectorId(channelId: string, connectorName: string): string | null {
|
|
409
|
+
const channel = this.settings.read().channels.find((c) => c.id === channelId)
|
|
410
|
+
const connector = channel?.connectors.find((c) => c.name === connectorName)
|
|
411
|
+
|
|
412
|
+
return connector?.id ?? null
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { homedir } from "node:os"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { FunnelFileSystem } from "@/engine/fs/file-system"
|
|
4
|
+
import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
|
|
5
|
+
import { FUNNEL_DIR } from "@/engine/settings/settings-store"
|
|
6
|
+
|
|
7
|
+
const TOKEN_FILE_NAME = "gateway.token"
|
|
8
|
+
const TOKEN_BYTES = 32
|
|
9
|
+
|
|
10
|
+
type Deps = {
|
|
11
|
+
fs?: FunnelFileSystem
|
|
12
|
+
dir?: string
|
|
13
|
+
generate?: () => string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const defaultFs = new NodeFunnelFileSystem()
|
|
17
|
+
|
|
18
|
+
const defaultGenerate = (): string => {
|
|
19
|
+
const buf = new Uint8Array(TOKEN_BYTES)
|
|
20
|
+
crypto.getRandomValues(buf)
|
|
21
|
+
|
|
22
|
+
return [...buf].map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reads / generates the gateway daemon token used to authenticate
|
|
27
|
+
* `/listeners*`, `/status`, and `/ws` connections.
|
|
28
|
+
*
|
|
29
|
+
* Token file: `<dir>/gateway.token` (default `~/.funnel/gateway.token`),
|
|
30
|
+
* written with mode 0600. Clients on the same machine as the daemon read
|
|
31
|
+
* the file directly; the token never leaves the user's home directory.
|
|
32
|
+
*/
|
|
33
|
+
export class FunnelGatewayToken {
|
|
34
|
+
private readonly fs: FunnelFileSystem
|
|
35
|
+
private readonly path: string
|
|
36
|
+
private readonly generate: () => string
|
|
37
|
+
|
|
38
|
+
constructor(deps: Deps = {}) {
|
|
39
|
+
this.fs = deps.fs ?? defaultFs
|
|
40
|
+
this.path = join(deps.dir ?? FUNNEL_DIR, TOKEN_FILE_NAME)
|
|
41
|
+
this.generate = deps.generate ?? defaultGenerate
|
|
42
|
+
Object.freeze(this)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
read(): string | null {
|
|
46
|
+
if (!this.fs.existsSync(this.path)) return null
|
|
47
|
+
|
|
48
|
+
const value = this.fs.readFileSync(this.path).trim()
|
|
49
|
+
|
|
50
|
+
return value.length > 0 ? value : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the existing token or, if missing, generates one and writes it with mode 0600.
|
|
55
|
+
*
|
|
56
|
+
* NOTE: not atomic — two concurrent `ensure()` calls (e.g., `fnl gateway start` racing
|
|
57
|
+
* itself before the PID lock is acquired) could each generate independent tokens. The
|
|
58
|
+
* gateway PID file makes this practically a non-issue; if you need stronger guarantees,
|
|
59
|
+
* take a file lock around this call externally.
|
|
60
|
+
*/
|
|
61
|
+
ensure(): string {
|
|
62
|
+
const existing = this.read()
|
|
63
|
+
|
|
64
|
+
if (existing) return existing
|
|
65
|
+
|
|
66
|
+
const token = this.generate()
|
|
67
|
+
|
|
68
|
+
this.fs.mkdirSync(dirname(this.path), { recursive: true })
|
|
69
|
+
this.fs.writeSecretFileSync(this.path, `${token}\n`)
|
|
70
|
+
|
|
71
|
+
return token
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getPath(): string {
|
|
75
|
+
return this.path
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const DEFAULT_GATEWAY_TOKEN_PATH = join(homedir(), ".funnel", TOKEN_FILE_NAME)
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { join
|
|
2
|
-
import { FunnelFileSystem } from "@/
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { FunnelFileSystem } from "@/engine/fs/file-system"
|
|
3
|
+
import { resolveDaemonScript } from "@/gateway/resolve-daemon-script"
|
|
4
|
+
import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
|
|
5
|
+
import { FunnelProcessRunner } from "@/engine/process/process-runner"
|
|
6
|
+
import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
|
|
7
|
+
import { FUNNEL_DIR } from "@/engine/settings/settings-store"
|
|
8
|
+
import { FunnelClock } from "@/engine/time/clock"
|
|
9
|
+
import { NodeFunnelClock } from "@/engine/time/node-clock"
|
|
9
10
|
|
|
10
11
|
const DEFAULT_PORT = 9742
|
|
11
12
|
const DEFAULT_TMP_DIR = "/tmp/funnel"
|
|
13
|
+
const STARTUP_TIMEOUT_MS = 5000
|
|
14
|
+
const SIGTERM_TIMEOUT_MS = 2000
|
|
15
|
+
const POLL_INTERVAL_MS = 100
|
|
16
|
+
const SIGKILL_GRACE_MS = 200
|
|
12
17
|
|
|
13
18
|
type Deps = {
|
|
14
19
|
process?: FunnelProcessRunner
|
|
@@ -78,12 +83,17 @@ export class FunnelGateway {
|
|
|
78
83
|
|
|
79
84
|
this.fs.mkdirSync(this.tmpDir, { recursive: true })
|
|
80
85
|
|
|
81
|
-
const gatewayScript =
|
|
86
|
+
const gatewayScript = resolveDaemonScript()
|
|
82
87
|
const command = this.buildStartCommand(gatewayScript, options)
|
|
83
88
|
|
|
84
89
|
this.process.detach(["bash", "-c", command])
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS
|
|
92
|
+
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
if (this.isRunning()) return true
|
|
95
|
+
await this.sleep(POLL_INTERVAL_MS)
|
|
96
|
+
}
|
|
87
97
|
|
|
88
98
|
return this.isRunning()
|
|
89
99
|
}
|
|
@@ -111,7 +121,7 @@ export class FunnelGateway {
|
|
|
111
121
|
return false
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
const deadline = this.clock.millis() +
|
|
124
|
+
const deadline = this.clock.millis() + SIGTERM_TIMEOUT_MS
|
|
115
125
|
|
|
116
126
|
while (this.clock.millis() < deadline) {
|
|
117
127
|
if (!this.isProcessAlive(pid)) {
|
|
@@ -119,7 +129,7 @@ export class FunnelGateway {
|
|
|
119
129
|
return true
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
await this.sleep(
|
|
132
|
+
await this.sleep(POLL_INTERVAL_MS)
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
try {
|
|
@@ -128,7 +138,7 @@ export class FunnelGateway {
|
|
|
128
138
|
// ignore
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
await this.sleep(
|
|
141
|
+
await this.sleep(SIGKILL_GRACE_MS)
|
|
132
142
|
this.removePid()
|
|
133
143
|
|
|
134
144
|
return !this.isProcessAlive(pid)
|
|
@@ -162,6 +172,10 @@ export class FunnelGateway {
|
|
|
162
172
|
return this.gatewayLog
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
getPort(): number {
|
|
176
|
+
return this.port
|
|
177
|
+
}
|
|
178
|
+
|
|
165
179
|
private readPid(): number | null {
|
|
166
180
|
if (!this.fs.existsSync(this.pidFile)) return null
|
|
167
181
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { FunnelLogger } from "@/
|
|
2
|
-
import { NodeFunnelLogger } from "@/
|
|
3
|
-
import { FunnelProcessRunner } from "@/
|
|
4
|
-
import { NodeFunnelProcessRunner } from "@/
|
|
1
|
+
import { FunnelLogger } from "@/engine/logger/logger"
|
|
2
|
+
import { NodeFunnelLogger } from "@/engine/logger/node-logger"
|
|
3
|
+
import { FunnelProcessRunner } from "@/engine/process/process-runner"
|
|
4
|
+
import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
|
|
5
5
|
|
|
6
6
|
type Props = {
|
|
7
7
|
selfPid: number
|