approval-md 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +83 -914
  2. package/SPEC.md +101 -13
  3. package/cli.js +134 -7
  4. package/dist/src/adapters/smtp.d.ts +18 -0
  5. package/dist/src/adapters/smtp.js +24 -3
  6. package/dist/src/adapters/smtp.js.map +1 -1
  7. package/dist/src/channels/contract.d.ts +72 -0
  8. package/dist/src/channels/contract.js +161 -1
  9. package/dist/src/channels/contract.js.map +1 -1
  10. package/dist/src/channels/relay-server.d.ts +90 -0
  11. package/dist/src/channels/relay-server.js +295 -0
  12. package/dist/src/channels/relay-server.js.map +1 -0
  13. package/dist/src/channels/relay.d.ts +273 -0
  14. package/dist/src/channels/relay.js +564 -0
  15. package/dist/src/channels/relay.js.map +1 -0
  16. package/dist/src/channels/tagging.d.ts +25 -0
  17. package/dist/src/channels/tagging.js +63 -6
  18. package/dist/src/channels/tagging.js.map +1 -1
  19. package/dist/src/channels/telegram-minimal.d.ts +251 -0
  20. package/dist/src/channels/telegram-minimal.js +618 -0
  21. package/dist/src/channels/telegram-minimal.js.map +1 -0
  22. package/dist/src/channels/telegram-webhook.d.ts +273 -0
  23. package/dist/src/channels/telegram-webhook.js +536 -0
  24. package/dist/src/channels/telegram-webhook.js.map +1 -0
  25. package/dist/src/channels/telegram.d.ts +725 -20
  26. package/dist/src/channels/telegram.js +1332 -123
  27. package/dist/src/channels/telegram.js.map +1 -1
  28. package/dist/src/cli/amend.js +358 -5
  29. package/dist/src/cli/amend.js.map +1 -1
  30. package/dist/src/cli/attest.js +43 -6
  31. package/dist/src/cli/attest.js.map +1 -1
  32. package/dist/src/cli/audit-card.d.ts +1 -1
  33. package/dist/src/cli/audit-card.js +40 -1
  34. package/dist/src/cli/audit-card.js.map +1 -1
  35. package/dist/src/cli/audit.d.ts +7 -5
  36. package/dist/src/cli/audit.js +22 -10
  37. package/dist/src/cli/audit.js.map +1 -1
  38. package/dist/src/cli/channel-relay.d.ts +108 -0
  39. package/dist/src/cli/channel-relay.js +334 -0
  40. package/dist/src/cli/channel-relay.js.map +1 -0
  41. package/dist/src/cli/channel-telegram-webhook.d.ts +275 -0
  42. package/dist/src/cli/channel-telegram-webhook.js +716 -0
  43. package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
  44. package/dist/src/cli/channel-telegram.d.ts +347 -28
  45. package/dist/src/cli/channel-telegram.js +648 -56
  46. package/dist/src/cli/channel-telegram.js.map +1 -1
  47. package/dist/src/cli/channel.d.ts +1 -1
  48. package/dist/src/cli/channel.js +5 -1
  49. package/dist/src/cli/channel.js.map +1 -1
  50. package/dist/src/cli/daemon.js +37 -5
  51. package/dist/src/cli/daemon.js.map +1 -1
  52. package/dist/src/cli/doctor.d.ts +63 -0
  53. package/dist/src/cli/doctor.js +856 -111
  54. package/dist/src/cli/doctor.js.map +1 -1
  55. package/dist/src/cli/execute.d.ts +29 -1
  56. package/dist/src/cli/execute.js +387 -21
  57. package/dist/src/cli/execute.js.map +1 -1
  58. package/dist/src/cli/exit-codes.d.ts +12 -0
  59. package/dist/src/cli/exit-codes.js +13 -0
  60. package/dist/src/cli/exit-codes.js.map +1 -1
  61. package/dist/src/cli/gate.d.ts +2 -0
  62. package/dist/src/cli/gate.js +233 -2
  63. package/dist/src/cli/gate.js.map +1 -1
  64. package/dist/src/cli/help.d.ts +10 -1
  65. package/dist/src/cli/help.js +283 -54
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook.d.ts +485 -6
  68. package/dist/src/cli/hook.js +2370 -70
  69. package/dist/src/cli/hook.js.map +1 -1
  70. package/dist/src/cli/init.js +16 -7
  71. package/dist/src/cli/init.js.map +1 -1
  72. package/dist/src/cli/instructions.js +24 -3
  73. package/dist/src/cli/instructions.js.map +1 -1
  74. package/dist/src/cli/log-anchor.d.ts +55 -0
  75. package/dist/src/cli/log-anchor.js +199 -60
  76. package/dist/src/cli/log-anchor.js.map +1 -1
  77. package/dist/src/cli/log-unlock.d.ts +23 -0
  78. package/dist/src/cli/log-unlock.js +109 -0
  79. package/dist/src/cli/log-unlock.js.map +1 -0
  80. package/dist/src/cli/main.d.ts +0 -12
  81. package/dist/src/cli/main.js +72 -4
  82. package/dist/src/cli/main.js.map +1 -1
  83. package/dist/src/cli/muse.d.ts +2 -0
  84. package/dist/src/cli/muse.js +62 -0
  85. package/dist/src/cli/muse.js.map +1 -0
  86. package/dist/src/cli/payload.js +79 -1
  87. package/dist/src/cli/payload.js.map +1 -1
  88. package/dist/src/cli/scaffold.js +6 -0
  89. package/dist/src/cli/scaffold.js.map +1 -1
  90. package/dist/src/cli/serve.d.ts +53 -0
  91. package/dist/src/cli/serve.js +270 -0
  92. package/dist/src/cli/serve.js.map +1 -0
  93. package/dist/src/cli/up.js +26 -2
  94. package/dist/src/cli/up.js.map +1 -1
  95. package/dist/src/cli/verb-registry.d.ts +2 -0
  96. package/dist/src/cli/verb-registry.js +338 -39
  97. package/dist/src/cli/verb-registry.js.map +1 -1
  98. package/dist/src/cli/wordmark.d.ts +2 -2
  99. package/dist/src/cli/wordmark.js +2 -2
  100. package/dist/src/core/attest-bootstrap.d.ts +95 -0
  101. package/dist/src/core/attest-bootstrap.js +135 -0
  102. package/dist/src/core/attest-bootstrap.js.map +1 -0
  103. package/dist/src/core/attest.d.ts +20 -3
  104. package/dist/src/core/attest.js +25 -0
  105. package/dist/src/core/attest.js.map +1 -1
  106. package/dist/src/core/audit.d.ts +153 -12
  107. package/dist/src/core/audit.js +415 -20
  108. package/dist/src/core/audit.js.map +1 -1
  109. package/dist/src/core/channel-lease.d.ts +322 -0
  110. package/dist/src/core/channel-lease.js +663 -0
  111. package/dist/src/core/channel-lease.js.map +1 -0
  112. package/dist/src/core/command-class.d.ts +51 -0
  113. package/dist/src/core/command-class.js +766 -8
  114. package/dist/src/core/command-class.js.map +1 -1
  115. package/dist/src/core/daemon-host.d.ts +178 -0
  116. package/dist/src/core/daemon-host.js +228 -0
  117. package/dist/src/core/daemon-host.js.map +1 -0
  118. package/dist/src/core/daemon-identity.d.ts +238 -0
  119. package/dist/src/core/daemon-identity.js +252 -0
  120. package/dist/src/core/daemon-identity.js.map +1 -0
  121. package/dist/src/core/gate.d.ts +364 -6
  122. package/dist/src/core/gate.js +802 -27
  123. package/dist/src/core/gate.js.map +1 -1
  124. package/dist/src/core/harness-version.d.ts +128 -4
  125. package/dist/src/core/harness-version.js +179 -12
  126. package/dist/src/core/harness-version.js.map +1 -1
  127. package/dist/src/core/harness-wait.d.ts +82 -3
  128. package/dist/src/core/harness-wait.js +89 -3
  129. package/dist/src/core/harness-wait.js.map +1 -1
  130. package/dist/src/core/log-lock.d.ts +435 -0
  131. package/dist/src/core/log-lock.js +1093 -0
  132. package/dist/src/core/log-lock.js.map +1 -0
  133. package/dist/src/core/log-subscribe.d.ts +21 -0
  134. package/dist/src/core/log-subscribe.js +16 -6
  135. package/dist/src/core/log-subscribe.js.map +1 -1
  136. package/dist/src/core/log-write-layer.d.ts +55 -0
  137. package/dist/src/core/log-write-layer.js +91 -0
  138. package/dist/src/core/log-write-layer.js.map +1 -0
  139. package/dist/src/core/log.d.ts +130 -22
  140. package/dist/src/core/log.js +473 -50
  141. package/dist/src/core/log.js.map +1 -1
  142. package/dist/src/core/payload-census.d.ts +51 -0
  143. package/dist/src/core/payload-census.js +125 -2
  144. package/dist/src/core/payload-census.js.map +1 -1
  145. package/dist/src/core/payload-store.d.ts +31 -0
  146. package/dist/src/core/payload-store.js +113 -8
  147. package/dist/src/core/payload-store.js.map +1 -1
  148. package/dist/src/core/payload.d.ts +18 -3
  149. package/dist/src/core/payload.js +20 -4
  150. package/dist/src/core/payload.js.map +1 -1
  151. package/dist/src/core/policy-diff.d.ts +10 -0
  152. package/dist/src/core/policy-diff.js +10 -2
  153. package/dist/src/core/policy-diff.js.map +1 -1
  154. package/dist/src/core/policy-explain.d.ts +7 -1
  155. package/dist/src/core/policy-explain.js +16 -1
  156. package/dist/src/core/policy-explain.js.map +1 -1
  157. package/dist/src/core/policy-load.d.ts +24 -0
  158. package/dist/src/core/policy-load.js.map +1 -1
  159. package/dist/src/core/policy-match.d.ts +38 -0
  160. package/dist/src/core/policy-match.js +35 -0
  161. package/dist/src/core/policy-match.js.map +1 -1
  162. package/dist/src/core/policy-proposal.d.ts +40 -0
  163. package/dist/src/core/policy-proposal.js +37 -16
  164. package/dist/src/core/policy-proposal.js.map +1 -1
  165. package/dist/src/core/prompt-layout.d.ts +186 -1
  166. package/dist/src/core/prompt-layout.js +345 -3
  167. package/dist/src/core/prompt-layout.js.map +1 -1
  168. package/dist/src/core/read-scope.d.ts +10 -0
  169. package/dist/src/core/read-scope.js +4 -0
  170. package/dist/src/core/read-scope.js.map +1 -1
  171. package/dist/src/core/run-payload.d.ts +160 -0
  172. package/dist/src/core/run-payload.js +304 -0
  173. package/dist/src/core/run-payload.js.map +1 -0
  174. package/dist/src/core/sender-identity.d.ts +59 -11
  175. package/dist/src/core/sender-identity.js +79 -11
  176. package/dist/src/core/sender-identity.js.map +1 -1
  177. package/dist/src/core/state.d.ts +52 -1
  178. package/dist/src/core/state.js +52 -6
  179. package/dist/src/core/state.js.map +1 -1
  180. package/dist/src/core/telegram-config.d.ts +79 -0
  181. package/dist/src/core/telegram-config.js +108 -0
  182. package/dist/src/core/telegram-config.js.map +1 -1
  183. package/dist/src/core/validate.d.ts +20 -6
  184. package/dist/src/core/validate.js +0 -0
  185. package/dist/src/core/validate.js.map +1 -1
  186. package/dist/src/core/verify.d.ts +23 -1
  187. package/dist/src/core/verify.js +20 -2
  188. package/dist/src/core/verify.js.map +1 -1
  189. package/dist/src/daemon/audit.d.ts +80 -5
  190. package/dist/src/daemon/audit.js +126 -6
  191. package/dist/src/daemon/audit.js.map +1 -1
  192. package/dist/src/daemon/daemon.d.ts +174 -2
  193. package/dist/src/daemon/daemon.js +298 -7
  194. package/dist/src/daemon/daemon.js.map +1 -1
  195. package/dist/src/daemon/git-evidence.d.ts +2 -2
  196. package/dist/src/daemon/git-evidence.js +1 -1
  197. package/dist/src/daemon/projection.d.ts +13 -2
  198. package/dist/src/daemon/projection.js +39 -6
  199. package/dist/src/daemon/projection.js.map +1 -1
  200. package/dist/src/mcp/server.d.ts +39 -3
  201. package/dist/src/mcp/server.js +22 -3
  202. package/dist/src/mcp/server.js.map +1 -1
  203. package/dist/src/muse/server.d.ts +16 -0
  204. package/dist/src/muse/server.js +238 -0
  205. package/dist/src/muse/server.js.map +1 -0
  206. package/dist/src/serve/archive.d.ts +166 -0
  207. package/dist/src/serve/archive.js +363 -0
  208. package/dist/src/serve/archive.js.map +1 -0
  209. package/dist/src/serve/arguments.d.ts +93 -0
  210. package/dist/src/serve/arguments.js +228 -0
  211. package/dist/src/serve/arguments.js.map +1 -0
  212. package/dist/src/serve/credentials.d.ts +91 -0
  213. package/dist/src/serve/credentials.js +143 -0
  214. package/dist/src/serve/credentials.js.map +1 -0
  215. package/dist/src/serve/follow.d.ts +86 -0
  216. package/dist/src/serve/follow.js +101 -0
  217. package/dist/src/serve/follow.js.map +1 -0
  218. package/dist/src/serve/hook-thread.d.ts +134 -0
  219. package/dist/src/serve/hook-thread.js +333 -0
  220. package/dist/src/serve/hook-thread.js.map +1 -0
  221. package/dist/src/serve/hook-worker.d.ts +134 -0
  222. package/dist/src/serve/hook-worker.js +173 -0
  223. package/dist/src/serve/hook-worker.js.map +1 -0
  224. package/dist/src/serve/server.d.ts +412 -0
  225. package/dist/src/serve/server.js +1251 -0
  226. package/dist/src/serve/server.js.map +1 -0
  227. package/docs/README-extended.md +768 -0
  228. package/docs/cli-reference.md +2139 -67
  229. package/package.json +4 -2
  230. package/schema/event.schema.json +413 -30
  231. package/schema/fixtures/event/invalid/audit-lock-reclaimed-age-without-created.json +19 -0
  232. package/schema/fixtures/event/invalid/audit-lock-reclaimed-agent-actor.json +14 -0
  233. package/schema/fixtures/event/invalid/audit-lock-reclaimed-lockfile-path.json +14 -0
  234. package/schema/fixtures/event/invalid/audit-lock-reclaimed-operator-cleared-system-actor.json +14 -0
  235. package/schema/fixtures/event/invalid/audit-lock-reclaimed-unknown-reason.json +14 -0
  236. package/schema/fixtures/event/invalid/audit-lock-reclaimed-without-age.json +13 -0
  237. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +6 -1
  238. package/schema/fixtures/event/invalid/audit-reviewed-bad-payload-hash.json +21 -0
  239. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +5 -0
  240. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-hash.json +19 -0
  241. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-seq.json +19 -0
  242. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict-source.json +19 -0
  243. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict.json +19 -0
  244. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +6 -1
  245. package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
  246. package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
  247. package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
  248. package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
  249. package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
  250. package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
  251. package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
  252. package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
  253. package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
  254. package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
  255. package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
  256. package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
  257. package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
  258. package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
  259. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
  260. package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
  261. package/schema/fixtures/event/valid/audit-sampled.json +2 -1
  262. package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
  263. package/schema/fixtures/event/valid/daemon-identity.json +16 -0
  264. package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
  265. package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
  266. package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
  267. package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
  268. package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
  269. package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
  270. package/schema/policy.schema.json +102 -6
