@ouro.bot/cli 0.1.0-alpha.83 → 0.1.0-alpha.831
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -23
- package/RepairGuide.ouro/agent.json +5 -0
- package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
- package/RepairGuide.ouro/psyche/SOUL.md +55 -0
- package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
- package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
- package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
- package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
- package/assets/bluebubbles-host +264 -0
- package/assets/sanctuary-host-launcher.sh +16 -0
- package/changelog.json +5308 -16
- package/deploy/unraid/Dockerfile +54 -0
- package/deploy/unraid/README.txt +2983 -0
- package/deploy/unraid/audit-container-spec.sh +4 -0
- package/deploy/unraid/container-runtime.json +1 -0
- package/deploy/unraid/docker-man-template-transaction.mjs +711 -0
- package/deploy/unraid/docker-man-template-xml.cjs +105 -0
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +60 -0
- package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
- package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
- package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
- package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
- package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +43 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
- package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
- package/deploy/unraid/sanctuary-authority-installation.json +36 -0
- package/deploy/unraid/sanctuary-authority-service.sh +30 -0
- package/deploy/unraid/sanctuary-deployment-target.mjs +710 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2084 -0
- package/deploy/unraid/sanctuary-unit16-run.sh +560 -0
- package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +66 -0
- package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
- package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +795 -0
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +15 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +25 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +15 -0
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +135 -0
- package/deploy/unraid/sanctuary.xml +28 -0
- package/dist/a2a/card.js +57 -0
- package/dist/a2a/client.js +180 -0
- package/dist/a2a/config.js +50 -0
- package/dist/a2a/delegation-stores.js +56 -0
- package/dist/a2a/did-resolution.js +93 -0
- package/dist/a2a/identity.js +170 -0
- package/dist/a2a/inbound-share.js +107 -0
- package/dist/a2a/mission-result-wire.js +196 -0
- package/dist/a2a/onboarding.js +115 -0
- package/dist/a2a/pin-store.js +132 -0
- package/dist/a2a/seen-ledger.js +120 -0
- package/dist/a2a/server.js +604 -0
- package/dist/a2a/task-store.js +69 -0
- package/dist/a2a/transport.js +45 -0
- package/dist/a2a/types.js +3 -0
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +360 -0
- package/dist/arc/episodes.js +118 -0
- package/dist/arc/evolution.js +490 -0
- package/dist/arc/flight-recorder.js +1034 -0
- package/dist/arc/intentions.js +134 -0
- package/dist/arc/json-store.js +117 -0
- package/dist/arc/obligations.js +386 -0
- package/dist/arc/packets.js +336 -0
- package/dist/arc/presence.js +185 -0
- package/dist/arc/task-lifecycle.js +57 -0
- package/dist/commerce/store.js +755 -0
- package/dist/commerce/types.js +3 -0
- package/dist/heart/active-work.js +798 -40
- package/dist/heart/agent-entry.js +252 -3
- package/dist/heart/approval-files.js +113 -0
- package/dist/heart/approval-store.js +960 -0
- package/dist/heart/attachments/image-normalize.js +194 -0
- package/dist/heart/attachments/materialize.js +97 -0
- package/dist/heart/attachments/originals.js +88 -0
- package/dist/heart/attachments/render.js +29 -0
- package/dist/heart/attachments/sources/bluebubbles.js +156 -0
- package/dist/heart/attachments/sources/cli-local-file.js +78 -0
- package/dist/heart/attachments/sources/index.js +18 -0
- package/dist/heart/attachments/sources/telegram.js +71 -0
- package/dist/heart/attachments/store.js +132 -0
- package/dist/heart/attachments/types.js +93 -0
- package/dist/heart/auth/auth-flow.js +495 -0
- package/dist/heart/autonomy-budget.js +478 -0
- package/dist/heart/awaiting/await-alert.js +148 -0
- package/dist/heart/awaiting/await-expiry.js +143 -0
- package/dist/heart/awaiting/await-loader.js +91 -0
- package/dist/heart/awaiting/await-parser.js +149 -0
- package/dist/heart/awaiting/await-runtime-state.js +111 -0
- package/dist/heart/awaiting/await-scheduler.js +382 -0
- package/dist/heart/background-operations.js +281 -0
- package/dist/heart/bridges/manager.js +137 -17
- package/dist/heart/bridges/store.js +14 -2
- package/dist/heart/bundle-state.js +168 -0
- package/dist/heart/commitments.js +44 -18
- package/dist/heart/config-registry.js +331 -0
- package/dist/heart/config.js +174 -131
- package/dist/heart/context-loss-gauntlet.js +387 -0
- package/dist/heart/context-loss-sentinel.js +1157 -0
- package/dist/heart/core.js +2169 -581
- package/dist/heart/cross-chat-delivery.js +9 -22
- package/dist/heart/daemon/agent-config-check.js +451 -0
- package/dist/heart/daemon/agent-discovery.js +185 -3
- package/dist/heart/daemon/agent-service.js +542 -0
- package/dist/heart/daemon/agentic-repair.js +547 -0
- package/dist/heart/daemon/await-private-wake.js +59 -0
- package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
- package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
- package/dist/heart/daemon/bluebubbles-host.js +273 -0
- package/dist/heart/daemon/boot-sync-probe.js +197 -0
- package/dist/heart/daemon/cadence.js +180 -0
- package/dist/heart/daemon/cli-defaults.js +854 -0
- package/dist/heart/daemon/cli-desk.js +322 -0
- package/dist/heart/daemon/cli-exec.js +8822 -0
- package/dist/heart/daemon/cli-help.js +716 -0
- package/dist/heart/daemon/cli-parse.js +2771 -0
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/cli-render.js +777 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/connect-bay.js +323 -0
- package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
- package/dist/heart/daemon/container-healthcheck.js +112 -0
- package/dist/heart/daemon/container-runtime.js +97 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +171 -0
- package/dist/heart/daemon/container-spec-auditor.js +229 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
- package/dist/heart/daemon/daemon-cli.js +29 -2349
- package/dist/heart/daemon/daemon-entry.js +930 -11
- package/dist/heart/daemon/daemon-health.js +186 -0
- package/dist/heart/daemon/daemon-rollup.js +57 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
- package/dist/heart/daemon/daemon-tombstone.js +236 -0
- package/dist/heart/daemon/daemon.js +2084 -105
- package/dist/heart/daemon/dns-workflow.js +394 -0
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +1755 -0
- package/dist/heart/daemon/freshness.js +345 -0
- package/dist/heart/daemon/habit-private-wake.js +61 -0
- package/dist/heart/daemon/health-monitor.js +142 -11
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
- package/dist/heart/daemon/http-health-probe.js +80 -0
- package/dist/heart/daemon/human-command-screens.js +234 -0
- package/dist/heart/daemon/human-readiness.js +114 -0
- package/dist/heart/daemon/inner-status.js +78 -0
- package/dist/heart/daemon/interactive-repair.js +394 -0
- package/dist/heart/daemon/launchd.js +37 -8
- package/dist/heart/daemon/log-tailer.js +79 -10
- package/dist/heart/daemon/logs-prune.js +132 -0
- package/dist/heart/daemon/mcp-canary.js +524 -0
- package/dist/heart/daemon/message-router.js +65 -5
- package/dist/heart/daemon/migrate-to-desk.js +848 -0
- package/dist/heart/daemon/os-cron-deps.js +135 -0
- package/dist/heart/daemon/os-cron.js +22 -14
- package/dist/heart/daemon/ouro-bot-entry.js +4 -2
- package/dist/heart/daemon/ouro-entry.js +3 -1
- package/dist/heart/daemon/plugin-cli.js +432 -0
- package/dist/heart/daemon/process-manager.js +733 -61
- package/dist/heart/daemon/provider-discovery.js +137 -0
- package/dist/heart/daemon/provider-ping-progress.js +83 -0
- package/dist/heart/daemon/prunable-bundle.js +144 -0
- package/dist/heart/daemon/pulse.js +511 -0
- package/dist/heart/daemon/readiness-repair.js +365 -0
- package/dist/heart/daemon/run-hooks.js +2 -0
- package/dist/heart/daemon/runtime-logging.js +47 -14
- package/dist/heart/daemon/runtime-metadata.js +2 -30
- package/dist/heart/daemon/runtime-mode.js +13 -2
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2654 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1830 -0
- package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +955 -0
- package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
- package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
- package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
- package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +819 -0
- package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
- package/dist/heart/daemon/sanctuary-bundle-migration.js +729 -0
- package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
- package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
- package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
- package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
- package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
- package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
- package/dist/heart/daemon/sanctuary-package-management.js +103 -0
- package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +630 -0
- package/dist/heart/daemon/sense-manager.js +699 -48
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +1 -1
- package/dist/heart/daemon/socket-client.js +180 -12
- package/dist/heart/daemon/stale-bundle-prune.js +113 -0
- package/dist/heart/daemon/startup-tui.js +339 -0
- package/dist/heart/daemon/supercronic-supervisor.js +282 -0
- package/dist/heart/daemon/task-scheduler.js +117 -39
- package/dist/heart/daemon/terminal-ui.js +499 -0
- package/dist/heart/daemon/thoughts.js +138 -54
- package/dist/heart/daemon/up-progress.js +366 -0
- package/dist/heart/daemon/vault-items.js +56 -0
- package/dist/heart/delegation.js +1 -4
- package/dist/heart/external-events/router.js +1430 -0
- package/dist/heart/frontend-approval-runtime.js +506 -0
- package/dist/heart/frontend-journal.js +215 -0
- package/dist/heart/frontend-session-service.js +237 -0
- package/dist/heart/frontend-socket-client.js +164 -0
- package/dist/heart/frontend-socket.js +400 -0
- package/dist/heart/habits/habit-cancel.js +810 -0
- package/dist/heart/habits/habit-lifecycle.js +1327 -0
- package/dist/heart/habits/habit-migration.js +194 -0
- package/dist/heart/habits/habit-parser.js +278 -0
- package/dist/heart/habits/habit-runtime-state.js +93 -0
- package/dist/heart/habits/habit-scheduler.js +653 -0
- package/dist/heart/habits/habit-session-summary.js +318 -0
- package/dist/heart/habits/habit-session.js +636 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
- package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
- package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
- package/dist/heart/{daemon → hatch}/specialist-tools.js +53 -18
- package/dist/heart/identity.js +196 -59
- package/dist/heart/kept-notes.js +289 -0
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/machine-identity.js +161 -0
- package/dist/heart/mail-import-discovery.js +356 -0
- package/dist/heart/mailbox/mailbox-http-hooks.js +98 -0
- package/dist/heart/mailbox/mailbox-http-response.js +7 -0
- package/dist/heart/mailbox/mailbox-http-routes.js +398 -0
- package/dist/heart/mailbox/mailbox-http-static.js +103 -0
- package/dist/heart/mailbox/mailbox-http-transport.js +129 -0
- package/dist/heart/mailbox/mailbox-http.js +99 -0
- package/dist/heart/mailbox/mailbox-read.js +39 -0
- package/dist/heart/mailbox/mailbox-types.js +27 -0
- package/dist/heart/mailbox/mailbox-view.js +197 -0
- package/dist/heart/mailbox/readers/agent-machine.js +428 -0
- package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
- package/dist/heart/mailbox/readers/mail.js +375 -0
- package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
- package/dist/heart/mailbox/readers/sessions.js +231 -0
- package/dist/heart/mailbox/readers/shared.js +111 -0
- package/dist/heart/mcp/mcp-server.js +696 -0
- package/dist/heart/migrate-config.js +100 -0
- package/dist/heart/model-capabilities.js +11 -0
- package/dist/heart/orientation-frame.js +345 -0
- package/dist/heart/platform.js +81 -0
- package/dist/heart/primitives.js +1 -1
- package/dist/heart/private-runtime/decision-renderer.js +158 -0
- package/dist/heart/private-runtime/index.js +18 -0
- package/dist/heart/private-runtime/ledger.js +221 -0
- package/dist/heart/private-runtime/policy.js +252 -0
- package/dist/heart/private-runtime/types.js +2 -0
- package/dist/heart/progress-story.js +1 -1
- package/dist/heart/provider-attempt.js +137 -0
- package/dist/heart/provider-binding-resolver.js +273 -0
- package/dist/heart/provider-credentials.js +449 -0
- package/dist/heart/provider-failover.js +311 -0
- package/dist/heart/provider-models.js +96 -0
- package/dist/heart/provider-ping.js +265 -0
- package/dist/heart/provider-readiness-cache.js +119 -0
- package/dist/heart/provider-visibility.js +188 -0
- package/dist/heart/providers/anthropic-token.js +131 -0
- package/dist/heart/providers/anthropic.js +173 -60
- package/dist/heart/providers/azure.js +23 -11
- package/dist/heart/providers/error-classification.js +127 -0
- package/dist/heart/providers/github-copilot.js +34 -38
- package/dist/heart/providers/minimax-vlm.js +189 -0
- package/dist/heart/providers/minimax.js +26 -8
- package/dist/heart/providers/openai-codex-token.js +349 -0
- package/dist/heart/providers/openai-codex.js +55 -40
- package/dist/heart/providers/openai-compatible.js +177 -0
- package/dist/heart/run-ledger.js +247 -0
- package/dist/heart/runtime-capability-check.js +170 -0
- package/dist/heart/runtime-credentials.js +607 -0
- package/dist/heart/runtime-cwd.js +87 -0
- package/dist/heart/sense-truth.js +22 -4
- package/dist/heart/session-activity.js +92 -24
- package/dist/heart/session-events.js +1504 -0
- package/dist/heart/session-playback-cli-main.js +5 -0
- package/dist/heart/session-playback-cli.js +36 -0
- package/dist/heart/session-playback.js +231 -0
- package/dist/heart/session-redaction-repair-cli-main.js +31 -0
- package/dist/heart/session-redaction-repair.js +552 -0
- package/dist/heart/session-stats-cli-main.js +5 -0
- package/dist/heart/session-stats.js +183 -0
- package/dist/heart/session-transcript.js +133 -0
- package/dist/heart/start-of-turn-packet.js +372 -0
- package/dist/heart/steward-policy.js +618 -0
- package/dist/heart/streaming.js +604 -207
- package/dist/heart/structured-output.js +196 -0
- package/dist/heart/sync-classification.js +176 -0
- package/dist/heart/sync.js +449 -0
- package/dist/heart/target-resolution.js +11 -7
- package/dist/heart/tempo.js +93 -0
- package/dist/heart/temporal-view.js +41 -0
- package/dist/heart/timeouts.js +101 -0
- package/dist/heart/tool-activity-callbacks.js +59 -0
- package/dist/heart/tool-approval.js +382 -0
- package/dist/heart/tool-description.js +155 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +482 -0
- package/dist/heart/turn-execution-lease.js +30 -0
- package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
- package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
- package/dist/heart/versioning/ouro-path-installer.js +432 -0
- package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
- package/dist/heart/versioning/ouro-version-manager.js +409 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
- package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
- package/dist/heart/versioning/update-hooks.js +154 -0
- package/dist/heart/versioning/version-intent.js +114 -0
- package/dist/heart/work-card.js +380 -0
- package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +1 -0
- package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
- package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
- package/dist/mailbox-ui/index.html +16 -0
- package/dist/mailroom/attention.js +167 -0
- package/dist/mailroom/autonomy.js +209 -0
- package/dist/mailroom/blob-store.js +889 -0
- package/dist/mailroom/body-cache.js +61 -0
- package/dist/mailroom/cache-sync-cli.js +58 -0
- package/dist/mailroom/core.js +803 -0
- package/dist/mailroom/entry.js +160 -0
- package/dist/mailroom/file-store.js +568 -0
- package/dist/mailroom/hosted-cache-sync.js +384 -0
- package/dist/mailroom/mbox-import.js +393 -0
- package/dist/mailroom/migration.js +164 -0
- package/dist/mailroom/outbound.js +380 -0
- package/dist/mailroom/policy.js +263 -0
- package/dist/mailroom/reader.js +292 -0
- package/dist/mailroom/search-cache.js +513 -0
- package/dist/mailroom/search-relevance.js +319 -0
- package/dist/mailroom/smtp-ingress.js +176 -0
- package/dist/mailroom/source-state.js +176 -0
- package/dist/mailroom/thread.js +109 -0
- package/dist/mailroom/travel-extract.js +89 -0
- package/dist/mind/bundle-manifest.js +24 -2
- package/dist/mind/context.js +309 -111
- package/dist/mind/desk-section.js +362 -0
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/{memory.js → diary.js} +68 -77
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/{associative-recall.js → note-search.js} +47 -58
- package/dist/mind/obligation-steering.js +199 -3
- package/dist/mind/pending.js +36 -10
- package/dist/mind/prompt-budget.js +479 -0
- package/dist/mind/prompt-refresh.js +3 -2
- package/dist/mind/prompt.js +1050 -189
- package/dist/mind/provenance-trust.js +26 -0
- package/dist/mind/record-paths.js +312 -0
- package/dist/mind/scrutiny.js +173 -0
- package/dist/mind/session-transaction.js +564 -0
- package/dist/nerves/cli-logging.js +7 -1
- package/dist/nerves/coverage/audit-rules.js +15 -6
- package/dist/nerves/coverage/audit.js +28 -2
- package/dist/nerves/coverage/cli.js +1 -1
- package/dist/nerves/coverage/contract.js +5 -5
- package/dist/nerves/coverage/file-completeness.js +139 -5
- package/dist/nerves/coverage/run-artifacts.js +55 -35
- package/dist/nerves/coverage/source-scanner.js +4 -4
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +293 -9
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/redact.js +79 -0
- package/dist/nerves/review/cli-main.js +5 -0
- package/dist/nerves/review/cli.js +156 -0
- package/dist/nerves/review/core.js +159 -0
- package/dist/nerves/runtime.js +17 -1
- package/dist/repertoire/ado-client.js +15 -56
- package/dist/repertoire/ado-semantic.js +16 -10
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/bitwarden-store.js +1045 -0
- package/dist/repertoire/bundle-templates.js +71 -0
- package/dist/repertoire/bw-installer.js +180 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +331 -0
- package/dist/repertoire/coding/feedback.js +174 -32
- package/dist/repertoire/coding/index.js +15 -3
- package/dist/repertoire/coding/manager.js +159 -10
- package/dist/repertoire/coding/spawner.js +58 -11
- package/dist/repertoire/coding/tools.js +221 -15
- package/dist/repertoire/coding/workbench-client.js +272 -0
- package/dist/repertoire/coding/workbench-manager.js +346 -0
- package/dist/repertoire/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +178 -0
- package/dist/repertoire/desk/classifier.js +362 -0
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +811 -18
- package/dist/repertoire/mcp-client.js +44 -3
- package/dist/repertoire/mcp-manager.js +427 -139
- package/dist/repertoire/mcp-tools.js +124 -0
- package/dist/repertoire/plugin-mcp.js +175 -0
- package/dist/repertoire/plugins.js +253 -0
- package/dist/repertoire/relationship-authorization.js +322 -0
- package/dist/repertoire/shell-sessions.js +134 -0
- package/dist/repertoire/skills.js +48 -4
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tool-arguments.js +170 -0
- package/dist/repertoire/tool-results.js +29 -0
- package/dist/repertoire/tools-a2a.js +699 -0
- package/dist/repertoire/tools-attachments.js +322 -0
- package/dist/repertoire/tools-awaiting.js +575 -0
- package/dist/repertoire/tools-base.js +77 -1138
- package/dist/repertoire/tools-bluebubbles.js +2 -0
- package/dist/repertoire/tools-bridge.js +144 -0
- package/dist/repertoire/tools-bundle.js +993 -0
- package/dist/repertoire/tools-commerce.js +253 -0
- package/dist/repertoire/tools-config.js +186 -0
- package/dist/repertoire/tools-continuity.js +575 -0
- package/dist/repertoire/tools-credential.js +383 -0
- package/dist/repertoire/tools-evolution.js +527 -0
- package/dist/repertoire/tools-files.js +344 -0
- package/dist/repertoire/tools-flight.js +290 -0
- package/dist/repertoire/tools-flow.js +119 -0
- package/dist/repertoire/tools-github.js +3 -8
- package/dist/repertoire/tools-habits.js +103 -0
- package/dist/repertoire/tools-mail.js +1828 -0
- package/dist/repertoire/tools-notes.js +485 -0
- package/dist/repertoire/tools-obligations.js +182 -0
- package/dist/repertoire/tools-orientation.js +31 -0
- package/dist/repertoire/tools-record.js +482 -0
- package/dist/repertoire/tools-rsvp.js +139 -0
- package/dist/repertoire/tools-runtime.js +150 -0
- package/dist/repertoire/tools-sanctuary-host.js +202 -0
- package/dist/repertoire/tools-session.js +1133 -0
- package/dist/repertoire/tools-shell.js +149 -0
- package/dist/repertoire/tools-steward-policy.js +119 -0
- package/dist/repertoire/tools-stripe.js +224 -0
- package/dist/repertoire/tools-surface.js +369 -0
- package/dist/repertoire/tools-teams.js +12 -39
- package/dist/repertoire/tools-telegram-contacts.js +37 -0
- package/dist/repertoire/tools-travel.js +125 -0
- package/dist/repertoire/tools-trip.js +982 -0
- package/dist/repertoire/tools-unraid.js +531 -0
- package/dist/repertoire/tools-user-profile.js +146 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools-voice.js +145 -0
- package/dist/repertoire/tools.js +494 -148
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/unraid-client.js +155 -0
- package/dist/repertoire/unraid-restart.js +339 -0
- package/dist/repertoire/user-profile.js +131 -0
- package/dist/repertoire/vault-setup.js +246 -0
- package/dist/repertoire/vault-unlock.js +628 -0
- package/dist/rsvp/aisleplanner-client.js +354 -0
- package/dist/rsvp/cli.js +926 -0
- package/dist/rsvp/config.js +327 -0
- package/dist/rsvp/cutover.js +433 -0
- package/dist/rsvp/diagnostics.js +232 -0
- package/dist/rsvp/diff-renderer.js +176 -0
- package/dist/rsvp/habit-policy.js +110 -0
- package/dist/rsvp/habit-stage.js +123 -0
- package/dist/rsvp/incident-bundle.js +97 -0
- package/dist/rsvp/migration.js +204 -0
- package/dist/rsvp/native-habit-runner.js +630 -0
- package/dist/rsvp/outbound-state.js +628 -0
- package/dist/rsvp/query.js +118 -0
- package/dist/rsvp/replay.js +233 -0
- package/dist/rsvp/snapshot.js +232 -0
- package/dist/rsvp/spend-ledger.js +175 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/a2a-entry.js +115 -0
- package/dist/senses/acp-server.js +386 -0
- package/dist/senses/attention-queue.js +186 -0
- package/dist/senses/await-turn-message.js +58 -0
- package/dist/senses/bluebubbles/active-turns.js +216 -0
- package/dist/senses/bluebubbles/attachment-cache.js +53 -0
- package/dist/senses/bluebubbles/attachment-download.js +137 -0
- package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +355 -40
- package/dist/senses/bluebubbles/context-packet.js +207 -0
- package/dist/senses/bluebubbles/context-smoke.js +144 -0
- package/dist/senses/bluebubbles/entry.js +90 -0
- package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
- package/dist/senses/bluebubbles/index.js +3841 -0
- package/dist/senses/bluebubbles/latest-turn.js +311 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
- package/dist/senses/bluebubbles/outbound-state.js +308 -0
- package/dist/senses/bluebubbles/processed-log.js +133 -0
- package/dist/senses/bluebubbles/reaction-policy.js +47 -0
- package/dist/senses/bluebubbles/replay.js +284 -0
- package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
- package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
- package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
- package/dist/senses/bluebubbles/webhook-registration.js +228 -0
- package/dist/senses/bluebubbles-meta-guard.js +39 -0
- package/dist/senses/cli/bracketed-paste.js +82 -0
- package/dist/senses/cli/image-paste.js +287 -0
- package/dist/senses/cli/image-ref-navigation.js +75 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/input-keys.js +174 -0
- package/dist/senses/cli/kill-ring.js +86 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +607 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +60 -0
- package/dist/senses/cli/streaming-markdown.js +526 -0
- package/dist/senses/cli/tool-display.js +85 -0
- package/dist/senses/cli/tool-render.js +85 -0
- package/dist/senses/cli/tui-store.js +241 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/cli.js +604 -320
- package/dist/senses/commands.js +66 -3
- package/dist/senses/context-packet-ledger.js +371 -0
- package/dist/senses/context-packets.js +154 -0
- package/dist/senses/habit-lifecycle-guard.js +24 -0
- package/dist/senses/habit-turn-message.js +160 -0
- package/dist/senses/ingress-evidence.js +2 -0
- package/dist/senses/mail-entry.js +66 -0
- package/dist/senses/mail.js +524 -0
- package/dist/senses/pipeline.js +1115 -204
- package/dist/senses/private-runtime-worker.js +859 -0
- package/dist/senses/private-runtime.js +1517 -0
- package/dist/senses/proactive-content-guard.js +51 -0
- package/dist/senses/root-host-approval-port.js +345 -0
- package/dist/senses/root-host-approval-runtime.js +393 -0
- package/dist/senses/sanctuary-authority-resident.js +102 -0
- package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
- package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
- package/dist/senses/sanctuary-grounding.js +118 -0
- package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
- package/dist/senses/sanctuary-health-acceptance-probe.js +774 -0
- package/dist/senses/sanctuary-health-runner.js +100 -0
- package/dist/senses/sanctuary-health.js +408 -0
- package/dist/senses/sanctuary-install-state-contract.js +126 -0
- package/dist/senses/sanctuary-interactive-control.js +410 -0
- package/dist/senses/sanctuary-media-catalog-contract.js +297 -0
- package/dist/senses/sanctuary-media-optimization.js +514 -0
- package/dist/senses/sanctuary-runtime.js +340 -0
- package/dist/senses/sanctuary-sab.js +85 -0
- package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
- package/dist/senses/shared-turn.js +772 -0
- package/dist/senses/surface-tool.js +109 -0
- package/dist/senses/teams-entry.js +60 -8
- package/dist/senses/teams.js +519 -218
- package/dist/senses/telegram-admission.js +718 -0
- package/dist/senses/telegram-approval-runtime.js +793 -0
- package/dist/senses/telegram-attachments.js +186 -0
- package/dist/senses/telegram-audit-ledger.js +220 -0
- package/dist/senses/telegram-authority-transport.js +231 -0
- package/dist/senses/telegram-client.js +1749 -0
- package/dist/senses/telegram-effect-adapter.js +896 -0
- package/dist/senses/telegram-entry.js +83 -0
- package/dist/senses/telegram.js +2324 -0
- package/dist/senses/trust-gate.js +104 -9
- package/dist/senses/voice/audio-playback.js +237 -0
- package/dist/senses/voice/audio-routing.js +119 -0
- package/dist/senses/voice/elevenlabs.js +202 -0
- package/dist/senses/voice/floor-control.js +431 -0
- package/dist/senses/voice/floor-controller.js +115 -0
- package/dist/senses/voice/golden-path.js +116 -0
- package/dist/senses/voice/index.js +29 -0
- package/dist/senses/voice/meeting.js +113 -0
- package/dist/senses/voice/outbound.js +190 -0
- package/dist/senses/voice/phone.js +33 -0
- package/dist/senses/voice/playback.js +139 -0
- package/dist/senses/voice/realtime-eval.js +496 -0
- package/dist/senses/voice/realtime-trace.js +531 -0
- package/dist/senses/voice/transcript.js +70 -0
- package/dist/senses/voice/turn.js +191 -0
- package/dist/senses/voice/twilio-phone-runtime.js +807 -0
- package/dist/senses/voice/twilio-phone.js +5031 -0
- package/dist/senses/voice/types.js +2 -0
- package/dist/senses/voice/whisper.js +161 -0
- package/dist/senses/voice-entry.js +81 -0
- package/dist/senses/voice-realtime-eval-command.js +99 -0
- package/dist/senses/voice-realtime-eval-entry.js +21 -0
- package/dist/senses/voice-twilio-entry.js +87 -0
- package/dist/trips/core.js +138 -0
- package/dist/trips/store.js +265 -0
- package/dist/util/frontmatter.js +69 -0
- package/npm-shrinkwrap.json +8138 -0
- package/package.json +72 -13
- package/skills/agent-commerce.md +113 -0
- package/skills/browser-navigation.md +117 -0
- package/skills/commerce-setup-guide.md +116 -0
- package/skills/commerce-setup.md +84 -0
- package/skills/configure-dev-tools.md +99 -0
- package/skills/travel-planning.md +138 -0
- package/dist/heart/daemon/auth-flow.js +0 -430
- package/dist/heart/daemon/ouro-path-installer.js +0 -260
- package/dist/heart/daemon/ouro-version-manager.js +0 -171
- package/dist/heart/daemon/update-hooks.js +0 -138
- package/dist/heart/obligations.js +0 -191
- package/dist/heart/safe-workspace.js +0 -228
- package/dist/heart/session-recall.js +0 -116
- package/dist/mind/friends/channel.js +0 -84
- package/dist/mind/friends/group-context.js +0 -144
- package/dist/mind/friends/resolver.js +0 -84
- package/dist/mind/friends/store-file.js +0 -190
- package/dist/mind/friends/store.js +0 -4
- package/dist/mind/friends/tokens.js +0 -26
- package/dist/mind/friends/trust-explanation.js +0 -74
- package/dist/mind/friends/types.js +0 -29
- package/dist/repertoire/tasks/board.js +0 -134
- package/dist/repertoire/tasks/index.js +0 -224
- package/dist/repertoire/tasks/lifecycle.js +0 -80
- package/dist/repertoire/tasks/middleware.js +0 -65
- package/dist/repertoire/tasks/parser.js +0 -173
- package/dist/repertoire/tasks/scanner.js +0 -132
- package/dist/repertoire/tasks/transitions.js +0 -144
- package/dist/senses/bluebubbles-entry.js +0 -13
- package/dist/senses/bluebubbles.js +0 -1181
- package/dist/senses/debug-activity.js +0 -154
- package/dist/senses/inner-dialog-worker.js +0 -90
- package/dist/senses/inner-dialog.js +0 -525
- package/subagents/README.md +0 -7
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
- /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
- /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
- /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
- /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
- /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
|
@@ -0,0 +1,1749 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS = exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS = exports.TELEGRAM_APPROVAL_EXPIRED_TEXT = exports.TELEGRAM_APPROVAL_TTL_MS = exports.FileTelegramPendingApprovalStore = exports.FileTelegramUpdateInboxStore = exports.FileTelegramOffsetStore = exports.TelegramApiError = void 0;
|
|
4
|
+
exports.createTelegramLongPoll = createTelegramLongPoll;
|
|
5
|
+
exports.classifyTelegramPersistedApprovalState = classifyTelegramPersistedApprovalState;
|
|
6
|
+
exports.createTelegramApprovalTransport = createTelegramApprovalTransport;
|
|
7
|
+
exports.escapeTelegramHtml = escapeTelegramHtml;
|
|
8
|
+
exports.splitTelegramText = splitTelegramText;
|
|
9
|
+
exports.sendTelegramText = sendTelegramText;
|
|
10
|
+
exports.createTelegramBotApi = createTelegramBotApi;
|
|
11
|
+
const runtime_1 = require("../nerves/runtime");
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
class TelegramApiError extends Error {
|
|
16
|
+
status;
|
|
17
|
+
errorCode;
|
|
18
|
+
retryAfterSeconds;
|
|
19
|
+
constructor(message, options = {}) {
|
|
20
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
21
|
+
this.name = "TelegramApiError";
|
|
22
|
+
this.status = options.status ?? null;
|
|
23
|
+
this.errorCode = options.errorCode ?? null;
|
|
24
|
+
this.retryAfterSeconds = options.retryAfterSeconds ?? null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.TelegramApiError = TelegramApiError;
|
|
28
|
+
function durableAtomicWrite(path, contents) {
|
|
29
|
+
const directory = (0, node_path_1.dirname)(path);
|
|
30
|
+
(0, node_fs_1.mkdirSync)(directory, { recursive: true });
|
|
31
|
+
const temporaryPath = `${path}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`;
|
|
32
|
+
(0, node_fs_1.writeFileSync)(temporaryPath, contents, { mode: 0o600 });
|
|
33
|
+
const temporary = (0, node_fs_1.openSync)(temporaryPath, "r");
|
|
34
|
+
try {
|
|
35
|
+
(0, node_fs_1.fsyncSync)(temporary);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
(0, node_fs_1.closeSync)(temporary);
|
|
39
|
+
}
|
|
40
|
+
(0, node_fs_1.renameSync)(temporaryPath, path);
|
|
41
|
+
const directoryHandle = (0, node_fs_1.openSync)(directory, "r");
|
|
42
|
+
try {
|
|
43
|
+
(0, node_fs_1.fsyncSync)(directoryHandle);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
(0, node_fs_1.closeSync)(directoryHandle);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
class FileTelegramOffsetStore {
|
|
50
|
+
path;
|
|
51
|
+
constructor(path) {
|
|
52
|
+
this.path = path;
|
|
53
|
+
}
|
|
54
|
+
load() {
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = (0, node_fs_1.readFileSync)(this.path, "utf8");
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
61
|
+
return 0;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const value = JSON.parse(raw);
|
|
66
|
+
if (!Number.isSafeInteger(value.nextUpdateId) || value.nextUpdateId < 0)
|
|
67
|
+
throw new Error("invalid offset");
|
|
68
|
+
return value.nextUpdateId;
|
|
69
|
+
}
|
|
70
|
+
catch (cause) {
|
|
71
|
+
throw new Error("Telegram offset state is corrupt", { cause });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
save(nextUpdateId) {
|
|
75
|
+
if (!Number.isSafeInteger(nextUpdateId) || nextUpdateId < 0)
|
|
76
|
+
throw new Error("Telegram offset must be a non-negative safe integer");
|
|
77
|
+
durableAtomicWrite(this.path, `${JSON.stringify({ nextUpdateId })}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.FileTelegramOffsetStore = FileTelegramOffsetStore;
|
|
81
|
+
const DEFAULT_TELEGRAM_INDETERMINATE_RETENTION_MS = 24 * 60 * 60 * 1_000;
|
|
82
|
+
const DEFAULT_TELEGRAM_MAX_INDETERMINATE_RECEIPTS = 1_000;
|
|
83
|
+
// Long-poll retry backoff. A transient failure (a Telegram 5xx, a dropped
|
|
84
|
+
// authority socket in the resident, or a relayed gateway 409 during a
|
|
85
|
+
// getUpdates-session reclaim) must not exit the sense process; the loop waits
|
|
86
|
+
// and retries, doubling from the base up to the cap and resetting on success.
|
|
87
|
+
const TELEGRAM_POLL_RETRY_BASE_MS = 1_000;
|
|
88
|
+
const TELEGRAM_POLL_RETRY_MAX_MS = 30_000;
|
|
89
|
+
const TELEGRAM_UPDATE_DIGEST_DOMAIN = "ouroboros.telegram.update.v1";
|
|
90
|
+
const TELEGRAM_UPDATE_SEQUENCE_DOMAIN = "ouroboros.telegram.update-sequence.v1";
|
|
91
|
+
const TELEGRAM_UPDATE_DIGEST = /^tgu_[A-Za-z0-9_-]{43}$/u;
|
|
92
|
+
const TELEGRAM_UPDATE_SEQUENCE_DIGEST = /^tgs_[A-Za-z0-9_-]{43}$/u;
|
|
93
|
+
const TELEGRAM_ADMISSION_ID = /^[a-f0-9]{20}$/u;
|
|
94
|
+
const TELEGRAM_SESSION_EVENT_ID = /^evt-[0-9]{6,}$/u;
|
|
95
|
+
function boundedTelegramWorkText(value, maxLength) {
|
|
96
|
+
return typeof value === "string" && value.length > 0 && value.length <= maxLength;
|
|
97
|
+
}
|
|
98
|
+
function updateReceipt(update) {
|
|
99
|
+
return {
|
|
100
|
+
digest: `tgu_${(0, node_crypto_1.createHash)("sha256").update(`${TELEGRAM_UPDATE_DIGEST_DOMAIN}\0${JSON.stringify(update)}`, "utf8").digest("base64url")}`,
|
|
101
|
+
sequenceDigest: `tgs_${(0, node_crypto_1.createHash)("sha256").update(`${TELEGRAM_UPDATE_SEQUENCE_DOMAIN}\0${update.update_id}`, "utf8").digest("base64url")}`,
|
|
102
|
+
updateClass: update.callback_query ? "callback" : update.message ? "message" : "other",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function sameReceipt(left, right) {
|
|
106
|
+
return left.digest === right.digest;
|
|
107
|
+
}
|
|
108
|
+
function uniqueReceipts(records) {
|
|
109
|
+
return records.filter((record, index) => records.findIndex((candidate) => sameReceipt(candidate, record)) === index);
|
|
110
|
+
}
|
|
111
|
+
class FileTelegramUpdateInboxStore {
|
|
112
|
+
path;
|
|
113
|
+
now;
|
|
114
|
+
indeterminateRetentionMs;
|
|
115
|
+
maxIndeterminateReceipts;
|
|
116
|
+
constructor(path, options = {}) {
|
|
117
|
+
this.path = path;
|
|
118
|
+
this.now = options.now ?? Date.now;
|
|
119
|
+
this.indeterminateRetentionMs = options.indeterminateRetentionMs ?? DEFAULT_TELEGRAM_INDETERMINATE_RETENTION_MS;
|
|
120
|
+
this.maxIndeterminateReceipts = options.maxIndeterminateReceipts ?? DEFAULT_TELEGRAM_MAX_INDETERMINATE_RECEIPTS;
|
|
121
|
+
if (!Number.isSafeInteger(this.indeterminateRetentionMs) || this.indeterminateRetentionMs < 1) {
|
|
122
|
+
throw new Error("Telegram indeterminate receipt retention must be a positive safe integer");
|
|
123
|
+
}
|
|
124
|
+
if (!Number.isSafeInteger(this.maxIndeterminateReceipts) || this.maxIndeterminateReceipts < 1) {
|
|
125
|
+
throw new Error("Telegram indeterminate receipt limit must be a positive safe integer");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
timestamp() {
|
|
129
|
+
const value = this.now();
|
|
130
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
131
|
+
throw new Error("Telegram inbox clock must return a non-negative safe integer");
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
prune(records, timestamp) {
|
|
135
|
+
const cutoff = timestamp - this.indeterminateRetentionMs;
|
|
136
|
+
return [...records]
|
|
137
|
+
.filter((record) => record.quarantinedAt >= cutoff)
|
|
138
|
+
.sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.digest.localeCompare(right.digest))
|
|
139
|
+
.slice(-this.maxIndeterminateReceipts);
|
|
140
|
+
}
|
|
141
|
+
read() {
|
|
142
|
+
try {
|
|
143
|
+
const value = JSON.parse((0, node_fs_1.readFileSync)(this.path, "utf8"));
|
|
144
|
+
const validReceipt = (record) => Boolean(record)
|
|
145
|
+
&& typeof record === "object"
|
|
146
|
+
&& !Array.isArray(record)
|
|
147
|
+
&& Object.keys(record).sort().join(",") === "digest,sequenceDigest,updateClass"
|
|
148
|
+
&& TELEGRAM_UPDATE_DIGEST.test(record.digest)
|
|
149
|
+
&& TELEGRAM_UPDATE_SEQUENCE_DIGEST.test(record.sequenceDigest)
|
|
150
|
+
&& ["callback", "message", "other"].includes(record.updateClass);
|
|
151
|
+
const validIndeterminate = (record) => Boolean(record)
|
|
152
|
+
&& typeof record === "object"
|
|
153
|
+
&& !Array.isArray(record)
|
|
154
|
+
&& Object.keys(record).sort().join(",") === "digest,quarantinedAt,sequenceDigest,updateClass,warningAcknowledged"
|
|
155
|
+
&& TELEGRAM_UPDATE_DIGEST.test(record.digest)
|
|
156
|
+
&& TELEGRAM_UPDATE_SEQUENCE_DIGEST.test(record.sequenceDigest)
|
|
157
|
+
&& ["callback", "message", "other"].includes(record.updateClass)
|
|
158
|
+
&& Number.isSafeInteger(record.quarantinedAt)
|
|
159
|
+
&& record.quarantinedAt >= 0
|
|
160
|
+
&& typeof record.warningAcknowledged === "boolean";
|
|
161
|
+
const validAdmitted = (record) => Boolean(record)
|
|
162
|
+
&& typeof record === "object"
|
|
163
|
+
&& !Array.isArray(record)
|
|
164
|
+
&& Object.keys(record).sort().join(",") === "admissionId,eventId,friendId,reference,sessionKey"
|
|
165
|
+
&& TELEGRAM_ADMISSION_ID.test(record.admissionId)
|
|
166
|
+
&& boundedTelegramWorkText(record.friendId, 512)
|
|
167
|
+
&& boundedTelegramWorkText(record.sessionKey, 1_024)
|
|
168
|
+
&& TELEGRAM_SESSION_EVENT_ID.test(record.eventId)
|
|
169
|
+
&& record.reference === `telegram-admission:${record.admissionId}`;
|
|
170
|
+
const validIndeterminateAdmitted = (record) => {
|
|
171
|
+
if (!record || typeof record !== "object" || Array.isArray(record)
|
|
172
|
+
|| Object.keys(record).sort().join(",") !== "admissionId,eventId,friendId,quarantinedAt,reference,sessionKey")
|
|
173
|
+
return false;
|
|
174
|
+
const { quarantinedAt, ...work } = record;
|
|
175
|
+
return Number.isSafeInteger(quarantinedAt) && quarantinedAt >= 0 && validAdmitted(work);
|
|
176
|
+
};
|
|
177
|
+
if (value.version === 5) {
|
|
178
|
+
if (Object.keys(value).sort().join(",") !== "admittedDispatching,admittedIndeterminate,admittedPending,admittedSettled,dispatching,indeterminate,pending,settled,version"
|
|
179
|
+
|| !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate) || !Array.isArray(value.settled)
|
|
180
|
+
|| !Array.isArray(value.admittedPending) || !Array.isArray(value.admittedDispatching)
|
|
181
|
+
|| !Array.isArray(value.admittedSettled) || !Array.isArray(value.admittedIndeterminate)
|
|
182
|
+
|| !value.pending.every(validReceipt) || !value.dispatching.every(validReceipt) || !value.settled.every(validReceipt)
|
|
183
|
+
|| !value.indeterminate.every(validIndeterminate)
|
|
184
|
+
|| !value.admittedPending.every(validAdmitted) || !value.admittedDispatching.every(validAdmitted)
|
|
185
|
+
|| !value.admittedSettled.every(validAdmitted) || !value.admittedIndeterminate.every(validIndeterminateAdmitted)) {
|
|
186
|
+
throw new Error("invalid bounded inbox shape");
|
|
187
|
+
}
|
|
188
|
+
const state = structuredClone(value);
|
|
189
|
+
const timestamp = this.timestamp();
|
|
190
|
+
const prunedUpdates = this.prune(state.indeterminate, timestamp);
|
|
191
|
+
const prunedAdmitted = state.admittedIndeterminate
|
|
192
|
+
.filter((work) => work.quarantinedAt >= timestamp - this.indeterminateRetentionMs)
|
|
193
|
+
.sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.admissionId.localeCompare(right.admissionId))
|
|
194
|
+
.slice(-this.maxIndeterminateReceipts);
|
|
195
|
+
if (prunedUpdates.length !== state.indeterminate.length || prunedAdmitted.length !== state.admittedIndeterminate.length) {
|
|
196
|
+
state.indeterminate = prunedUpdates;
|
|
197
|
+
state.admittedIndeterminate = prunedAdmitted;
|
|
198
|
+
this.write(state);
|
|
199
|
+
}
|
|
200
|
+
return state;
|
|
201
|
+
}
|
|
202
|
+
if (value.version === 4) {
|
|
203
|
+
if (Object.keys(value).sort().join(",") !== "dispatching,indeterminate,pending,settled,version"
|
|
204
|
+
|| !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate)
|
|
205
|
+
|| !Array.isArray(value.settled)
|
|
206
|
+
|| !value.pending.every(validReceipt) || !value.dispatching.every(validReceipt)
|
|
207
|
+
|| !value.settled.every(validReceipt)
|
|
208
|
+
|| !value.indeterminate.every(validIndeterminate))
|
|
209
|
+
throw new Error("invalid bounded inbox shape");
|
|
210
|
+
const state = {
|
|
211
|
+
...structuredClone(value),
|
|
212
|
+
version: 5,
|
|
213
|
+
admittedPending: [],
|
|
214
|
+
admittedDispatching: [],
|
|
215
|
+
admittedSettled: [],
|
|
216
|
+
admittedIndeterminate: [],
|
|
217
|
+
};
|
|
218
|
+
const pruned = this.prune(state.indeterminate, this.timestamp());
|
|
219
|
+
state.indeterminate = pruned;
|
|
220
|
+
this.write(state);
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
223
|
+
if (value.version === 3) {
|
|
224
|
+
if (Object.keys(value).sort().join(",") !== "dispatching,indeterminate,pending,version"
|
|
225
|
+
|| !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate)
|
|
226
|
+
|| !value.pending.every(validReceipt) || !value.dispatching.every(validReceipt)
|
|
227
|
+
|| !value.indeterminate.every(validIndeterminate))
|
|
228
|
+
throw new Error("invalid bounded inbox shape");
|
|
229
|
+
const timestamp = this.timestamp();
|
|
230
|
+
const migrated = {
|
|
231
|
+
version: 5,
|
|
232
|
+
pending: structuredClone(value.pending),
|
|
233
|
+
dispatching: structuredClone(value.dispatching),
|
|
234
|
+
settled: [],
|
|
235
|
+
indeterminate: this.prune(structuredClone(value.indeterminate), timestamp),
|
|
236
|
+
admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
|
|
237
|
+
};
|
|
238
|
+
this.write(migrated);
|
|
239
|
+
return migrated;
|
|
240
|
+
}
|
|
241
|
+
if (value.version === 2) {
|
|
242
|
+
if (Object.keys(value).sort().join(",") !== "dispatching,indeterminate,pending,version")
|
|
243
|
+
throw new Error("invalid opaque inbox shape");
|
|
244
|
+
const arrays = [value.pending, value.dispatching, value.indeterminate];
|
|
245
|
+
if (!arrays.every(Array.isArray))
|
|
246
|
+
throw new Error("invalid opaque inbox shape");
|
|
247
|
+
if (!arrays.flat().every(validReceipt))
|
|
248
|
+
throw new Error("invalid opaque inbox receipt");
|
|
249
|
+
const timestamp = this.timestamp();
|
|
250
|
+
const migrated = {
|
|
251
|
+
version: 5,
|
|
252
|
+
pending: structuredClone(value.pending),
|
|
253
|
+
dispatching: structuredClone(value.dispatching),
|
|
254
|
+
settled: [],
|
|
255
|
+
indeterminate: this.prune(value.indeterminate.map((record) => ({
|
|
256
|
+
...record,
|
|
257
|
+
quarantinedAt: timestamp,
|
|
258
|
+
warningAcknowledged: false,
|
|
259
|
+
})), timestamp),
|
|
260
|
+
admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
|
|
261
|
+
};
|
|
262
|
+
this.write(migrated);
|
|
263
|
+
return migrated;
|
|
264
|
+
}
|
|
265
|
+
const pending = value.pending;
|
|
266
|
+
const dispatching = value.dispatching ?? [];
|
|
267
|
+
const completed = value.completedUpdateIds;
|
|
268
|
+
if (!Array.isArray(pending) || !Array.isArray(dispatching) || !Array.isArray(completed))
|
|
269
|
+
throw new Error("invalid legacy inbox shape");
|
|
270
|
+
const validLegacy = (update) => Boolean(update)
|
|
271
|
+
&& typeof update === "object"
|
|
272
|
+
&& Number.isSafeInteger(update.update_id)
|
|
273
|
+
&& update.update_id >= 0;
|
|
274
|
+
if (![...pending, ...dispatching].every(validLegacy)
|
|
275
|
+
|| !completed.every((id) => Number.isSafeInteger(id) && id >= 0))
|
|
276
|
+
throw new Error("invalid legacy inbox record");
|
|
277
|
+
const timestamp = this.timestamp();
|
|
278
|
+
const migrated = {
|
|
279
|
+
version: 5,
|
|
280
|
+
pending: [],
|
|
281
|
+
dispatching: [],
|
|
282
|
+
settled: [],
|
|
283
|
+
indeterminate: this.prune(uniqueReceipts([...pending, ...dispatching].map(updateReceipt)).map((record) => ({
|
|
284
|
+
...record,
|
|
285
|
+
quarantinedAt: timestamp,
|
|
286
|
+
warningAcknowledged: false,
|
|
287
|
+
})), timestamp),
|
|
288
|
+
admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
|
|
289
|
+
};
|
|
290
|
+
this.write(migrated);
|
|
291
|
+
return migrated;
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
295
|
+
return {
|
|
296
|
+
version: 5, pending: [], dispatching: [], settled: [], indeterminate: [],
|
|
297
|
+
admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
|
|
298
|
+
};
|
|
299
|
+
throw new Error("Telegram update inbox state is corrupt", { cause: error });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
write(state) {
|
|
303
|
+
durableAtomicWrite(this.path, `${JSON.stringify(state)}\n`);
|
|
304
|
+
}
|
|
305
|
+
load() {
|
|
306
|
+
const state = this.read();
|
|
307
|
+
return [...state.pending, ...state.dispatching, ...state.settled, ...state.indeterminate];
|
|
308
|
+
}
|
|
309
|
+
loadPending() {
|
|
310
|
+
return this.read().pending;
|
|
311
|
+
}
|
|
312
|
+
loadIndeterminate() {
|
|
313
|
+
return this.read().indeterminate;
|
|
314
|
+
}
|
|
315
|
+
quarantineStranded() {
|
|
316
|
+
const state = this.read();
|
|
317
|
+
const stranded = uniqueReceipts([...state.pending, ...state.dispatching]);
|
|
318
|
+
const timestamp = this.timestamp();
|
|
319
|
+
state.pending = [];
|
|
320
|
+
state.dispatching = [];
|
|
321
|
+
const combined = [...state.indeterminate, ...stranded.map((record) => ({
|
|
322
|
+
...record,
|
|
323
|
+
quarantinedAt: timestamp,
|
|
324
|
+
warningAcknowledged: false,
|
|
325
|
+
}))];
|
|
326
|
+
state.indeterminate = this.prune(combined.filter((record, index) => (combined.findIndex((candidate) => sameReceipt(candidate, record)) === index)), timestamp);
|
|
327
|
+
const warnings = state.indeterminate.filter((record) => !record.warningAcknowledged);
|
|
328
|
+
if (stranded.length > 0)
|
|
329
|
+
this.write(state);
|
|
330
|
+
return warnings.map(({ quarantinedAt: _quarantinedAt, warningAcknowledged: _warningAcknowledged, ...receipt }) => receipt);
|
|
331
|
+
}
|
|
332
|
+
acknowledgeIndeterminateWarning(receipt) {
|
|
333
|
+
const state = this.read();
|
|
334
|
+
const warning = state.indeterminate.find((candidate) => sameReceipt(candidate, receipt));
|
|
335
|
+
if (!warning || warning.warningAcknowledged)
|
|
336
|
+
return false;
|
|
337
|
+
warning.warningAcknowledged = true;
|
|
338
|
+
this.write(state);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
capture(update) {
|
|
342
|
+
const state = this.read();
|
|
343
|
+
const receipt = updateReceipt(update);
|
|
344
|
+
const existing = [...state.pending, ...state.dispatching, ...state.settled, ...state.indeterminate]
|
|
345
|
+
.find((candidate) => candidate.sequenceDigest === receipt.sequenceDigest);
|
|
346
|
+
if (existing) {
|
|
347
|
+
if (!sameReceipt(existing, receipt))
|
|
348
|
+
throw new Error("Telegram update inbox has a conflicting opaque receipt");
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
state.pending.push(receipt);
|
|
352
|
+
this.write(state);
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
claim(update) {
|
|
356
|
+
const state = this.read();
|
|
357
|
+
const receipt = updateReceipt(update);
|
|
358
|
+
if (state.dispatching.some((candidate) => sameReceipt(candidate, receipt)))
|
|
359
|
+
return false;
|
|
360
|
+
const index = state.pending.findIndex((candidate) => sameReceipt(candidate, receipt));
|
|
361
|
+
if (index < 0)
|
|
362
|
+
return false;
|
|
363
|
+
state.pending.splice(index, 1);
|
|
364
|
+
state.dispatching.push(receipt);
|
|
365
|
+
this.write(state);
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
complete(update) {
|
|
369
|
+
const state = this.read();
|
|
370
|
+
const receipt = updateReceipt(update);
|
|
371
|
+
const completed = [...state.pending, ...state.dispatching].some((candidate) => sameReceipt(candidate, receipt));
|
|
372
|
+
state.pending = state.pending.filter((candidate) => !sameReceipt(candidate, receipt));
|
|
373
|
+
state.dispatching = state.dispatching.filter((candidate) => !sameReceipt(candidate, receipt));
|
|
374
|
+
if (completed && !state.settled.some((candidate) => sameReceipt(candidate, receipt)))
|
|
375
|
+
state.settled.push(receipt);
|
|
376
|
+
if (completed)
|
|
377
|
+
this.write(state);
|
|
378
|
+
}
|
|
379
|
+
commit(update) {
|
|
380
|
+
const state = this.read();
|
|
381
|
+
const receipt = updateReceipt(update);
|
|
382
|
+
const settled = state.settled.filter((candidate) => !sameReceipt(candidate, receipt));
|
|
383
|
+
if (settled.length === state.settled.length)
|
|
384
|
+
return;
|
|
385
|
+
state.settled = settled;
|
|
386
|
+
this.write(state);
|
|
387
|
+
}
|
|
388
|
+
captureAdmittedWork(work) {
|
|
389
|
+
if (!this.validAdmittedWork(work))
|
|
390
|
+
throw new Error("Telegram admitted work is invalid");
|
|
391
|
+
const state = this.read();
|
|
392
|
+
const existing = [
|
|
393
|
+
...state.admittedPending,
|
|
394
|
+
...state.admittedDispatching,
|
|
395
|
+
...state.admittedSettled,
|
|
396
|
+
...state.admittedIndeterminate,
|
|
397
|
+
].find((candidate) => candidate.admissionId === work.admissionId);
|
|
398
|
+
if (existing) {
|
|
399
|
+
const { quarantinedAt: _quarantinedAt, ...canonical } = existing;
|
|
400
|
+
if (JSON.stringify(canonical) !== JSON.stringify(work))
|
|
401
|
+
throw new Error("Telegram admitted work has a conflicting receipt");
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
state.admittedPending.push(structuredClone(work));
|
|
405
|
+
this.write(state);
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
loadPendingAdmittedWork() {
|
|
409
|
+
return structuredClone(this.read().admittedPending);
|
|
410
|
+
}
|
|
411
|
+
claimAdmittedWork(admissionId) {
|
|
412
|
+
if (!TELEGRAM_ADMISSION_ID.test(admissionId))
|
|
413
|
+
throw new Error("Telegram admission id is invalid");
|
|
414
|
+
const state = this.read();
|
|
415
|
+
if (state.admittedDispatching.some((candidate) => candidate.admissionId === admissionId)
|
|
416
|
+
|| state.admittedSettled.some((candidate) => candidate.admissionId === admissionId)
|
|
417
|
+
|| state.admittedIndeterminate.some((candidate) => candidate.admissionId === admissionId))
|
|
418
|
+
return null;
|
|
419
|
+
const index = state.admittedPending.findIndex((candidate) => candidate.admissionId === admissionId);
|
|
420
|
+
if (index < 0)
|
|
421
|
+
return null;
|
|
422
|
+
const [work] = state.admittedPending.splice(index, 1);
|
|
423
|
+
state.admittedDispatching.push(work);
|
|
424
|
+
this.write(state);
|
|
425
|
+
return structuredClone(work);
|
|
426
|
+
}
|
|
427
|
+
completeAdmittedWork(admissionId) {
|
|
428
|
+
if (!TELEGRAM_ADMISSION_ID.test(admissionId))
|
|
429
|
+
throw new Error("Telegram admission id is invalid");
|
|
430
|
+
const state = this.read();
|
|
431
|
+
const index = state.admittedDispatching.findIndex((candidate) => candidate.admissionId === admissionId);
|
|
432
|
+
if (index < 0)
|
|
433
|
+
return;
|
|
434
|
+
const [work] = state.admittedDispatching.splice(index, 1);
|
|
435
|
+
state.admittedSettled.push(work);
|
|
436
|
+
state.admittedSettled = state.admittedSettled.slice(-this.maxIndeterminateReceipts);
|
|
437
|
+
this.write(state);
|
|
438
|
+
}
|
|
439
|
+
quarantineStrandedAdmittedWork() {
|
|
440
|
+
const state = this.read();
|
|
441
|
+
if (state.admittedDispatching.length === 0)
|
|
442
|
+
return [];
|
|
443
|
+
const timestamp = this.timestamp();
|
|
444
|
+
const stranded = state.admittedDispatching.map((work) => ({ ...work, quarantinedAt: timestamp }));
|
|
445
|
+
state.admittedDispatching = [];
|
|
446
|
+
state.admittedIndeterminate = [...state.admittedIndeterminate, ...stranded]
|
|
447
|
+
.filter((work) => work.quarantinedAt >= timestamp - this.indeterminateRetentionMs)
|
|
448
|
+
.sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.admissionId.localeCompare(right.admissionId))
|
|
449
|
+
.slice(-this.maxIndeterminateReceipts);
|
|
450
|
+
this.write(state);
|
|
451
|
+
return stranded.map(({ quarantinedAt: _quarantinedAt, ...work }) => work);
|
|
452
|
+
}
|
|
453
|
+
admittedWorkState(admissionId) {
|
|
454
|
+
if (!TELEGRAM_ADMISSION_ID.test(admissionId))
|
|
455
|
+
throw new Error("Telegram admission id is invalid");
|
|
456
|
+
const state = this.read();
|
|
457
|
+
if (state.admittedPending.some((work) => work.admissionId === admissionId))
|
|
458
|
+
return "pending";
|
|
459
|
+
if (state.admittedDispatching.some((work) => work.admissionId === admissionId))
|
|
460
|
+
return "dispatching";
|
|
461
|
+
if (state.admittedSettled.some((work) => work.admissionId === admissionId))
|
|
462
|
+
return "settled";
|
|
463
|
+
if (state.admittedIndeterminate.some((work) => work.admissionId === admissionId))
|
|
464
|
+
return "indeterminate";
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
validAdmittedWork(work) {
|
|
468
|
+
return Boolean(work) && typeof work === "object" && !Array.isArray(work)
|
|
469
|
+
&& Object.keys(work).sort().join(",") === "admissionId,eventId,friendId,reference,sessionKey"
|
|
470
|
+
&& TELEGRAM_ADMISSION_ID.test(work.admissionId)
|
|
471
|
+
&& boundedTelegramWorkText(work.friendId, 512)
|
|
472
|
+
&& boundedTelegramWorkText(work.sessionKey, 1_024)
|
|
473
|
+
&& TELEGRAM_SESSION_EVENT_ID.test(work.eventId)
|
|
474
|
+
&& work.reference === `telegram-admission:${work.admissionId}`;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
exports.FileTelegramUpdateInboxStore = FileTelegramUpdateInboxStore;
|
|
478
|
+
// A poll failure is retryable when it is a transient transport condition: a
|
|
479
|
+
// Telegram Bot API error (network/HTTP), or an authority-socket error flagged
|
|
480
|
+
// transient by the resident's socket client (a dropped or refused connection).
|
|
481
|
+
// Dispatch and durable-audit invariant failures are not transport conditions;
|
|
482
|
+
// they must surface rather than be spun on indefinitely.
|
|
483
|
+
function isRetryablePollError(error) {
|
|
484
|
+
if (error instanceof TelegramApiError)
|
|
485
|
+
return true;
|
|
486
|
+
return error instanceof Error
|
|
487
|
+
&& error.isTransientTransportError === true;
|
|
488
|
+
}
|
|
489
|
+
function createTelegramLongPoll(options) {
|
|
490
|
+
let nextUpdateId = options.offsetStore.load();
|
|
491
|
+
const shutdown = new AbortController();
|
|
492
|
+
const retryAfterPollError = (signal, attempt) => new Promise((resolve) => {
|
|
493
|
+
const delay = Math.min(TELEGRAM_POLL_RETRY_BASE_MS * 2 ** Math.max(0, attempt - 1), TELEGRAM_POLL_RETRY_MAX_MS);
|
|
494
|
+
const timer = setTimeout(resolve, delay);
|
|
495
|
+
signal.addEventListener("abort", () => {
|
|
496
|
+
clearTimeout(timer);
|
|
497
|
+
resolve();
|
|
498
|
+
}, { once: true });
|
|
499
|
+
});
|
|
500
|
+
const inboundAttachments = (message) => {
|
|
501
|
+
const result = [];
|
|
502
|
+
const push = (media, kind, fallbackName, fallbackMimeType) => {
|
|
503
|
+
if (typeof media?.file_id !== "string" || !media.file_id.trim())
|
|
504
|
+
return;
|
|
505
|
+
result.push({
|
|
506
|
+
fileId: media.file_id,
|
|
507
|
+
kind,
|
|
508
|
+
displayName: media.file_name?.trim() || fallbackName,
|
|
509
|
+
...((media.mime_type?.trim() || fallbackMimeType) ? { mimeType: media.mime_type?.trim().toLowerCase() || fallbackMimeType } : {}),
|
|
510
|
+
...(Number.isSafeInteger(media.file_size) ? { byteCount: media.file_size } : {}),
|
|
511
|
+
});
|
|
512
|
+
};
|
|
513
|
+
const photo = message.photo?.filter((candidate) => typeof candidate.file_id === "string" && candidate.file_id.trim())
|
|
514
|
+
.sort((left, right) => (left.file_size ?? left.width ?? 0) - (right.file_size ?? right.width ?? 0)).at(-1);
|
|
515
|
+
if (photo)
|
|
516
|
+
result.push({ fileId: photo.file_id, kind: "image", displayName: "telegram-photo.jpg", mimeType: "image/jpeg", ...(Number.isSafeInteger(photo.file_size) ? { byteCount: photo.file_size } : {}) });
|
|
517
|
+
push(message.document, "document", "telegram-document");
|
|
518
|
+
push(message.audio, "audio", "telegram-audio", "audio/mpeg");
|
|
519
|
+
push(message.voice, "audio", "telegram-voice.ogg", "audio/ogg");
|
|
520
|
+
push(message.video, "binary", "telegram-video.mp4", "video/mp4");
|
|
521
|
+
push(message.animation, "binary", "telegram-animation.mp4", "video/mp4");
|
|
522
|
+
push(message.sticker, message.sticker?.is_animated || message.sticker?.is_video ? "binary" : "image", message.sticker?.is_animated ? "telegram-sticker.tgs" : message.sticker?.is_video ? "telegram-sticker.webm" : "telegram-sticker.webp", message.sticker?.is_animated ? "application/x-tgsticker" : message.sticker?.is_video ? "video/webm" : "image/webp");
|
|
523
|
+
return result;
|
|
524
|
+
};
|
|
525
|
+
const rawAttachmentCount = (message) => [
|
|
526
|
+
message.document, message.audio, message.video, message.voice, message.animation, message.sticker,
|
|
527
|
+
].filter(Boolean).length + (message.photo?.length ? 1 : 0);
|
|
528
|
+
const authorizedMessage = (update, authority) => {
|
|
529
|
+
const message = update.message;
|
|
530
|
+
const userId = message?.from ? String(message.from.id) : "";
|
|
531
|
+
const chatId = message ? String(message.chat.id) : "";
|
|
532
|
+
if (!message || message.chat.type !== "private" || userId !== options.expectedUserId || chatId !== options.expectedChatId)
|
|
533
|
+
return null;
|
|
534
|
+
const attachments = inboundAttachments(message);
|
|
535
|
+
const attachmentCount = rawAttachmentCount(message);
|
|
536
|
+
const attachmentNotices = attachmentCount > attachments.length
|
|
537
|
+
? ["attachment unavailable: Telegram media metadata was incomplete"]
|
|
538
|
+
: [];
|
|
539
|
+
const text = message.text ?? message.caption ?? "";
|
|
540
|
+
if (typeof text !== "string" || (!text && attachmentCount === 0))
|
|
541
|
+
return null;
|
|
542
|
+
return {
|
|
543
|
+
updateId: update.update_id,
|
|
544
|
+
messageId: String(message.message_id),
|
|
545
|
+
userId,
|
|
546
|
+
chatId,
|
|
547
|
+
text,
|
|
548
|
+
attachments,
|
|
549
|
+
...(attachmentNotices.length ? { attachmentNotices } : {}),
|
|
550
|
+
...(Number.isSafeInteger(message.reply_to_message?.message_id) && message.reply_to_message.message_id > 0
|
|
551
|
+
? { replyToMessageId: String(message.reply_to_message.message_id) }
|
|
552
|
+
: {}),
|
|
553
|
+
...(authority ? { authority } : {}),
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
const unknownMessage = (update, authority) => {
|
|
557
|
+
const message = update.message;
|
|
558
|
+
if (!options.onUnknownMessage || !options.botId || !message?.from || message.chat.type !== "private")
|
|
559
|
+
return null;
|
|
560
|
+
const userId = String(message.from.id);
|
|
561
|
+
const chatId = String(message.chat.id);
|
|
562
|
+
if (userId === options.expectedUserId && chatId === options.expectedChatId)
|
|
563
|
+
return null;
|
|
564
|
+
const displayLabel = [message.from.first_name, message.from.last_name].filter(Boolean).join(" ")
|
|
565
|
+
|| (message.from.username ? `@${message.from.username}` : `Telegram user ${userId}`);
|
|
566
|
+
const attachments = inboundAttachments(message);
|
|
567
|
+
const attachmentCount = rawAttachmentCount(message);
|
|
568
|
+
return {
|
|
569
|
+
updateId: update.update_id,
|
|
570
|
+
messageId: message.message_id,
|
|
571
|
+
botId: options.botId,
|
|
572
|
+
userId,
|
|
573
|
+
chatId,
|
|
574
|
+
text: message.text ?? message.caption ?? "",
|
|
575
|
+
displayLabel,
|
|
576
|
+
hasAttachments: attachmentCount > 0,
|
|
577
|
+
attachments,
|
|
578
|
+
...(attachmentCount > attachments.length ? { attachmentNotices: ["attachment unavailable: Telegram media metadata was incomplete"] } : {}),
|
|
579
|
+
...(authority ? { authority } : {}),
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
const authorizedCallback = (update) => {
|
|
583
|
+
const callback = update.callback_query;
|
|
584
|
+
return Boolean(callback?.message
|
|
585
|
+
&& String(callback.from.id) === options.expectedUserId
|
|
586
|
+
&& String(callback.message.chat.id) === options.expectedChatId);
|
|
587
|
+
};
|
|
588
|
+
const dispatch = async (update) => {
|
|
589
|
+
const authority = options.transportMetadata?.(update) ?? undefined;
|
|
590
|
+
const handled = !update.callback_query || authorizedCallback(update)
|
|
591
|
+
? await options.onUpdate?.(update, authority) ?? false
|
|
592
|
+
: false;
|
|
593
|
+
if (handled)
|
|
594
|
+
return;
|
|
595
|
+
const message = authorizedMessage(update, authority);
|
|
596
|
+
if (message) {
|
|
597
|
+
await options.onMessage(message);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const stranger = unknownMessage(update, authority);
|
|
601
|
+
if (stranger) {
|
|
602
|
+
await options.onUnknownMessage(stranger);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const distinctAccount = Boolean(update.message?.from && String(update.message.from.id) !== options.expectedUserId);
|
|
606
|
+
(0, runtime_1.emitNervesEvent)({
|
|
607
|
+
component: "senses",
|
|
608
|
+
event: "telegram.update_dropped",
|
|
609
|
+
message: "Telegram update dropped before dispatch",
|
|
610
|
+
meta: {
|
|
611
|
+
updateClass: update.message ? "message" : "other",
|
|
612
|
+
reason: "unauthorized_or_unsupported",
|
|
613
|
+
distinctAccount,
|
|
614
|
+
...options.acceptanceEventMeta?.(update, distinctAccount),
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
const pollOnce = async (signal) => {
|
|
619
|
+
const requestSignal = signal ? AbortSignal.any([shutdown.signal, signal]) : shutdown.signal;
|
|
620
|
+
if (requestSignal.aborted)
|
|
621
|
+
throw new Error("Telegram long poll stopped");
|
|
622
|
+
options.onBeforeDispatch?.();
|
|
623
|
+
const stranded = options.inboxStore?.quarantineStranded?.() ?? options.inboxStore?.loadIndeterminate() ?? [];
|
|
624
|
+
for (const indeterminate of stranded) {
|
|
625
|
+
(0, runtime_1.emitNervesEvent)({
|
|
626
|
+
level: "warn",
|
|
627
|
+
component: "senses",
|
|
628
|
+
event: "telegram.update_dropped",
|
|
629
|
+
message: "Telegram update dispatch outcome is indeterminate after restart",
|
|
630
|
+
meta: { updateClass: indeterminate.updateClass, reason: "dispatch_indeterminate", ...options.acceptanceEventMeta?.() },
|
|
631
|
+
});
|
|
632
|
+
options.onDispatchSettled?.();
|
|
633
|
+
options.inboxStore?.acknowledgeIndeterminateWarning(indeterminate);
|
|
634
|
+
}
|
|
635
|
+
const updates = await options.api.request("getUpdates", {
|
|
636
|
+
offset: nextUpdateId,
|
|
637
|
+
timeout: 50,
|
|
638
|
+
allowed_updates: ["message", "callback_query"],
|
|
639
|
+
}, requestSignal);
|
|
640
|
+
if (!Array.isArray(updates))
|
|
641
|
+
throw new Error("Telegram getUpdates result must be an array");
|
|
642
|
+
for (const update of updates) {
|
|
643
|
+
if (!update || !Number.isSafeInteger(update.update_id) || (update.update_id < nextUpdateId && !options.settleTransport))
|
|
644
|
+
continue;
|
|
645
|
+
options.onBeforeDispatch?.();
|
|
646
|
+
const next = Math.max(nextUpdateId, update.update_id + 1);
|
|
647
|
+
const requiresDurableDispatch = Boolean(authorizedCallback(update) || authorizedMessage(update) || unknownMessage(update));
|
|
648
|
+
const newlyCaptured = requiresDurableDispatch ? (options.inboxStore?.capture(update) ?? true) : true;
|
|
649
|
+
if (newlyCaptured) {
|
|
650
|
+
if (requiresDurableDispatch && options.inboxStore && !options.inboxStore.claim(update)) {
|
|
651
|
+
options.onDispatchSettled?.();
|
|
652
|
+
const indeterminate = options.inboxStore.loadIndeterminate()
|
|
653
|
+
.some((receipt) => sameReceipt(receipt, updateReceipt(update)));
|
|
654
|
+
await options.settleTransport?.(update, indeterminate ? "indeterminate" : "completed");
|
|
655
|
+
options.offsetStore.save(next);
|
|
656
|
+
nextUpdateId = next;
|
|
657
|
+
options.inboxStore.commit?.(update);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
let dispatchError;
|
|
661
|
+
try {
|
|
662
|
+
await dispatch(update);
|
|
663
|
+
if (requiresDurableDispatch)
|
|
664
|
+
options.inboxStore?.complete(update);
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
options.inboxStore?.quarantineStranded?.();
|
|
668
|
+
dispatchError = error;
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
options.onDispatchSettled?.();
|
|
672
|
+
}
|
|
673
|
+
catch (auditError) {
|
|
674
|
+
if (dispatchError !== undefined)
|
|
675
|
+
throw new AggregateError([dispatchError, auditError], "Telegram dispatch and acceptance audit verification failed");
|
|
676
|
+
throw auditError;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
if (options.settleTransport) {
|
|
680
|
+
await options.settleTransport?.(update, dispatchError === undefined ? "completed" : "indeterminate");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
catch (settlementError) {
|
|
684
|
+
if (dispatchError !== undefined)
|
|
685
|
+
throw new AggregateError([dispatchError, settlementError], "Telegram dispatch and transport settlement failed");
|
|
686
|
+
throw settlementError;
|
|
687
|
+
}
|
|
688
|
+
if (dispatchError !== undefined)
|
|
689
|
+
throw dispatchError;
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
options.onDispatchSettled?.();
|
|
693
|
+
if (options.settleTransport) {
|
|
694
|
+
// Only an existing inbox can report that this update was already captured.
|
|
695
|
+
const indeterminate = options.inboxStore.loadIndeterminate()
|
|
696
|
+
.some((receipt) => sameReceipt(receipt, updateReceipt(update)));
|
|
697
|
+
await options.settleTransport(update, indeterminate ? "indeterminate" : "completed");
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
options.offsetStore.save(next);
|
|
701
|
+
nextUpdateId = next;
|
|
702
|
+
if (requiresDurableDispatch)
|
|
703
|
+
options.inboxStore?.commit?.(update);
|
|
704
|
+
}
|
|
705
|
+
return nextUpdateId;
|
|
706
|
+
};
|
|
707
|
+
return {
|
|
708
|
+
pollOnce,
|
|
709
|
+
async run(signal) {
|
|
710
|
+
const runSignal = signal ? AbortSignal.any([shutdown.signal, signal]) : shutdown.signal;
|
|
711
|
+
let consecutiveFailures = 0;
|
|
712
|
+
while (!runSignal.aborted) {
|
|
713
|
+
try {
|
|
714
|
+
await pollOnce(runSignal);
|
|
715
|
+
consecutiveFailures = 0;
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
if (shutdown.signal.aborted || signal?.aborted)
|
|
719
|
+
return;
|
|
720
|
+
// Only transient transport failures are retried here: a Telegram API
|
|
721
|
+
// error, or a dropped/again-failing authority socket in the resident.
|
|
722
|
+
// Exiting the process on those (as an unhandled throw would) only
|
|
723
|
+
// trades an in-process retry for a heavier container/daemon restart
|
|
724
|
+
// cycle, which is what produced the observed ~10s crash-loops. A
|
|
725
|
+
// non-transport failure (a dispatch or durable-audit invariant) still
|
|
726
|
+
// surfaces so it is not silently spun on.
|
|
727
|
+
if (!isRetryablePollError(error))
|
|
728
|
+
throw error;
|
|
729
|
+
consecutiveFailures += 1;
|
|
730
|
+
(0, runtime_1.emitNervesEvent)({
|
|
731
|
+
level: "warn",
|
|
732
|
+
component: "senses",
|
|
733
|
+
event: "telegram.poll_retry",
|
|
734
|
+
message: "Telegram long poll request failed; retrying",
|
|
735
|
+
meta: {
|
|
736
|
+
attempt: consecutiveFailures,
|
|
737
|
+
...(error instanceof TelegramApiError ? { status: error.status, errorCode: error.errorCode } : {}),
|
|
738
|
+
reason: error.message,
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
await retryAfterPollError(runSignal, consecutiveFailures);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
stop() {
|
|
746
|
+
shutdown.abort(new Error("Telegram long poll stopped"));
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function classifyTelegramPersistedApprovalState(pending) {
|
|
751
|
+
const hasAttempt = pending.decisionAttempt !== undefined;
|
|
752
|
+
const hasTerminal = pending.terminal !== undefined;
|
|
753
|
+
const hasTerminalMac = pending.terminalMac !== undefined;
|
|
754
|
+
const hasReceipt = pending.settlementReceipt !== undefined;
|
|
755
|
+
const hasAuthority = hasAttempt || hasTerminalMac || hasReceipt;
|
|
756
|
+
const hasTombstoneState = pending.terminalizedAt !== undefined
|
|
757
|
+
|| pending.tombstoneExpiresAt !== undefined
|
|
758
|
+
|| pending.tombstoneMac !== undefined
|
|
759
|
+
|| pending.staleTap !== undefined;
|
|
760
|
+
const hasCompleteTombstoneState = pending.terminalizedAt !== undefined
|
|
761
|
+
&& pending.tombstoneExpiresAt !== undefined
|
|
762
|
+
&& pending.tombstoneMac !== undefined
|
|
763
|
+
&& pending.expiryObservation !== undefined;
|
|
764
|
+
const hasExclusiveTerminalState = pending.expiryObservation !== undefined || hasTombstoneState;
|
|
765
|
+
const messageIdNumber = typeof pending.messageId === "string" ? Number(pending.messageId) : Number.NaN;
|
|
766
|
+
const hasCanonicalMessageId = typeof pending.messageId === "string"
|
|
767
|
+
&& /^[1-9][0-9]*$/u.test(pending.messageId)
|
|
768
|
+
&& Number.isSafeInteger(messageIdNumber)
|
|
769
|
+
&& String(messageIdNumber) === pending.messageId;
|
|
770
|
+
const validTerminal = hasTerminal
|
|
771
|
+
&& typeof pending.terminal.accepted === "boolean"
|
|
772
|
+
&& typeof pending.terminal.terminalText === "string"
|
|
773
|
+
&& pending.terminal.terminalText.length > 0
|
|
774
|
+
&& pending.terminal.terminalText.length <= 4_096;
|
|
775
|
+
const exactDeliveryInterruption = pending.terminalKind === "delivery_interruption"
|
|
776
|
+
&& pending.deliveryState === "delivery_indeterminate"
|
|
777
|
+
&& pending.messageId === null
|
|
778
|
+
&& validTerminal
|
|
779
|
+
&& pending.terminal.accepted === false
|
|
780
|
+
&& !hasAuthority
|
|
781
|
+
&& !hasExclusiveTerminalState;
|
|
782
|
+
if (pending.terminalKind !== undefined) {
|
|
783
|
+
if (!exactDeliveryInterruption)
|
|
784
|
+
throw new Error("Telegram persisted delivery-interruption terminal state is invalid");
|
|
785
|
+
return "delivery_interruption";
|
|
786
|
+
}
|
|
787
|
+
if (hasAuthority && hasExclusiveTerminalState) {
|
|
788
|
+
throw new Error("Telegram persisted action authority cannot coexist with expiry or tombstone state");
|
|
789
|
+
}
|
|
790
|
+
if (pending.deliveryState === "terminal_tombstone") {
|
|
791
|
+
if (!hasCompleteTombstoneState)
|
|
792
|
+
throw new Error("Telegram persisted terminal tombstone is structurally incomplete");
|
|
793
|
+
}
|
|
794
|
+
else if (hasTombstoneState) {
|
|
795
|
+
throw new Error("Telegram persisted tombstone state has invalid delivery routing");
|
|
796
|
+
}
|
|
797
|
+
if (hasAttempt && (pending.deliveryState !== "bound" || !hasCanonicalMessageId)) {
|
|
798
|
+
throw new Error("Telegram persisted decision attempt has invalid delivery routing");
|
|
799
|
+
}
|
|
800
|
+
if (hasReceipt && (!hasAttempt || !hasTerminal || !hasTerminalMac)) {
|
|
801
|
+
throw new Error("Telegram persisted settlement receipt is structurally incomplete");
|
|
802
|
+
}
|
|
803
|
+
if (hasTerminalMac && (!hasAttempt || !hasTerminal)) {
|
|
804
|
+
throw new Error("Telegram persisted terminal authentication is structurally incomplete");
|
|
805
|
+
}
|
|
806
|
+
if (hasTerminal) {
|
|
807
|
+
if (!validTerminal || !hasAttempt || !hasTerminalMac) {
|
|
808
|
+
throw new Error("Telegram persisted action terminal state is structurally incomplete");
|
|
809
|
+
}
|
|
810
|
+
return "action_terminal";
|
|
811
|
+
}
|
|
812
|
+
if (pending.deliveryState === "terminal_tombstone")
|
|
813
|
+
return "terminal_tombstone";
|
|
814
|
+
if (pending.expiryObservation !== undefined) {
|
|
815
|
+
if (pending.deliveryState !== "bound" || !hasCanonicalMessageId) {
|
|
816
|
+
throw new Error("Telegram persisted expiry observation has invalid delivery routing");
|
|
817
|
+
}
|
|
818
|
+
return "expiry_observed";
|
|
819
|
+
}
|
|
820
|
+
return hasAttempt ? "decision_attempt" : "ordinary";
|
|
821
|
+
}
|
|
822
|
+
class FileTelegramPendingApprovalStore {
|
|
823
|
+
path;
|
|
824
|
+
constructor(path) {
|
|
825
|
+
this.path = path;
|
|
826
|
+
}
|
|
827
|
+
load() {
|
|
828
|
+
try {
|
|
829
|
+
const value = JSON.parse((0, node_fs_1.readFileSync)(this.path, "utf8"));
|
|
830
|
+
if (!Array.isArray(value))
|
|
831
|
+
throw new Error("pending approvals must be an array");
|
|
832
|
+
return structuredClone(value);
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
836
|
+
return [];
|
|
837
|
+
throw new Error("Telegram pending approval state is corrupt", { cause: error });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
save(records) {
|
|
841
|
+
durableAtomicWrite(this.path, `${JSON.stringify(records)}\n`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
exports.FileTelegramPendingApprovalStore = FileTelegramPendingApprovalStore;
|
|
845
|
+
exports.TELEGRAM_APPROVAL_TTL_MS = 300_000;
|
|
846
|
+
exports.TELEGRAM_APPROVAL_EXPIRED_TEXT = "⚠️ Approval expired";
|
|
847
|
+
exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS = 30_000;
|
|
848
|
+
exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS = 600_000;
|
|
849
|
+
function assertTelegramCallbackData(value) {
|
|
850
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
851
|
+
if (bytes < 1 || bytes > 64)
|
|
852
|
+
throw new Error("Telegram callback_data must be 1 to 64 bytes");
|
|
853
|
+
}
|
|
854
|
+
function createTelegramApprovalTransport(options) {
|
|
855
|
+
const now = options.now ?? Date.now;
|
|
856
|
+
const effectBarrier = options.effectBarrier ?? (() => undefined);
|
|
857
|
+
const pendingByCallback = new Map();
|
|
858
|
+
const approvalOperations = new Map();
|
|
859
|
+
const uniquePending = () => [...new Set(pendingByCallback.values())];
|
|
860
|
+
const persist = () => {
|
|
861
|
+
effectBarrier();
|
|
862
|
+
options.pendingStore.save(uniquePending().map(({ decisionToken: _secret, ...record }) => record));
|
|
863
|
+
};
|
|
864
|
+
const persistMutation = (pending, mutate) => {
|
|
865
|
+
const before = structuredClone(pending);
|
|
866
|
+
try {
|
|
867
|
+
mutate();
|
|
868
|
+
persist();
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
for (const key of Object.keys(pending))
|
|
872
|
+
delete pending[key];
|
|
873
|
+
Object.assign(pending, before);
|
|
874
|
+
throw error;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
const add = (pending) => {
|
|
878
|
+
pendingByCallback.set(pending.approveCallbackData, pending);
|
|
879
|
+
pendingByCallback.set(pending.denyCallbackData, pending);
|
|
880
|
+
};
|
|
881
|
+
const remove = (pending) => {
|
|
882
|
+
pendingByCallback.delete(pending.approveCallbackData);
|
|
883
|
+
pendingByCallback.delete(pending.denyCallbackData);
|
|
884
|
+
};
|
|
885
|
+
const persistRemoval = (pending) => {
|
|
886
|
+
options.pendingStore.save(uniquePending().filter((record) => record !== pending).map(({ decisionToken: _secret, ...record }) => record));
|
|
887
|
+
remove(pending);
|
|
888
|
+
};
|
|
889
|
+
const acceptanceEvidenceUnsigned = (pending, fields) => ({ ...options.acceptanceEventMeta?.(), approvalId: pending.approvalId, ...pending.acceptanceBinding, ...fields });
|
|
890
|
+
const emitAcceptanceEvidence = (event, pending, fields) => {
|
|
891
|
+
if (!pending.acceptanceBinding)
|
|
892
|
+
return;
|
|
893
|
+
if (!options.signAcceptanceEvidence)
|
|
894
|
+
throw new Error("Telegram acceptance evidence signer is unavailable");
|
|
895
|
+
const unsigned = acceptanceEvidenceUnsigned(pending, fields);
|
|
896
|
+
const meta = { ...unsigned, evidenceMac: options.signAcceptanceEvidence(event, unsigned) };
|
|
897
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event, message: "Telegram approval acceptance evidence recorded", meta });
|
|
898
|
+
options.onAcceptanceEvidence?.(event, meta);
|
|
899
|
+
};
|
|
900
|
+
const withApprovalExclusive = async (approvalId, wait, operation) => {
|
|
901
|
+
while (approvalOperations.has(approvalId)) {
|
|
902
|
+
if (!wait)
|
|
903
|
+
return undefined;
|
|
904
|
+
await approvalOperations.get(approvalId).done;
|
|
905
|
+
}
|
|
906
|
+
let release;
|
|
907
|
+
const done = new Promise((resolve) => { release = resolve; });
|
|
908
|
+
const claim = { done, release };
|
|
909
|
+
approvalOperations.set(approvalId, claim);
|
|
910
|
+
try {
|
|
911
|
+
return await operation();
|
|
912
|
+
}
|
|
913
|
+
finally {
|
|
914
|
+
approvalOperations.delete(approvalId);
|
|
915
|
+
release();
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
const expiryObservationFields = (pending, observedAt) => ({
|
|
919
|
+
expiryObservationSchemaVersion: "telegram-approval-expiry-observation-v1",
|
|
920
|
+
expiryDeadlineAt: pending.expiresAt,
|
|
921
|
+
expiryObservedAt: observedAt,
|
|
922
|
+
});
|
|
923
|
+
const terminalTombstoneFields = (pending) => ({
|
|
924
|
+
...expiryObservationFields(pending, pending.expiryObservation.observedAt),
|
|
925
|
+
terminalizedAt: pending.terminalizedAt,
|
|
926
|
+
tombstoneExpiresAt: pending.tombstoneExpiresAt,
|
|
927
|
+
});
|
|
928
|
+
const staleTapFields = (pending, staleTap) => ({
|
|
929
|
+
...terminalTombstoneFields(pending),
|
|
930
|
+
staleTapSchemaVersion: staleTap.schemaVersion,
|
|
931
|
+
staleTapState: staleTap.state,
|
|
932
|
+
queryIdDigest: staleTap.queryIdDigest,
|
|
933
|
+
attemptedAt: staleTap.attemptedAt,
|
|
934
|
+
consumedAt: staleTap.consumedAt,
|
|
935
|
+
});
|
|
936
|
+
const signState = (event, pending, fields) => {
|
|
937
|
+
const evidenceMac = options.signAcceptanceEvidence?.(event, acceptanceEvidenceUnsigned(pending, fields)) ?? null;
|
|
938
|
+
return evidenceMac;
|
|
939
|
+
};
|
|
940
|
+
const decisionAttemptPayload = (pending, attempt) => ({
|
|
941
|
+
approvalId: pending.approvalId,
|
|
942
|
+
messageId: pending.messageId,
|
|
943
|
+
expiresAt: pending.expiresAt,
|
|
944
|
+
approveCallbackData: pending.approveCallbackData,
|
|
945
|
+
denyCallbackData: pending.denyCallbackData,
|
|
946
|
+
acceptanceBinding: pending.acceptanceBinding,
|
|
947
|
+
schemaVersion: attempt.schemaVersion,
|
|
948
|
+
decision: attempt.decision,
|
|
949
|
+
queryIdDigest: attempt.queryIdDigest,
|
|
950
|
+
attemptedAt: attempt.attemptedAt,
|
|
951
|
+
});
|
|
952
|
+
const decisionAttemptMac = (decisionToken, pending, attempt) => (0, node_crypto_1.createHmac)("sha256", decisionToken).update(JSON.stringify(decisionAttemptPayload(pending, attempt))).digest("hex");
|
|
953
|
+
const resolveDecisionToken = async (pending) => {
|
|
954
|
+
const decisionToken = pending.decisionToken ?? await options.resolveDecisionToken?.(pending.approvalId);
|
|
955
|
+
if (!decisionToken)
|
|
956
|
+
throw new Error("Telegram approval restart requires a decision token resolver");
|
|
957
|
+
return decisionToken;
|
|
958
|
+
};
|
|
959
|
+
const validateDecisionAttempt = async (pending, expected) => {
|
|
960
|
+
const attempt = pending.decisionAttempt;
|
|
961
|
+
const lowerBound = pending.acceptanceBinding?.boundAt ?? pending.expiresAt - exports.TELEGRAM_APPROVAL_TTL_MS;
|
|
962
|
+
const unsigned = { schemaVersion: attempt.schemaVersion, decision: attempt.decision, queryIdDigest: attempt.queryIdDigest, attemptedAt: attempt.attemptedAt };
|
|
963
|
+
const token = pending.decisionToken ?? await options.resolveDecisionToken?.(pending.approvalId);
|
|
964
|
+
const expectedMac = token ? decisionAttemptMac(token, pending, unsigned) : null;
|
|
965
|
+
const suppliedMac = typeof attempt.evidenceMac === "string" ? Buffer.from(attempt.evidenceMac, "hex") : Buffer.alloc(0);
|
|
966
|
+
const calculatedMac = expectedMac ? Buffer.from(expectedMac, "hex") : Buffer.alloc(0);
|
|
967
|
+
const tokenMacValid = calculatedMac.length > 0 && suppliedMac.length === calculatedMac.length && (0, node_crypto_1.timingSafeEqual)(suppliedMac, calculatedMac);
|
|
968
|
+
const recoveryMacValid = typeof attempt.recoveryMac === "string"
|
|
969
|
+
&& options.signAcceptanceEvidence !== undefined
|
|
970
|
+
&& attempt.recoveryMac === options.signAcceptanceEvidence("telegram.approval_decision_attempt_recovery_state", acceptanceEvidenceUnsigned(pending, decisionAttemptPayload(pending, unsigned)));
|
|
971
|
+
if (attempt.schemaVersion !== "telegram-approval-decision-attempt-v1"
|
|
972
|
+
|| !["approve", "deny"].includes(attempt.decision)
|
|
973
|
+
|| !/^[0-9a-f]{64}$/u.test(attempt.queryIdDigest)
|
|
974
|
+
|| !Number.isSafeInteger(attempt.attemptedAt) || attempt.attemptedAt < lowerBound || attempt.attemptedAt >= pending.expiresAt
|
|
975
|
+
|| (!tokenMacValid && !recoveryMacValid)
|
|
976
|
+
|| (expected !== undefined && (attempt.decision !== expected.decision || attempt.queryIdDigest !== expected.queryIdDigest))) {
|
|
977
|
+
throw new Error("Telegram persisted decision attempt is invalid");
|
|
978
|
+
}
|
|
979
|
+
return attempt;
|
|
980
|
+
};
|
|
981
|
+
const settlementReceiptFields = (receipt) => ({
|
|
982
|
+
settlementReceiptSchemaVersion: receipt.schemaVersion,
|
|
983
|
+
settlementKind: receipt.kind,
|
|
984
|
+
callbackAt: receipt.callbackAt,
|
|
985
|
+
accepted: receipt.accepted,
|
|
986
|
+
reason: receipt.reason,
|
|
987
|
+
acknowledgementState: receipt.acknowledgementState,
|
|
988
|
+
recoveredAt: receipt.recoveredAt ?? null,
|
|
989
|
+
decisionAttemptDigest: receipt.decisionAttemptDigest,
|
|
990
|
+
});
|
|
991
|
+
const terminalOutcomeFields = (pending) => ({
|
|
992
|
+
accepted: pending.terminal?.accepted,
|
|
993
|
+
terminalText: pending.terminal?.terminalText,
|
|
994
|
+
decisionAttemptDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(pending.decisionAttempt)).digest("hex"),
|
|
995
|
+
});
|
|
996
|
+
const validateTerminalOutcome = (pending) => {
|
|
997
|
+
if (!pending.terminal || typeof pending.terminal.accepted !== "boolean" || typeof pending.terminal.terminalText !== "string"
|
|
998
|
+
|| pending.terminal.terminalText.length === 0 || pending.terminal.terminalText.length > 4_096
|
|
999
|
+
|| pending.terminalMac !== signState("telegram.approval_terminal_outcome_state", pending, terminalOutcomeFields(pending))) {
|
|
1000
|
+
throw new Error("Telegram persisted terminal outcome is invalid");
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
const validateSettlementReceipt = (pending) => {
|
|
1004
|
+
const receipt = pending.settlementReceipt;
|
|
1005
|
+
validateTerminalOutcome(pending);
|
|
1006
|
+
const decisionAttemptDigest = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(pending.decisionAttempt)).digest("hex");
|
|
1007
|
+
if (!receipt || !pending.terminal || !pending.decisionAttempt || receipt.schemaVersion !== "telegram-approval-settlement-receipt-v1"
|
|
1008
|
+
|| !["live", "recovery"].includes(receipt.kind)
|
|
1009
|
+
|| !Number.isSafeInteger(receipt.callbackAt)
|
|
1010
|
+
|| typeof receipt.accepted !== "boolean"
|
|
1011
|
+
|| receipt.reason !== (receipt.accepted ? "accepted" : "decision_refused")
|
|
1012
|
+
|| (receipt.kind === "live" ? !["acknowledged", "rejected_as_stale"].includes(receipt.acknowledgementState) : receipt.acknowledgementState !== "indeterminate_after_restart")
|
|
1013
|
+
|| (receipt.kind === "live" ? receipt.recoveredAt !== null : !Number.isSafeInteger(receipt.recoveredAt))
|
|
1014
|
+
|| receipt.callbackAt !== pending.decisionAttempt.attemptedAt
|
|
1015
|
+
|| !/^[0-9a-f]{64}$/u.test(receipt.decisionAttemptDigest) || receipt.decisionAttemptDigest !== decisionAttemptDigest
|
|
1016
|
+
|| receipt.evidenceMac !== signState("telegram.approval_settlement_receipt_state", pending, settlementReceiptFields(receipt))) {
|
|
1017
|
+
throw new Error("Telegram persisted settlement receipt is invalid");
|
|
1018
|
+
}
|
|
1019
|
+
return receipt;
|
|
1020
|
+
};
|
|
1021
|
+
const emitSettlementReceipt = async (pending) => {
|
|
1022
|
+
const receipt = validateSettlementReceipt(pending);
|
|
1023
|
+
const fields = receipt.kind === "live"
|
|
1024
|
+
? { callbackAt: receipt.callbackAt, acknowledged: receipt.acknowledgementState === "acknowledged", acknowledgementState: receipt.acknowledgementState, accepted: receipt.accepted, reason: receipt.reason, decisionAttemptDigest: receipt.decisionAttemptDigest }
|
|
1025
|
+
: { callbackAt: receipt.callbackAt, acknowledgementState: receipt.acknowledgementState, accepted: receipt.accepted, reason: receipt.reason, recoveredAt: receipt.recoveredAt, decisionAttemptDigest: receipt.decisionAttemptDigest };
|
|
1026
|
+
const event = receipt.kind === "live" ? "telegram.callback_settled" : "telegram.callback_recovery_settled";
|
|
1027
|
+
if (options.signAcceptanceEvidence) {
|
|
1028
|
+
const unsigned = acceptanceEvidenceUnsigned(pending, fields);
|
|
1029
|
+
const meta = { ...unsigned, evidenceMac: options.signAcceptanceEvidence(event, unsigned) };
|
|
1030
|
+
if (options.commitAcceptanceEvidence)
|
|
1031
|
+
await options.commitAcceptanceEvidence(event, meta);
|
|
1032
|
+
else
|
|
1033
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event, message: "Telegram approval acceptance evidence recorded", meta });
|
|
1034
|
+
options.onAcceptanceEvidence?.(event, meta);
|
|
1035
|
+
}
|
|
1036
|
+
else
|
|
1037
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event, message: "Telegram approval callback settled", meta: { approvalId: pending.approvalId, ...fields, ...options.acceptanceEventMeta?.() } });
|
|
1038
|
+
};
|
|
1039
|
+
const validateExpiryObservation = (pending) => {
|
|
1040
|
+
const observation = pending.expiryObservation;
|
|
1041
|
+
const fields = expiryObservationFields(pending, observation.observedAt);
|
|
1042
|
+
const expectedMac = pending.acceptanceBinding
|
|
1043
|
+
? options.signAcceptanceEvidence?.("telegram.approval_expiry_observed", acceptanceEvidenceUnsigned(pending, fields)) ?? null
|
|
1044
|
+
: null;
|
|
1045
|
+
if (observation.schemaVersion !== "telegram-approval-expiry-observation-v1" || observation.deadlineAt !== pending.expiresAt
|
|
1046
|
+
|| !Number.isSafeInteger(observation.observedAt) || observation.observedAt < observation.deadlineAt
|
|
1047
|
+
|| observation.evidenceMac !== expectedMac)
|
|
1048
|
+
throw new Error("Telegram persisted expiry observation is invalid");
|
|
1049
|
+
return observation;
|
|
1050
|
+
};
|
|
1051
|
+
const ensureExpiryObservation = (pending, observedAt = now()) => {
|
|
1052
|
+
let observation = pending.expiryObservation;
|
|
1053
|
+
if (!observation) {
|
|
1054
|
+
const fields = expiryObservationFields(pending, observedAt);
|
|
1055
|
+
const evidenceMac = pending.acceptanceBinding
|
|
1056
|
+
? options.signAcceptanceEvidence?.("telegram.approval_expiry_observed", acceptanceEvidenceUnsigned(pending, fields)) ?? null
|
|
1057
|
+
: null;
|
|
1058
|
+
if (pending.acceptanceBinding && evidenceMac === null)
|
|
1059
|
+
throw new Error("Telegram acceptance evidence signer is unavailable");
|
|
1060
|
+
observation = { schemaVersion: "telegram-approval-expiry-observation-v1", deadlineAt: pending.expiresAt, observedAt, evidenceMac };
|
|
1061
|
+
persistMutation(pending, () => { pending.expiryObservation = observation; });
|
|
1062
|
+
}
|
|
1063
|
+
observation = validateExpiryObservation(pending);
|
|
1064
|
+
const fields = expiryObservationFields(pending, observation.observedAt);
|
|
1065
|
+
emitAcceptanceEvidence("telegram.approval_expiry_observed", pending, fields);
|
|
1066
|
+
return observation;
|
|
1067
|
+
};
|
|
1068
|
+
const validateStaleTap = (pending) => {
|
|
1069
|
+
if (!pending.staleTap)
|
|
1070
|
+
return;
|
|
1071
|
+
if (pending.staleTap.schemaVersion !== "telegram-approval-stale-tap-v1"
|
|
1072
|
+
|| !["attempted", "consumed"].includes(pending.staleTap.state)
|
|
1073
|
+
|| !/^[0-9a-f]{64}$/u.test(pending.staleTap.queryIdDigest)
|
|
1074
|
+
|| !Number.isSafeInteger(pending.staleTap.attemptedAt)
|
|
1075
|
+
|| (pending.staleTap.state === "attempted" && pending.staleTap.consumedAt !== null)
|
|
1076
|
+
|| (pending.staleTap.state === "consumed" && (!Number.isSafeInteger(pending.staleTap.consumedAt)
|
|
1077
|
+
|| Number(pending.staleTap.consumedAt) < pending.staleTap.attemptedAt))
|
|
1078
|
+
|| pending.staleTap.evidenceMac !== signState("telegram.approval_stale_tap_state", pending, staleTapFields(pending, pending.staleTap))) {
|
|
1079
|
+
throw new Error("Telegram persisted stale-tap record is invalid");
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
const validateTerminalTombstone = (pending) => {
|
|
1083
|
+
try {
|
|
1084
|
+
if (pending.deliveryState !== "terminal_tombstone" || !pending.acceptanceBinding || pending.messageId === null
|
|
1085
|
+
|| pending.decisionToken !== undefined || !Number.isSafeInteger(pending.terminalizedAt)
|
|
1086
|
+
|| !Number.isSafeInteger(pending.tombstoneExpiresAt)
|
|
1087
|
+
|| pending.tombstoneExpiresAt !== Number(pending.terminalizedAt) + exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS) {
|
|
1088
|
+
throw new Error("shape");
|
|
1089
|
+
}
|
|
1090
|
+
const observation = validateExpiryObservation(pending);
|
|
1091
|
+
if (Number(pending.terminalizedAt) < observation.observedAt
|
|
1092
|
+
|| pending.tombstoneMac !== signState("telegram.approval_terminal_tombstone_state", pending, terminalTombstoneFields(pending)))
|
|
1093
|
+
throw new Error("order");
|
|
1094
|
+
validateStaleTap(pending);
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
throw new Error("Telegram persisted terminal tombstone is invalid");
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
for (const loaded of options.pendingStore.load()) {
|
|
1101
|
+
const pending = {
|
|
1102
|
+
...loaded,
|
|
1103
|
+
deliveryState: loaded.deliveryState ?? "bound",
|
|
1104
|
+
};
|
|
1105
|
+
assertTelegramCallbackData(pending.approveCallbackData);
|
|
1106
|
+
assertTelegramCallbackData(pending.denyCallbackData);
|
|
1107
|
+
if (pendingByCallback.has(pending.approveCallbackData) || pendingByCallback.has(pending.denyCallbackData)) {
|
|
1108
|
+
throw new Error("Telegram persisted approval callback handle collision");
|
|
1109
|
+
}
|
|
1110
|
+
add(pending);
|
|
1111
|
+
}
|
|
1112
|
+
const editTerminal = async (pending, terminalText, expiryObservedAt) => {
|
|
1113
|
+
if (pending.messageId === null)
|
|
1114
|
+
return now();
|
|
1115
|
+
const terminalEditStartedAt = now();
|
|
1116
|
+
try {
|
|
1117
|
+
effectBarrier();
|
|
1118
|
+
await options.effects.edit({ idempotencyKey: `approval:${pending.approvalId}:edit:${(0, node_crypto_1.createHash)("sha256").update(terminalText).digest("hex")}`, chatId: options.expectedChatId, messageId: Number(pending.messageId), text: terminalText });
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
const alreadyTerminal = error instanceof TelegramApiError && error.status === 400 && /message is not modified/i.test(error.message);
|
|
1122
|
+
if (!alreadyTerminal)
|
|
1123
|
+
throw error;
|
|
1124
|
+
}
|
|
1125
|
+
const terminalizedAt = now();
|
|
1126
|
+
if (pending.acceptanceBinding)
|
|
1127
|
+
emitAcceptanceEvidence("telegram.approval_prompt_terminalized", pending, { ...(expiryObservedAt === undefined ? {} : { expiryDeadlineAt: pending.expiresAt, expiryObservedAt }), terminalEditStartedAt, terminalizedAt, buttonsRemoved: true });
|
|
1128
|
+
else
|
|
1129
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "telegram.approval_prompt_terminalized", message: "Telegram approval prompt was terminalized", meta: { approvalId: pending.approvalId, buttonsRemoved: true, ...options.acceptanceEventMeta?.() } });
|
|
1130
|
+
return terminalizedAt;
|
|
1131
|
+
};
|
|
1132
|
+
const retainTerminalTombstone = (pending, terminalizedAt) => {
|
|
1133
|
+
pending.deliveryState = "terminal_tombstone";
|
|
1134
|
+
pending.decisionToken = undefined;
|
|
1135
|
+
pending.terminalizedAt = terminalizedAt;
|
|
1136
|
+
pending.tombstoneExpiresAt = terminalizedAt + exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS;
|
|
1137
|
+
pending.tombstoneMac = signState("telegram.approval_terminal_tombstone_state", pending, terminalTombstoneFields(pending)) ?? undefined;
|
|
1138
|
+
};
|
|
1139
|
+
const acknowledge = async (callbackQueryId, invalid) => {
|
|
1140
|
+
try {
|
|
1141
|
+
effectBarrier();
|
|
1142
|
+
await options.effects.acknowledge({
|
|
1143
|
+
idempotencyKey: `approval-callback:${(0, node_crypto_1.createHash)("sha256").update(callbackQueryId).digest("hex")}`,
|
|
1144
|
+
callbackQueryId,
|
|
1145
|
+
...(invalid ? { text: "This approval is no longer valid.", showAlert: true } : {}),
|
|
1146
|
+
});
|
|
1147
|
+
return "acknowledged";
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
const stale = error instanceof TelegramApiError
|
|
1151
|
+
&& error.status === 400
|
|
1152
|
+
&& /query is too old|query ID is invalid/i.test(error.message);
|
|
1153
|
+
if (!stale)
|
|
1154
|
+
throw error;
|
|
1155
|
+
return "rejected_as_stale";
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
const settleDecisionAttempt = async (pending, expected) => {
|
|
1159
|
+
if (pending.settlementReceipt) {
|
|
1160
|
+
const receipt = validateSettlementReceipt(pending);
|
|
1161
|
+
await emitSettlementReceipt(pending);
|
|
1162
|
+
await options.onSettlementComplete?.(pending.approvalId);
|
|
1163
|
+
persistRemoval(pending);
|
|
1164
|
+
return { accepted: receipt.accepted, terminalText: pending.terminal.terminalText, callbackAt: receipt.callbackAt };
|
|
1165
|
+
}
|
|
1166
|
+
let decisionToken;
|
|
1167
|
+
if (!pending.decisionAttempt) {
|
|
1168
|
+
const observed = expected;
|
|
1169
|
+
decisionToken = await resolveDecisionToken(pending);
|
|
1170
|
+
const fencingToken = decisionToken;
|
|
1171
|
+
const unsigned = {
|
|
1172
|
+
schemaVersion: "telegram-approval-decision-attempt-v1",
|
|
1173
|
+
decision: observed.decision,
|
|
1174
|
+
queryIdDigest: observed.queryIdDigest,
|
|
1175
|
+
attemptedAt: observed.observedAt,
|
|
1176
|
+
};
|
|
1177
|
+
if (unsigned.attemptedAt < (pending.acceptanceBinding?.boundAt ?? pending.expiresAt - exports.TELEGRAM_APPROVAL_TTL_MS)
|
|
1178
|
+
|| unsigned.attemptedAt >= pending.expiresAt)
|
|
1179
|
+
throw new Error("Telegram decision attempt was observed outside its deadline");
|
|
1180
|
+
persistMutation(pending, () => {
|
|
1181
|
+
pending.decisionAttempt = {
|
|
1182
|
+
...unsigned,
|
|
1183
|
+
evidenceMac: decisionAttemptMac(fencingToken, pending, unsigned),
|
|
1184
|
+
recoveryMac: signState("telegram.approval_decision_attempt_recovery_state", pending, decisionAttemptPayload(pending, unsigned)),
|
|
1185
|
+
};
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
const attempt = await validateDecisionAttempt(pending, expected);
|
|
1189
|
+
let outcome = pending.terminal;
|
|
1190
|
+
if (outcome)
|
|
1191
|
+
validateTerminalOutcome(pending);
|
|
1192
|
+
else {
|
|
1193
|
+
decisionToken ??= await resolveDecisionToken(pending);
|
|
1194
|
+
outcome = await options.onDecision({
|
|
1195
|
+
approvalId: pending.approvalId,
|
|
1196
|
+
decisionToken,
|
|
1197
|
+
decision: attempt.decision,
|
|
1198
|
+
decisionAt: attempt.attemptedAt,
|
|
1199
|
+
requesterId: options.expectedUserId,
|
|
1200
|
+
transport: "telegram",
|
|
1201
|
+
transportChatId: options.expectedChatId,
|
|
1202
|
+
transportMessageId: pending.messageId,
|
|
1203
|
+
...(pending.acceptanceBinding ? { acceptanceBinding: pending.acceptanceBinding } : {}),
|
|
1204
|
+
});
|
|
1205
|
+
persistMutation(pending, () => {
|
|
1206
|
+
pending.terminal = outcome;
|
|
1207
|
+
pending.terminalMac = signState("telegram.approval_terminal_outcome_state", pending, terminalOutcomeFields(pending));
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
const acknowledgementState = expected ? await acknowledge(expected.callbackQueryId, false) : "indeterminate_after_restart";
|
|
1211
|
+
await editTerminal(pending, outcome.terminalText);
|
|
1212
|
+
const reason = outcome.accepted ? "accepted" : "decision_refused";
|
|
1213
|
+
persistMutation(pending, () => {
|
|
1214
|
+
const receipt = {
|
|
1215
|
+
schemaVersion: "telegram-approval-settlement-receipt-v1",
|
|
1216
|
+
kind: expected ? "live" : "recovery",
|
|
1217
|
+
callbackAt: attempt.attemptedAt,
|
|
1218
|
+
accepted: outcome.accepted,
|
|
1219
|
+
reason,
|
|
1220
|
+
acknowledgementState,
|
|
1221
|
+
recoveredAt: expected ? null : now(),
|
|
1222
|
+
decisionAttemptDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(attempt)).digest("hex"),
|
|
1223
|
+
evidenceMac: null,
|
|
1224
|
+
};
|
|
1225
|
+
receipt.evidenceMac = signState("telegram.approval_settlement_receipt_state", pending, settlementReceiptFields(receipt));
|
|
1226
|
+
pending.settlementReceipt = receipt;
|
|
1227
|
+
});
|
|
1228
|
+
await emitSettlementReceipt(pending);
|
|
1229
|
+
await options.onSettlementComplete?.(pending.approvalId);
|
|
1230
|
+
persistRemoval(pending);
|
|
1231
|
+
return { ...outcome, callbackAt: attempt.attemptedAt };
|
|
1232
|
+
};
|
|
1233
|
+
const reconcileExpired = async () => {
|
|
1234
|
+
let firstFailure;
|
|
1235
|
+
for (const pending of uniquePending()) {
|
|
1236
|
+
try {
|
|
1237
|
+
const persistedState = classifyTelegramPersistedApprovalState(pending);
|
|
1238
|
+
if (persistedState === "ordinary" && pending.deliveryState !== "terminal_tombstone" && now() < pending.expiresAt)
|
|
1239
|
+
continue;
|
|
1240
|
+
await withApprovalExclusive(pending.approvalId, false, async () => {
|
|
1241
|
+
const current = uniquePending().find((candidate) => candidate.approvalId === pending.approvalId);
|
|
1242
|
+
if (!current)
|
|
1243
|
+
return;
|
|
1244
|
+
if (current.deliveryState === "terminal_tombstone") {
|
|
1245
|
+
validateTerminalTombstone(current);
|
|
1246
|
+
if (now() >= current.tombstoneExpiresAt) {
|
|
1247
|
+
remove(current);
|
|
1248
|
+
persist();
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const persistedState = classifyTelegramPersistedApprovalState(current);
|
|
1253
|
+
if (persistedState === "decision_attempt" || persistedState === "action_terminal") {
|
|
1254
|
+
await settleDecisionAttempt(current);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (persistedState === "ordinary" && !current.terminal && now() < current.expiresAt)
|
|
1258
|
+
return;
|
|
1259
|
+
if (current.terminal) {
|
|
1260
|
+
await editTerminal(current, current.terminal.terminalText);
|
|
1261
|
+
remove(current);
|
|
1262
|
+
persist();
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const observation = ensureExpiryObservation(current);
|
|
1266
|
+
await options.onExpire?.(current.approvalId);
|
|
1267
|
+
const terminalizedAt = await editTerminal(current, exports.TELEGRAM_APPROVAL_EXPIRED_TEXT, observation.observedAt);
|
|
1268
|
+
if (current.acceptanceBinding)
|
|
1269
|
+
persistMutation(current, () => retainTerminalTombstone(current, terminalizedAt));
|
|
1270
|
+
else {
|
|
1271
|
+
remove(current);
|
|
1272
|
+
persist();
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
catch (error) {
|
|
1277
|
+
firstFailure ??= error;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (firstFailure !== undefined)
|
|
1281
|
+
throw firstFailure;
|
|
1282
|
+
};
|
|
1283
|
+
return {
|
|
1284
|
+
async validatePendingTerminalControl(approvalId) {
|
|
1285
|
+
const pending = uniquePending().find((entry) => entry.approvalId === approvalId);
|
|
1286
|
+
if (!pending || !options.signAcceptanceEvidence)
|
|
1287
|
+
throw new Error("Telegram terminal control integrity is unavailable");
|
|
1288
|
+
const state = classifyTelegramPersistedApprovalState(pending);
|
|
1289
|
+
if (state === "action_terminal") {
|
|
1290
|
+
await validateDecisionAttempt(pending);
|
|
1291
|
+
validateTerminalOutcome(pending);
|
|
1292
|
+
}
|
|
1293
|
+
else if (state === "expiry_observed") {
|
|
1294
|
+
validateExpiryObservation(pending);
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
throw new Error("Telegram pending terminal control is not authenticated");
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
async sendApproval(input) {
|
|
1301
|
+
effectBarrier();
|
|
1302
|
+
const approveCallbackData = `a:${options.createOpaqueHandle()}`;
|
|
1303
|
+
const denyCallbackData = `d:${options.createOpaqueHandle()}`;
|
|
1304
|
+
assertTelegramCallbackData(approveCallbackData);
|
|
1305
|
+
assertTelegramCallbackData(denyCallbackData);
|
|
1306
|
+
if (pendingByCallback.has(approveCallbackData) || pendingByCallback.has(denyCallbackData)) {
|
|
1307
|
+
throw new Error("Telegram approval callback handle collision");
|
|
1308
|
+
}
|
|
1309
|
+
const pending = {
|
|
1310
|
+
approvalId: input.approvalId,
|
|
1311
|
+
decisionToken: input.decisionToken,
|
|
1312
|
+
messageId: null,
|
|
1313
|
+
deliveryState: "pending",
|
|
1314
|
+
approveCallbackData,
|
|
1315
|
+
denyCallbackData,
|
|
1316
|
+
expiresAt: now() + exports.TELEGRAM_APPROVAL_TTL_MS,
|
|
1317
|
+
prompt: input.prompt,
|
|
1318
|
+
};
|
|
1319
|
+
add(pending);
|
|
1320
|
+
try {
|
|
1321
|
+
persist();
|
|
1322
|
+
}
|
|
1323
|
+
catch (error) {
|
|
1324
|
+
remove(pending);
|
|
1325
|
+
throw error;
|
|
1326
|
+
}
|
|
1327
|
+
pending.deliveryState = "send_attempting";
|
|
1328
|
+
try {
|
|
1329
|
+
persist();
|
|
1330
|
+
}
|
|
1331
|
+
catch (error) {
|
|
1332
|
+
pending.deliveryState = "pending";
|
|
1333
|
+
remove(pending);
|
|
1334
|
+
throw error;
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
effectBarrier();
|
|
1338
|
+
const messageId = await options.effects.sendCard({
|
|
1339
|
+
idempotencyKey: `approval:${input.approvalId}:prompt`,
|
|
1340
|
+
chatId: options.expectedChatId,
|
|
1341
|
+
text: input.prompt,
|
|
1342
|
+
buttons: [[{ text: "Approve", callbackData: approveCallbackData }, { text: "Deny", callbackData: denyCallbackData }]],
|
|
1343
|
+
});
|
|
1344
|
+
if (!Number.isSafeInteger(messageId) || messageId <= 0)
|
|
1345
|
+
throw new Error("Telegram sendMessage response did not include a canonical message_id");
|
|
1346
|
+
pending.messageId = String(messageId);
|
|
1347
|
+
pending.deliveryState = "bound";
|
|
1348
|
+
if (input.acceptanceBinding) {
|
|
1349
|
+
const boundAt = now();
|
|
1350
|
+
pending.acceptanceBinding = {
|
|
1351
|
+
...input.acceptanceBinding,
|
|
1352
|
+
messageIdDigest: options.acceptanceMessageIdDigest?.(pending.messageId)
|
|
1353
|
+
?? (0, node_crypto_1.createHash)("sha256").update(pending.messageId, "utf8").digest("hex"),
|
|
1354
|
+
boundAt,
|
|
1355
|
+
};
|
|
1356
|
+
pending.expiresAt = boundAt + exports.TELEGRAM_APPROVAL_TTL_MS;
|
|
1357
|
+
}
|
|
1358
|
+
persist();
|
|
1359
|
+
emitAcceptanceEvidence("senses.telegram_approval_prompt_bound", pending, { boundAt: pending.acceptanceBinding?.boundAt });
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
pending.messageId = null;
|
|
1363
|
+
pending.deliveryState = "delivery_indeterminate";
|
|
1364
|
+
persist();
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1367
|
+
return { messageId: pending.messageId, approveCallbackData, denyCallbackData, expiresAt: pending.expiresAt };
|
|
1368
|
+
},
|
|
1369
|
+
async handleUpdate(update) {
|
|
1370
|
+
effectBarrier();
|
|
1371
|
+
const callback = update.callback_query;
|
|
1372
|
+
if (!callback)
|
|
1373
|
+
return { handled: false, accepted: false, reason: "not_callback" };
|
|
1374
|
+
const observedAt = now();
|
|
1375
|
+
const initiallyPending = pendingByCallback.get(callback.data ?? "");
|
|
1376
|
+
if (initiallyPending) {
|
|
1377
|
+
const result = await withApprovalExclusive(initiallyPending.approvalId, true, async () => handleUpdateUnlocked(update, observedAt));
|
|
1378
|
+
return result;
|
|
1379
|
+
}
|
|
1380
|
+
return handleUpdateUnlocked(update, observedAt);
|
|
1381
|
+
},
|
|
1382
|
+
reconcileExpired,
|
|
1383
|
+
async recoverDecisionAttempt(approvalId) {
|
|
1384
|
+
const result = await withApprovalExclusive(approvalId, true, async () => {
|
|
1385
|
+
const pending = uniquePending().find((record) => record.approvalId === approvalId);
|
|
1386
|
+
if (!pending)
|
|
1387
|
+
return false;
|
|
1388
|
+
const persistedState = classifyTelegramPersistedApprovalState(pending);
|
|
1389
|
+
if (persistedState !== "decision_attempt" && persistedState !== "action_terminal")
|
|
1390
|
+
return false;
|
|
1391
|
+
await settleDecisionAttempt(pending);
|
|
1392
|
+
return true;
|
|
1393
|
+
});
|
|
1394
|
+
return result;
|
|
1395
|
+
},
|
|
1396
|
+
async terminalizeOrphaned(approvalId, terminalText) {
|
|
1397
|
+
const result = await withApprovalExclusive(approvalId, true, async () => {
|
|
1398
|
+
const pending = uniquePending().find((record) => record.approvalId === approvalId);
|
|
1399
|
+
if (!pending)
|
|
1400
|
+
return { terminalEditSucceeded: true };
|
|
1401
|
+
const persistedState = classifyTelegramPersistedApprovalState(pending);
|
|
1402
|
+
if (persistedState === "expiry_observed")
|
|
1403
|
+
validateExpiryObservation(pending);
|
|
1404
|
+
if (persistedState === "decision_attempt" || persistedState === "action_terminal") {
|
|
1405
|
+
throw new Error("Telegram persisted approval authority cannot be orphan-cleaned");
|
|
1406
|
+
}
|
|
1407
|
+
let terminalEditSucceeded = true;
|
|
1408
|
+
try {
|
|
1409
|
+
await editTerminal(pending, terminalText);
|
|
1410
|
+
}
|
|
1411
|
+
catch {
|
|
1412
|
+
terminalEditSucceeded = false;
|
|
1413
|
+
}
|
|
1414
|
+
remove(pending);
|
|
1415
|
+
persist();
|
|
1416
|
+
return { terminalEditSucceeded };
|
|
1417
|
+
});
|
|
1418
|
+
return result;
|
|
1419
|
+
},
|
|
1420
|
+
async terminalizeRecovered(approvalId, terminalText) {
|
|
1421
|
+
await withApprovalExclusive(approvalId, true, async () => {
|
|
1422
|
+
const pending = uniquePending().find((record) => record.approvalId === approvalId);
|
|
1423
|
+
if (!pending)
|
|
1424
|
+
return;
|
|
1425
|
+
const persistedState = classifyTelegramPersistedApprovalState(pending);
|
|
1426
|
+
if (persistedState === "expiry_observed")
|
|
1427
|
+
validateExpiryObservation(pending);
|
|
1428
|
+
if (persistedState === "decision_attempt" || persistedState === "action_terminal") {
|
|
1429
|
+
await settleDecisionAttempt(pending);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
if (pending.messageId === null && (pending.deliveryState === "send_attempting" || pending.deliveryState === "delivery_indeterminate")) {
|
|
1433
|
+
pending.deliveryState = "delivery_indeterminate";
|
|
1434
|
+
pending.terminal = { accepted: false, terminalText };
|
|
1435
|
+
pending.terminalKind = "delivery_interruption";
|
|
1436
|
+
persist();
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
await editTerminal(pending, terminalText);
|
|
1440
|
+
remove(pending);
|
|
1441
|
+
persist();
|
|
1442
|
+
});
|
|
1443
|
+
},
|
|
1444
|
+
listPendingDeliveries() {
|
|
1445
|
+
return uniquePending().filter((record) => record.deliveryState !== "terminal_tombstone")
|
|
1446
|
+
.map(({ decisionToken: _secret, ...record }) => structuredClone(record));
|
|
1447
|
+
},
|
|
1448
|
+
};
|
|
1449
|
+
async function handleUpdateUnlocked(update, observedAt) {
|
|
1450
|
+
const callback = update.callback_query;
|
|
1451
|
+
const pending = pendingByCallback.get(callback.data ?? "");
|
|
1452
|
+
const userId = String(callback.from.id);
|
|
1453
|
+
const chatId = callback.message ? String(callback.message.chat.id) : "";
|
|
1454
|
+
const messageId = callback.message ? String(callback.message.message_id) : "";
|
|
1455
|
+
const persistedState = pending ? classifyTelegramPersistedApprovalState(pending) : undefined;
|
|
1456
|
+
let invalidReason = null;
|
|
1457
|
+
let expiryObservedAt;
|
|
1458
|
+
let expiryObservation;
|
|
1459
|
+
if (pending && persistedState === "expiry_observed") {
|
|
1460
|
+
expiryObservation = validateExpiryObservation(pending);
|
|
1461
|
+
expiryObservedAt = expiryObservation.observedAt;
|
|
1462
|
+
}
|
|
1463
|
+
if (!pending)
|
|
1464
|
+
invalidReason = "stale_callback";
|
|
1465
|
+
else if (userId !== options.expectedUserId)
|
|
1466
|
+
invalidReason = "foreign_user";
|
|
1467
|
+
else if (chatId !== options.expectedChatId)
|
|
1468
|
+
invalidReason = "foreign_chat";
|
|
1469
|
+
else if (pending.deliveryState === "terminal_tombstone") {
|
|
1470
|
+
if (pending.messageId === null || messageId !== pending.messageId)
|
|
1471
|
+
invalidReason = "foreign_message";
|
|
1472
|
+
else
|
|
1473
|
+
invalidReason = "stale_callback";
|
|
1474
|
+
}
|
|
1475
|
+
else if (pending.deliveryState === "send_attempting" || pending.deliveryState === "delivery_indeterminate")
|
|
1476
|
+
invalidReason = "delivery_indeterminate";
|
|
1477
|
+
else if (pending.deliveryState !== "bound" || pending.messageId === null)
|
|
1478
|
+
invalidReason = "prompt_not_bound";
|
|
1479
|
+
else if (messageId !== pending.messageId)
|
|
1480
|
+
invalidReason = "foreign_message";
|
|
1481
|
+
else if (persistedState === "expiry_observed")
|
|
1482
|
+
invalidReason = "expired";
|
|
1483
|
+
else {
|
|
1484
|
+
if (!pending.decisionAttempt && observedAt >= pending.expiresAt) {
|
|
1485
|
+
invalidReason = "expired";
|
|
1486
|
+
expiryObservedAt = observedAt;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (invalidReason) {
|
|
1490
|
+
if (pending?.deliveryState === "terminal_tombstone" && invalidReason === "stale_callback") {
|
|
1491
|
+
validateTerminalTombstone(pending);
|
|
1492
|
+
const queryIdDigest = (0, node_crypto_1.createHash)("sha256").update(callback.id).digest("hex");
|
|
1493
|
+
if (pending.staleTap) {
|
|
1494
|
+
validateStaleTap(pending);
|
|
1495
|
+
if (pending.staleTap.state === "attempted") {
|
|
1496
|
+
if (pending.staleTap.queryIdDigest !== queryIdDigest)
|
|
1497
|
+
throw new Error("Telegram stale-tap recovery query mismatch");
|
|
1498
|
+
const staleAt = pending.staleTap.attemptedAt;
|
|
1499
|
+
persistMutation(pending, () => {
|
|
1500
|
+
const consumed = { ...pending.staleTap, state: "consumed", consumedAt: now(), evidenceMac: null };
|
|
1501
|
+
consumed.evidenceMac = signState("telegram.approval_stale_tap_state", pending, staleTapFields(pending, consumed));
|
|
1502
|
+
pending.staleTap = consumed;
|
|
1503
|
+
});
|
|
1504
|
+
await acknowledge(callback.id, true);
|
|
1505
|
+
emitAcceptanceEvidence("telegram.approval_stale_callback_settled", pending, { staleAt, acknowledged: true, accepted: false, reason: "stale_callback" });
|
|
1506
|
+
}
|
|
1507
|
+
else {
|
|
1508
|
+
await acknowledge(callback.id, true);
|
|
1509
|
+
}
|
|
1510
|
+
return { handled: true, accepted: false, reason: invalidReason };
|
|
1511
|
+
}
|
|
1512
|
+
const staleAt = now();
|
|
1513
|
+
persistMutation(pending, () => {
|
|
1514
|
+
const attempted = { schemaVersion: "telegram-approval-stale-tap-v1", state: "attempted", queryIdDigest, attemptedAt: staleAt, consumedAt: null, evidenceMac: null };
|
|
1515
|
+
attempted.evidenceMac = signState("telegram.approval_stale_tap_state", pending, staleTapFields(pending, attempted));
|
|
1516
|
+
pending.staleTap = attempted;
|
|
1517
|
+
});
|
|
1518
|
+
persistMutation(pending, () => {
|
|
1519
|
+
const consumed = { ...pending.staleTap, state: "consumed", consumedAt: now(), evidenceMac: null };
|
|
1520
|
+
consumed.evidenceMac = signState("telegram.approval_stale_tap_state", pending, staleTapFields(pending, consumed));
|
|
1521
|
+
pending.staleTap = consumed;
|
|
1522
|
+
});
|
|
1523
|
+
await acknowledge(callback.id, true);
|
|
1524
|
+
emitAcceptanceEvidence("telegram.approval_stale_callback_settled", pending, { staleAt, acknowledged: true, accepted: false, reason: "stale_callback" });
|
|
1525
|
+
return { handled: true, accepted: false, reason: invalidReason };
|
|
1526
|
+
}
|
|
1527
|
+
if (pending && invalidReason === "expired") {
|
|
1528
|
+
expiryObservation = ensureExpiryObservation(pending, expiryObservedAt);
|
|
1529
|
+
expiryObservedAt = expiryObservation.observedAt;
|
|
1530
|
+
}
|
|
1531
|
+
await acknowledge(callback.id, true);
|
|
1532
|
+
if (pending && invalidReason === "delivery_indeterminate" && callback.message) {
|
|
1533
|
+
pending.messageId = messageId;
|
|
1534
|
+
await editTerminal(pending, pending.terminal?.terminalText ?? "⚠️ Approval prompt delivery was interrupted — no action was taken");
|
|
1535
|
+
remove(pending);
|
|
1536
|
+
persist();
|
|
1537
|
+
}
|
|
1538
|
+
if (pending && invalidReason === "expired") {
|
|
1539
|
+
await options.onExpire?.(pending.approvalId);
|
|
1540
|
+
const terminalizedAt = await editTerminal(pending, "⚠️ Approval expired", expiryObservation.observedAt);
|
|
1541
|
+
if (pending.acceptanceBinding)
|
|
1542
|
+
persistMutation(pending, () => retainTerminalTombstone(pending, terminalizedAt));
|
|
1543
|
+
else {
|
|
1544
|
+
remove(pending);
|
|
1545
|
+
persist();
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (pending?.acceptanceBinding && invalidReason === "expired")
|
|
1549
|
+
emitAcceptanceEvidence("telegram.callback_settled", pending, { callbackAt: expiryObservedAt, acknowledged: true, accepted: false, reason: invalidReason });
|
|
1550
|
+
else
|
|
1551
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "telegram.callback_settled", message: "Telegram approval callback settled", meta: { approvalId: pending?.approvalId ?? null, reason: invalidReason, accepted: false, ...options.acceptanceEventMeta?.() } });
|
|
1552
|
+
return { handled: true, accepted: false, reason: invalidReason };
|
|
1553
|
+
}
|
|
1554
|
+
const decision = callback.data === pending.approveCallbackData ? "approve" : "deny";
|
|
1555
|
+
const queryIdDigest = (0, node_crypto_1.createHash)("sha256").update(callback.id).digest("hex");
|
|
1556
|
+
const outcome = await settleDecisionAttempt(pending, { decision, queryIdDigest, callbackQueryId: callback.id, observedAt });
|
|
1557
|
+
const reason = outcome.accepted ? "accepted" : "decision_refused";
|
|
1558
|
+
return { handled: true, accepted: outcome.accepted, reason };
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function escapeTelegramHtml(value) {
|
|
1562
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
1563
|
+
}
|
|
1564
|
+
function sourceIndexForRenderedLimit(value, limit) {
|
|
1565
|
+
let renderedUnits = 0;
|
|
1566
|
+
let sourceIndex = 0;
|
|
1567
|
+
for (const character of value) {
|
|
1568
|
+
const escaped = escapeTelegramHtml(character);
|
|
1569
|
+
if (renderedUnits + escaped.length > limit)
|
|
1570
|
+
break;
|
|
1571
|
+
renderedUnits += escaped.length;
|
|
1572
|
+
sourceIndex += character.length;
|
|
1573
|
+
}
|
|
1574
|
+
return sourceIndex;
|
|
1575
|
+
}
|
|
1576
|
+
function preferredBoundary(value, limit, minimum) {
|
|
1577
|
+
const paragraph = value.lastIndexOf("\n\n", limit - 1);
|
|
1578
|
+
if (paragraph >= minimum)
|
|
1579
|
+
return paragraph + 2;
|
|
1580
|
+
const newline = value.lastIndexOf("\n", limit - 1);
|
|
1581
|
+
if (newline >= minimum)
|
|
1582
|
+
return newline + 1;
|
|
1583
|
+
for (let index = limit - 1; index >= minimum; index -= 1) {
|
|
1584
|
+
if (/\s/u.test(value[index]))
|
|
1585
|
+
return index + 1;
|
|
1586
|
+
}
|
|
1587
|
+
return limit;
|
|
1588
|
+
}
|
|
1589
|
+
function splitTelegramText(value, options = {}) {
|
|
1590
|
+
const targetUnits = options.targetUnits ?? 1_200;
|
|
1591
|
+
const maxUnits = options.maxUnits ?? 4_000;
|
|
1592
|
+
if (!Number.isInteger(targetUnits) || !Number.isInteger(maxUnits) || targetUnits < 1 || maxUnits < targetUnits) {
|
|
1593
|
+
throw new Error("Telegram chunk limits must be positive integers with targetUnits <= maxUnits");
|
|
1594
|
+
}
|
|
1595
|
+
if (!value)
|
|
1596
|
+
return [""];
|
|
1597
|
+
const chunks = [];
|
|
1598
|
+
let remaining = value;
|
|
1599
|
+
while (escapeTelegramHtml(remaining).length > maxUnits || escapeTelegramHtml(remaining).length > targetUnits) {
|
|
1600
|
+
const targetIndex = sourceIndexForRenderedLimit(remaining, targetUnits);
|
|
1601
|
+
const maxIndex = sourceIndexForRenderedLimit(remaining, maxUnits);
|
|
1602
|
+
const candidate = targetIndex > 0 ? targetIndex : maxIndex;
|
|
1603
|
+
if (candidate <= 0)
|
|
1604
|
+
throw new Error("Telegram chunk limit cannot fit one character");
|
|
1605
|
+
const splitAt = preferredBoundary(remaining, candidate, Math.floor(candidate / 2));
|
|
1606
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
1607
|
+
remaining = remaining.slice(splitAt);
|
|
1608
|
+
}
|
|
1609
|
+
chunks.push(remaining);
|
|
1610
|
+
return chunks;
|
|
1611
|
+
}
|
|
1612
|
+
async function sendTelegramText(api, chatId, text, options = {}) {
|
|
1613
|
+
const renderHtml = options.renderHtml ?? escapeTelegramHtml;
|
|
1614
|
+
const results = [];
|
|
1615
|
+
const recordMessageId = (value, chunk) => {
|
|
1616
|
+
const messageId = value && typeof value === "object" && !Array.isArray(value)
|
|
1617
|
+
? value.message_id
|
|
1618
|
+
: undefined;
|
|
1619
|
+
if (!Number.isSafeInteger(messageId) || messageId < 1)
|
|
1620
|
+
throw new TelegramApiError("Telegram sendMessage result omitted a canonical message_id");
|
|
1621
|
+
results.push(messageId);
|
|
1622
|
+
options.onMessageDelivered?.(messageId, chunk);
|
|
1623
|
+
};
|
|
1624
|
+
for (const chunk of splitTelegramText(text)) {
|
|
1625
|
+
try {
|
|
1626
|
+
recordMessageId(await api.request("sendMessage", {
|
|
1627
|
+
chat_id: chatId,
|
|
1628
|
+
text: renderHtml(chunk),
|
|
1629
|
+
parse_mode: "HTML",
|
|
1630
|
+
}, options.signal), chunk);
|
|
1631
|
+
}
|
|
1632
|
+
catch (error) {
|
|
1633
|
+
if (!(error instanceof TelegramApiError) || error.status !== 400)
|
|
1634
|
+
throw error;
|
|
1635
|
+
recordMessageId(await api.request("sendMessage", { chat_id: chatId, text: chunk }, options.signal), chunk);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return results;
|
|
1639
|
+
}
|
|
1640
|
+
function safeErrorMessage(message, token) {
|
|
1641
|
+
return message.split(token).join("[redacted]");
|
|
1642
|
+
}
|
|
1643
|
+
function parseEnvelope(raw, status, token) {
|
|
1644
|
+
let value;
|
|
1645
|
+
try {
|
|
1646
|
+
value = JSON.parse(raw);
|
|
1647
|
+
}
|
|
1648
|
+
catch (cause) {
|
|
1649
|
+
throw new TelegramApiError(`Telegram returned invalid JSON (HTTP ${status})`, { status, cause });
|
|
1650
|
+
}
|
|
1651
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.ok !== "boolean") {
|
|
1652
|
+
throw new TelegramApiError("Telegram returned an invalid response envelope", { status });
|
|
1653
|
+
}
|
|
1654
|
+
const envelope = value;
|
|
1655
|
+
if (!envelope.ok) {
|
|
1656
|
+
throw new TelegramApiError(safeErrorMessage(envelope.description ?? "Telegram request failed", token), {
|
|
1657
|
+
status,
|
|
1658
|
+
errorCode: typeof envelope.error_code === "number" ? envelope.error_code : null,
|
|
1659
|
+
retryAfterSeconds: typeof envelope.parameters?.retry_after === "number" ? envelope.parameters.retry_after : null,
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
if (!("result" in envelope)) {
|
|
1663
|
+
throw new TelegramApiError("Telegram success response omitted result", { status });
|
|
1664
|
+
}
|
|
1665
|
+
return envelope;
|
|
1666
|
+
}
|
|
1667
|
+
function createTelegramBotApi(options) {
|
|
1668
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1669
|
+
const apiRoot = options.apiRoot ?? "https://api.telegram.org";
|
|
1670
|
+
const baseUrl = `${apiRoot.replace(/\/$/, "")}/bot${options.token}`;
|
|
1671
|
+
const shutdown = new AbortController();
|
|
1672
|
+
const sleep = options.sleep ?? ((milliseconds, signal) => new Promise((resolve, reject) => {
|
|
1673
|
+
const timer = setTimeout(resolve, milliseconds);
|
|
1674
|
+
signal.addEventListener("abort", () => {
|
|
1675
|
+
clearTimeout(timer);
|
|
1676
|
+
reject(signal.reason);
|
|
1677
|
+
}, { once: true });
|
|
1678
|
+
}));
|
|
1679
|
+
return {
|
|
1680
|
+
async request(method, body, signal) {
|
|
1681
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1682
|
+
component: "senses",
|
|
1683
|
+
event: "telegram.request_start",
|
|
1684
|
+
message: "Telegram Bot API request started",
|
|
1685
|
+
meta: { method },
|
|
1686
|
+
});
|
|
1687
|
+
try {
|
|
1688
|
+
const requestSignal = signal
|
|
1689
|
+
? AbortSignal.any([shutdown.signal, signal])
|
|
1690
|
+
: shutdown.signal;
|
|
1691
|
+
let retries = 0;
|
|
1692
|
+
for (;;) {
|
|
1693
|
+
requestSignal.throwIfAborted();
|
|
1694
|
+
try {
|
|
1695
|
+
const response = await fetchImpl(`${baseUrl}/${method}`, {
|
|
1696
|
+
method: "POST",
|
|
1697
|
+
headers: { "content-type": "application/json" },
|
|
1698
|
+
body: JSON.stringify(body),
|
|
1699
|
+
signal: requestSignal,
|
|
1700
|
+
});
|
|
1701
|
+
const envelope = parseEnvelope(await response.text(), response.status, options.token);
|
|
1702
|
+
if (!response.ok) {
|
|
1703
|
+
throw new TelegramApiError(`Telegram request failed (HTTP ${response.status})`, { status: response.status });
|
|
1704
|
+
}
|
|
1705
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1706
|
+
component: "senses",
|
|
1707
|
+
event: "telegram.request_end",
|
|
1708
|
+
message: "Telegram Bot API request completed",
|
|
1709
|
+
meta: { method, status: response.status },
|
|
1710
|
+
});
|
|
1711
|
+
return envelope.result;
|
|
1712
|
+
}
|
|
1713
|
+
catch (caught) {
|
|
1714
|
+
const retryAfter = caught instanceof TelegramApiError ? caught.retryAfterSeconds : null;
|
|
1715
|
+
const canRetry = caught instanceof TelegramApiError
|
|
1716
|
+
&& caught.status === 429
|
|
1717
|
+
&& Number.isInteger(retryAfter)
|
|
1718
|
+
&& retries < 3;
|
|
1719
|
+
if (!canRetry)
|
|
1720
|
+
throw caught;
|
|
1721
|
+
retries += 1;
|
|
1722
|
+
const boundedSeconds = Math.min(Math.max(retryAfter, 1), 30);
|
|
1723
|
+
await sleep(boundedSeconds * 1_000, requestSignal);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
catch (caught) {
|
|
1728
|
+
const error = caught instanceof TelegramApiError
|
|
1729
|
+
? new TelegramApiError(safeErrorMessage(caught.message, options.token), {
|
|
1730
|
+
status: caught.status,
|
|
1731
|
+
errorCode: caught.errorCode,
|
|
1732
|
+
retryAfterSeconds: caught.retryAfterSeconds,
|
|
1733
|
+
})
|
|
1734
|
+
: new TelegramApiError(safeErrorMessage(caught instanceof Error ? caught.message : String(caught), options.token));
|
|
1735
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1736
|
+
level: "error",
|
|
1737
|
+
component: "senses",
|
|
1738
|
+
event: "telegram.request_error",
|
|
1739
|
+
message: "Telegram Bot API request failed",
|
|
1740
|
+
meta: { method, status: error.status, errorCode: error.errorCode },
|
|
1741
|
+
});
|
|
1742
|
+
throw error;
|
|
1743
|
+
}
|
|
1744
|
+
},
|
|
1745
|
+
stop() {
|
|
1746
|
+
shutdown.abort(new Error("Telegram Bot API client stopped"));
|
|
1747
|
+
},
|
|
1748
|
+
};
|
|
1749
|
+
}
|