@@ -0,0 +1,273 @@
1
+ /**
2
+ * The Telegram webhook receiver (SPEC.md §10.3, APRV-424).
3
+ *
4
+ * §10.3 has always written the channel interface as `notify(request) ->
5
+ * delivery_id`, `poll()/webhook() -> decision`. Until this task only the first
6
+ * half of the second clause existed: the Telegram channel long-polled
7
+ * `getUpdates`, which needs one always-running process per bot and forbids a
8
+ * second poller on the same token. This is the other half. Telegram posts each
9
+ * update to a URL the runtime registered, and a host that sleeps between
10
+ * requests can therefore serve a gate.
11
+ *
12
+ * ## It decides nothing, and it barely reads
13
+ *
14
+ * This file checks one header, one path, one method and one body size, parses
15
+ * JSON, and hands the object to {@link TelegramChannel.deliverUpdate}. Which
16
+ * account the Bot API attributed the tap to, which approver the attested
17
+ * policy maps that account to, whether the gate will take the answer, and what
18
+ * the message is edited to say afterwards are all decided exactly where they
19
+ * were decided before this task existed — in `channels/telegram.ts`'s
20
+ * `handleUpdate` and in `channels/contract.ts`'s `recordChannelDecision`,
21
+ * reached by the same call the poll loop makes. An update that arrives here is
22
+ * an update; it is not evidence about itself.
23
+ *
24
+ * ## The secret is the whole of the authentication
25
+ *
26
+ * A webhook URL is a public endpoint. Anyone who can route to it can POST a
27
+ * JSON object that LOOKS like a tap from the approver's chat, and nothing in
28
+ * the body could tell the difference: SPEC.md §11.1 invariant 4 — a
29
+ * self-reported field never reduces scrutiny — is exactly the rule that makes
30
+ * a body-only check worthless here. So Telegram's own `secret_token` is
31
+ * REQUIRED rather than optional. It is set on `setWebhook`, echoed on every
32
+ * delivery in `X-Telegram-Bot-Api-Secret-Token`, compared here in constant
33
+ * time, and a post without the matching value is refused before its path, its
34
+ * method or its body is looked at.
35
+ *
36
+ * Three consequences, each deliberate:
37
+ *
38
+ * - **The comparison is over SHA-256 digests under `timingSafeEqual`**, which
39
+ * is constant-time AND length-independent (`timingSafeEqual` throws on a
40
+ * length mismatch, which would itself be an oracle for the secret's length).
41
+ * `serve/credentials.ts` states the same argument for the same reason.
42
+ * - **Duplicate headers are refused rather than resolved.** Node joins
43
+ * repeated headers, so two of them would make the value this server compares
44
+ * differ from the one a reader of the request would name. There is no
45
+ * correct choice; the request is malformed.
46
+ * - **A refusal appends NOTHING to the log.** It is counted, complained about
47
+ * on the runner's stderr and answered with its own machine-readable code,
48
+ * and it never reaches the decision path. This is `TELEGRAM_ANOMALY_KINDS`'s
49
+ * rule, and it matters more here than it does there: writing "somebody
50
+ * posted a forgery" into an append-only approval log would let anyone who
51
+ * can reach this URL grow the record a human is asked to trust. A refusal is
52
+ * a fact about this transport, and this transport reports it where facts
53
+ * about transports belong.
54
+ *
55
+ * ## What is reused from `approval serve`, and what could not be
56
+ *
57
+ * The shape is APRV-421's, deliberately: loopback bind by default with the
58
+ * operator's proxy terminating TLS, the credential checked before the URL is
59
+ * parsed, a refusal carried as a body with a frozen code rather than as a bare
60
+ * status, an oversized body drained before the refusal is written (a response
61
+ * written while the client is still uploading closes the socket under it), and
62
+ * no state a restart loses.
63
+ *
64
+ * Its CODE could not be reused, and the reason is structural rather than
65
+ * stylistic. `src/serve/` imports `src/cli/`, which imports `src/channels/`;
66
+ * an import from here back into `src/serve/` would close an ESM cycle, which
67
+ * is not a compile error but a binding that is `undefined` in one direction on
68
+ * the day module initialisation order changes. `tests/layering.test.ts` pins
69
+ * that direction. The two servers are also different surfaces: that one
70
+ * answers two bearer credentials for two parties across six routes, this one
71
+ * answers Telegram's echoed secret on one.
72
+ *
73
+ * ## One update at a time
74
+ *
75
+ * Deliveries are serialized through a promise chain. `handleUpdate` keeps a
76
+ * single callback-query ack slot and the delivery maps are plain `Map`s, so
77
+ * two updates handled concurrently would interleave in ways nobody ordered.
78
+ * The poll loop gets this for free by awaiting each update in turn; an HTTP
79
+ * server has to say it.
80
+ *
81
+ * ## Stopping waits for the update it is holding
82
+ *
83
+ * A stop closes the listener, drains what is in flight, and only then drops
84
+ * the sockets. The first version of this file did the opposite — it destroyed
85
+ * every socket synchronously and resolved in about a millisecond — so
86
+ * `deleteWebhook`, the "stopped" line and the process's exit all ran while an
87
+ * update was still being handled: an append in progress, a dispatch cycle
88
+ * halfway through a send, and a caller whose response never arrived. The order
89
+ * is now stop accepting, drain, drop, and the verb's own stop path does its
90
+ * `deleteWebhook` after this resolves rather than beside it.
91
+ *
92
+ * ## Redelivery is safe because the gate is
93
+ *
94
+ * Telegram retries a delivery it did not get a 2xx for, so the same tap can
95
+ * arrive twice. Nothing here deduplicates, and nothing needs to: the second
96
+ * one reaches `decide()` through the same path as the first and is refused
97
+ * `already-decided` with the first human answer standing. That is the same
98
+ * property a button pressed twice has had since APRV-26.
99
+ */
100
+ import type { TelegramChannel, TelegramPollResult } from "./telegram.js";
101
+ /**
102
+ * The header Telegram echoes the `secret_token` in, lowercased as Node
103
+ * presents it.
104
+ */
105
+ export declare const TELEGRAM_SECRET_HEADER = "x-telegram-bot-api-secret-token";
106
+ /** The path this server answers when the operator names none. */
107
+ export declare const TELEGRAM_WEBHOOK_DEFAULT_PATH = "/telegram/webhook";
108
+ /** The interface it binds when the operator names none. Loopback, always. */
109
+ export declare const TELEGRAM_WEBHOOK_DEFAULT_HOST = "127.0.0.1";
110
+ /** The port it binds when the operator names none. */
111
+ export declare const TELEGRAM_WEBHOOK_DEFAULT_PORT = 4683;
112
+ /**
113
+ * Largest body accepted. A Telegram update is a small JSON object; the
114
+ * largest thing one can carry is a message's 4096-character text.
115
+ */
116
+ export declare const TELEGRAM_WEBHOOK_MAX_BODY_BYTES: number;
117
+ /**
118
+ * How much of an oversized body is read and thrown away before the socket is
119
+ * dropped. `serve/server.ts` carries the argument: the refusal cannot be
120
+ * written until the client stops writing, or the response closes the socket
121
+ * under it and the caller sees a transport error instead of a refusal.
122
+ */
123
+ export declare const TELEGRAM_WEBHOOK_DRAIN_LIMIT_BYTES: number;
124
+ /**
125
+ * How long {@link TelegramWebhookHandle.close} waits for requests already in
126
+ * flight before it destroys their sockets (APRV-424, review finding 2).
127
+ *
128
+ * A stop has to end. What this bounds is the pathological case — a client that
129
+ * opened a request and stopped writing, a handler that will never return — and
130
+ * not the ordinary one, which is an update whose decision is being appended
131
+ * and takes milliseconds. The ordinary one is what the drain exists for: the
132
+ * first version of this file destroyed every socket synchronously and
133
+ * resolved in about a millisecond, so `deleteWebhook`, the "stopped" line and
134
+ * the process's exit all happened while an append and a dispatch were still
135
+ * running.
136
+ */
137
+ export declare const TELEGRAM_WEBHOOK_CLOSE_TIMEOUT_MS = 10000;
138
+ /** How long the close drain waits between checks of the in-flight count. */
139
+ export declare const TELEGRAM_WEBHOOK_DRAIN_POLL_MS = 5;
140
+ /**
141
+ * Everything this receiver refuses, and nothing else. Frozen, per SPEC.md
142
+ * §11.1 invariant 6: a refusal is machine-readable and distinct.
143
+ *
144
+ * Distinct because the repairs are distinct. A secret mismatch is a forgery or
145
+ * a rotation the registration did not follow; an unknown path is a URL that
146
+ * does not match the one registered; a body too large is a caller that is not
147
+ * Telegram. None of them is a decision, and none of them is the same fact.
148
+ */
149
+ export declare const TELEGRAM_WEBHOOK_REFUSAL_CODES: readonly [
150
+ /** No `X-Telegram-Bot-Api-Secret-Token`, or one that is not the registered value. */
151
+ "webhook-secret-mismatch",
152
+ /** More than one secret header. Malformed, and not resolvable by choosing. */
153
+ "webhook-duplicate-secret-header",
154
+ /**
155
+ * The request line and the `Host` header do not form a URL (400).
156
+ *
157
+ * Split from `webhook-unknown-path` in the review of this task: one code for
158
+ * both collapsed "what arrived is not a request this server can parse" into
159
+ * "you are posting to a path this server does not serve", and those two
160
+ * share nothing but a status family — the first is a broken proxy or a
161
+ * hand-written client, the second is a registration that does not match the
162
+ * bind. `src/serve/server.ts` makes the same split, between
163
+ * `serve-malformed-url` and `serve-unknown-path`.
164
+ */
165
+ "webhook-malformed-request",
166
+ /** A path this server does not serve (404). */
167
+ "webhook-unknown-path",
168
+ /** The right path, the wrong method. Telegram POSTs. */
169
+ "webhook-method-not-allowed",
170
+ /** Larger than {@link TELEGRAM_WEBHOOK_MAX_BODY_BYTES}. */
171
+ "webhook-body-too-large",
172
+ /** The body could not be read off the socket, or is not JSON. */
173
+ "webhook-body-unreadable",
174
+ /** JSON that is not an Update object. */
175
+ "webhook-not-an-update",
176
+ /** The channel is already receiving updates by the other transport. */
177
+ "webhook-transport-conflict",
178
+ /** The channel threw while handling the update. Nothing is claimed about the log. */
179
+ "webhook-handler-failed"];
180
+ export type TelegramWebhookRefusalCode = (typeof TELEGRAM_WEBHOOK_REFUSAL_CODES)[number];
181
+ /** What this receiver has done since it bound. Counters, never decisions. */
182
+ export interface TelegramWebhookStats {
183
+ /** Requests answered, of any kind. */
184
+ requests: number;
185
+ /** Updates handed to the channel. */
186
+ updates: number;
187
+ /** Refusals, by code. Never a decision, never a log event. */
188
+ refusals: Record<TelegramWebhookRefusalCode, number>;
189
+ }
190
+ export interface TelegramWebhookOptions {
191
+ /** The channel every accepted update is handed to. */
192
+ channel: TelegramChannel;
193
+ /**
194
+ * The registered `secret_token`, as a value.
195
+ *
196
+ * Resolved by the VERB from the launch environment (SPEC.md §11.1 invariant
197
+ * 7: configuration is never loaded implicitly from the working tree).
198
+ * Nothing under `src/channels/` reads `process.env`, here as everywhere.
199
+ */
200
+ secret: string;
201
+ /** The path to answer. Defaults to {@link TELEGRAM_WEBHOOK_DEFAULT_PATH}. */
202
+ path?: string;
203
+ /** Interface to bind. Defaults to loopback; the CLI owns any widening. */
204
+ host?: string;
205
+ /** TCP port. `0` asks the kernel for an ephemeral one, which is what tests use. */
206
+ port?: number;
207
+ /** Where operational complaints go. Defaults to stderr. */
208
+ log?: (message: string) => void;
209
+ /**
210
+ * How long {@link TelegramWebhookHandle.close} drains before it drops what
211
+ * is left. Defaults to {@link TELEGRAM_WEBHOOK_CLOSE_TIMEOUT_MS}; a test
212
+ * that deliberately holds a handler open shortens it.
213
+ */
214
+ closeTimeoutMs?: number;
215
+ /**
216
+ * Run after each update the channel accepted (APRV-424).
217
+ *
218
+ * This is where the runtime's dispatch cycle goes, and it is the webhook's
219
+ * equivalent of the listener's `beforePoll`: the channel holds
220
+ * no queue and reads no log, so putting the next pending request on the
221
+ * phone after a tap is the caller's job. It MUST NOT throw; a rejection is
222
+ * complained about and swallowed, because a dispatch that failed must not
223
+ * turn an answered tap into an unanswered request.
224
+ */
225
+ afterUpdate?: (result: TelegramPollResult) => Promise<void> | void;
226
+ }
227
+ export interface TelegramWebhookHandle {
228
+ readonly host: string;
229
+ readonly port: number;
230
+ readonly path: string;
231
+ stats(): TelegramWebhookStats;
232
+ /** Run `work` behind the same queue update delivery uses. */
233
+ serialize<T>(work: () => Promise<T>): Promise<T>;
234
+ /** Requests this receiver has accepted and not yet answered. */
235
+ inFlight(): number;
236
+ /**
237
+ * Stop accepting, let what is in flight finish, then drop the sockets.
238
+ *
239
+ * In that order, and the order is the fix for review finding 2: a close that
240
+ * destroyed sockets first would tear the response off an update whose
241
+ * decision was mid-append, and would hand its caller back control in time
242
+ * to call `deleteWebhook` and print "stopped" over the top of it. Idempotent
243
+ * and safe to await twice; a second call awaits the first.
244
+ */
245
+ close(): Promise<void>;
246
+ }
247
+ /**
248
+ * Is `offered` the registered secret?
249
+ *
250
+ * Both sides are hashed first, so the comparison is over two equal-length
251
+ * buffers and `timingSafeEqual` can never throw — which is what keeps the
252
+ * length of the real secret out of the answer.
253
+ */
254
+ export declare function secretMatches(expected: string, offered: string | null): boolean;
255
+ /**
256
+ * The one secret header a request offered, `null` for none, and `"duplicate"`
257
+ * for more than one.
258
+ *
259
+ * Read off `rawHeaders` rather than `headers`, because Node joins repeats of a
260
+ * non-special header into one comma-separated value and the join is exactly
261
+ * what hides the second one.
262
+ */
263
+ export declare function secretHeaderOf(rawHeaders: readonly string[]): string | null | "duplicate";
264
+ /**
265
+ * Start the receiver. Resolves once it is bound and its real port is known;
266
+ * rejects when the bind fails, so the verb can report it and exit.
267
+ *
268
+ * The channel's transport is claimed BEFORE the listener exists, for the
269
+ * reason `approval serve` settles its credentials first: a server that bound
270
+ * and then discovered it could not receive would be a server Telegram had
271
+ * already posted to.
272
+ */
273
+ export declare function serveTelegramWebhook(options: TelegramWebhookOptions): Promise<TelegramWebhookHandle>;