blun-king-cli 9.1.587 → 9.1.588

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 (436) hide show
  1. package/CHANGELOG.md +11 -185
  2. package/LIESMICH.txt +51 -13
  3. package/README.md +44 -47
  4. package/agent-spine-plugin/.codex-plugin/plugin.json +16 -4
  5. package/agent-spine-plugin/CHANGELOG.md +37 -5
  6. package/agent-spine-plugin/README.md +3 -3
  7. package/agent-spine-plugin/blun.plugin.json +45 -10
  8. package/agent-spine-plugin/docs/artifact-evaluation.md +93 -0
  9. package/agent-spine-plugin/docs/host-integration.md +42 -27
  10. package/agent-spine-plugin/docs/preflight-recall.md +4 -2
  11. package/agent-spine-plugin/docs/session-timeline.md +97 -236
  12. package/agent-spine-plugin/docs/world-model.md +25 -0
  13. package/agent-spine-plugin/hooks/codex.json +1 -1
  14. package/agent-spine-plugin/hooks/hooks.json +1 -1
  15. package/agent-spine-plugin/package.json +1 -3
  16. package/agent-spine-plugin/scripts/check-hosts.js +3 -3
  17. package/agent-spine-plugin/scripts/release-check.js +10 -5
  18. package/agent-spine-plugin/scripts/run-checks.js +4 -1
  19. package/agent-spine-plugin/scripts/run-tests-hermetic.js +32 -6
  20. package/agent-spine-plugin/src/cli-learning.js +15 -0
  21. package/agent-spine-plugin/src/cli.js +2 -0
  22. package/agent-spine-plugin/src/hook.js +32 -32
  23. package/agent-spine-plugin/src/lib/action-lesson-recall.js +73 -8
  24. package/agent-spine-plugin/src/lib/briefing.js +146 -36
  25. package/agent-spine-plugin/src/lib/channel-continuity.js +19 -0
  26. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +14 -7
  27. package/agent-spine-plugin/src/lib/gateway-group-response.js +128 -0
  28. package/agent-spine-plugin/src/lib/gateway-runs.js +24 -15
  29. package/agent-spine-plugin/src/lib/hook-briefing-use.js +13 -3
  30. package/agent-spine-plugin/src/lib/hook-context.js +16 -3
  31. package/agent-spine-plugin/src/lib/hook-output.js +129 -5
  32. package/agent-spine-plugin/src/lib/hook-timeline.js +5 -3
  33. package/agent-spine-plugin/src/lib/indexed-memory.js +2 -2
  34. package/agent-spine-plugin/src/lib/learning-artifact-evaluator.js +114 -0
  35. package/agent-spine-plugin/src/lib/learning-context.js +11 -4
  36. package/agent-spine-plugin/src/lib/learning-measurements.js +2 -2
  37. package/agent-spine-plugin/src/lib/mcp-runtime.js +89 -3
  38. package/agent-spine-plugin/src/lib/mcp-source-context.js +12 -2
  39. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +91 -8
  40. package/agent-spine-plugin/src/lib/mcp-world-tools.js +2 -2
  41. package/agent-spine-plugin/src/lib/owned-file-lock.js +20 -1
  42. package/agent-spine-plugin/src/lib/persona-runtime.js +2 -2
  43. package/agent-spine-plugin/src/lib/preflight-delivery-id.js +27 -0
  44. package/agent-spine-plugin/src/lib/preflight.js +4 -4
  45. package/agent-spine-plugin/src/lib/session-timeline-codex.js +15 -0
  46. package/agent-spine-plugin/src/lib/session-timeline-contract.js +12 -4
  47. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +36 -7
  48. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +13 -10
  49. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +1 -1
  50. package/agent-spine-plugin/src/lib/session-timeline-king.js +14 -0
  51. package/agent-spine-plugin/src/lib/session-timeline-prior.js +18 -12
  52. package/agent-spine-plugin/src/lib/session-timeline-provider.js +5 -0
  53. package/agent-spine-plugin/src/lib/session-timeline-query.js +2 -0
  54. package/agent-spine-plugin/src/lib/session-timeline-results.js +35 -10
  55. package/agent-spine-plugin/src/lib/session-timeline-source-open.js +30 -0
  56. package/agent-spine-plugin/src/lib/session-timeline.js +122 -75
  57. package/agent-spine-plugin/src/lib/source-roots.js +3 -2
  58. package/agent-spine-plugin/src/lib/task-knowledge-context.js +22 -1
  59. package/agent-spine-plugin/src/lib/timeline-continuation-update.js +100 -0
  60. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +30 -7
  61. package/agent-spine-plugin/src/lib/timeline-user-feedback.js +217 -0
  62. package/agent-spine-plugin/src/lib/timeline-world-capture.js +233 -0
  63. package/agent-spine-plugin/src/lib/world-knowledge.js +59 -2
  64. package/agent-spine-plugin/src/lib/world-model.js +64 -9
  65. package/agent-spine-plugin/src/worker.js +13 -1
  66. package/bin/blun.js +43 -28
  67. package/bin/core-bootstrap.js +5 -4
  68. package/bin/king.js +43 -28
  69. package/bin/launcher-mode.js +1 -10
  70. package/bin/launcher-runtime.js +128 -295
  71. package/bin/managed-node.js +0 -0
  72. package/bin/managed-plugin-selection.cjs +0 -1
  73. package/bin/native-module-repair.js +0 -0
  74. package/bin/node-runtime.js +0 -0
  75. package/bin/node-version.js +0 -0
  76. package/bin/plugin-bootstrap.js +56 -120
  77. package/bin/private-paths.js +11 -34
  78. package/bin/standard-tools-bootstrap.js +34 -114
  79. package/bin/turn-thinking-policy.cjs +3 -11
  80. package/bin/update-copy.js +200 -0
  81. package/bin/update-lease.js +0 -0
  82. package/bin/update-notice.js +136 -289
  83. package/bin/verify-agent-behavior.cjs +122 -0
  84. package/bin/verify-agent-components.cjs +104 -0
  85. package/bin/verify-bundled-agent-sources.cjs +57 -0
  86. package/blun.mjs +143076 -135288
  87. package/bundled-agent-sources.json +701 -0
  88. package/package.json +12 -15
  89. package/standard-skills/translate-native/README.md +1293 -0
  90. package/standard-skills/translate-native/SKILL.md +172 -22
  91. package/standard-skills/translate-native/VERSION +1 -1
  92. package/standard-skills/translate-native/agents/openai.yaml +18 -0
  93. package/standard-skills/translate-native/assets/icon.svg +8 -0
  94. package/standard-skills/translate-native/docs/BLUN_CODE_INTEGRATION.md +76 -0
  95. package/standard-skills/translate-native/docs/PREMORTEM.md +489 -0
  96. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +2035 -0
  97. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1302 -0
  98. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +136 -0
  99. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HEALTH_HTTP.md +130 -0
  100. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +175 -0
  101. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +86 -0
  102. package/standard-skills/translate-native/integrations/AGENT_RULES.md +32 -0
  103. package/standard-skills/translate-native/integrations/adapters/blun-code-language-guard.js +514 -0
  104. package/standard-skills/translate-native/integrations/adapters/node-language-guard.js +230 -0
  105. package/standard-skills/translate-native/integrations/audit_log.py +327 -0
  106. package/standard-skills/translate-native/integrations/claude_language_hook.js +1536 -0
  107. package/standard-skills/translate-native/integrations/commercial_localization_profile.py +42 -0
  108. package/standard-skills/translate-native/integrations/delivery-policy.example.json +28 -0
  109. package/standard-skills/translate-native/integrations/enforced_delivery.py +543 -0
  110. package/standard-skills/translate-native/integrations/guard_service.py +435 -0
  111. package/standard-skills/translate-native/integrations/language_gateway.py +67 -0
  112. package/standard-skills/translate-native/integrations/mcp_auth_headers.py +198 -0
  113. package/standard-skills/translate-native/integrations/mcp_http_gateway.py +429 -0
  114. package/standard-skills/translate-native/integrations/non_language_html_entities.js +1485 -0
  115. package/standard-skills/translate-native/integrations/pre_output_guard.py +65 -0
  116. package/standard-skills/translate-native/integrations/task_router.py +101 -0
  117. package/standard-skills/translate-native/integrations/website_localization.py +401 -0
  118. package/standard-skills/translate-native/integrations/website_localization_api.py +581 -0
  119. package/standard-skills/translate-native/integrations/website_localization_benchmark.py +1885 -0
  120. package/standard-skills/translate-native/integrations/website_localization_benchmark_campaign.py +1772 -0
  121. package/standard-skills/translate-native/integrations/website_localization_benchmark_candidate.py +506 -0
  122. package/standard-skills/translate-native/integrations/website_localization_benchmark_http.py +400 -0
  123. package/standard-skills/translate-native/integrations/website_localization_benchmark_review_store.py +781 -0
  124. package/standard-skills/translate-native/integrations/website_localization_benchmark_reviewer_http.py +500 -0
  125. package/standard-skills/translate-native/integrations/website_localization_benchmark_runtime.py +1107 -0
  126. package/standard-skills/translate-native/integrations/website_localization_benchmark_suite.py +463 -0
  127. package/standard-skills/translate-native/integrations/website_localization_cms.py +2835 -0
  128. package/standard-skills/translate-native/integrations/website_localization_cms_client.py +875 -0
  129. package/standard-skills/translate-native/integrations/website_localization_cms_dispatch.py +805 -0
  130. package/standard-skills/translate-native/integrations/website_localization_cms_http.py +588 -0
  131. package/standard-skills/translate-native/integrations/website_localization_cms_lifecycle_monitor.py +991 -0
  132. package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +1441 -0
  133. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +414 -0
  134. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +1073 -0
  135. package/standard-skills/translate-native/integrations/website_localization_cms_removal_dispatch.py +865 -0
  136. package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +583 -0
  137. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +964 -0
  138. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +665 -0
  139. package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +1153 -0
  140. package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +675 -0
  141. package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +1125 -0
  142. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification.py +674 -0
  143. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +444 -0
  144. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +1469 -0
  145. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +1142 -0
  146. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +634 -0
  147. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +804 -0
  148. package/standard-skills/translate-native/integrations/website_localization_deepl_baseline.py +922 -0
  149. package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +482 -0
  150. package/standard-skills/translate-native/integrations/website_localization_health.py +1541 -0
  151. package/standard-skills/translate-native/integrations/website_localization_health_http.py +372 -0
  152. package/standard-skills/translate-native/integrations/website_localization_http_provider.py +297 -0
  153. package/standard-skills/translate-native/integrations/website_localization_native_reference_http.py +479 -0
  154. package/standard-skills/translate-native/integrations/website_localization_native_reference_intake.py +363 -0
  155. package/standard-skills/translate-native/integrations/website_localization_native_reference_queue.py +1449 -0
  156. package/standard-skills/translate-native/integrations/website_localization_native_reference_store.py +420 -0
  157. package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +235 -0
  158. package/standard-skills/translate-native/integrations/website_localization_queue.py +671 -0
  159. package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +516 -0
  160. package/standard-skills/translate-native/integrations/website_localization_release.py +928 -0
  161. package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +1008 -0
  162. package/standard-skills/translate-native/integrations/website_localization_runner.py +276 -0
  163. package/standard-skills/translate-native/integrations/website_localization_runtime.py +862 -0
  164. package/standard-skills/translate-native/integrations/website_localization_service.py +350 -0
  165. package/standard-skills/translate-native/integrations/website_localization_supervisor.py +511 -0
  166. package/standard-skills/translate-native/integrations/website_localization_worker.py +663 -0
  167. package/standard-skills/translate-native/provenance.json +3 -4
  168. package/standard-skills/translate-native/references/commercial-localization.md +177 -0
  169. package/standard-skills/translate-native/scripts/blun_language_guard.py +7 -1
  170. package/standard-skills/translate-native/scripts/check_commercial_review.py +80 -0
  171. package/standard-skills/translate-native/scripts/commercial_localization_profile.py +333 -0
  172. package/standard-tools/language-guard/LICENSE +21 -0
  173. package/standard-tools/language-guard/VERSION +1 -0
  174. package/standard-tools/language-guard/blun_language_guard.py +7 -1
  175. package/standard-tools/language-guard/check_commercial_review.py +80 -0
  176. package/standard-tools/language-guard/commercial_localization_profile.py +333 -0
  177. package/standard-tools/language-guard/language_gateway.py +62 -0
  178. package/standard-tools/language-guard/pre_output_guard.py +64 -0
  179. package/standard-tools/language-guard/provenance.json +4 -11
  180. package/standard-tools/manifest.json +34 -11
  181. package/telegram-plugin/commands/access.md +2 -10
  182. package/telegram-plugin/dist/bridge.mjs +64041 -687
  183. package/telegram-plugin/dist/mcp-server.mjs +72810 -9027
  184. package/telegram-plugin/dist/noise.mjs +28 -63511
  185. package/agent-spine-plugin/CONTRIBUTING.md +0 -52
  186. package/agent-spine-plugin/SECURITY.md +0 -47
  187. package/agent-spine-plugin/docs/assignment-continuation.md +0 -48
  188. package/agent-spine-plugin/docs/releasing.md +0 -85
  189. package/agent-spine-plugin/docs/structured-completion.md +0 -67
  190. package/bin/abort-listener-policy.cjs +0 -43
  191. package/bin/active-steer-priority-policy.cjs +0 -24
  192. package/bin/agent-api-http-adapter.mjs +0 -446
  193. package/bin/agent-api-private-http-server.mjs +0 -288
  194. package/bin/agent-api-runtime.mjs +0 -252
  195. package/bin/agent-api-service-environment.mjs +0 -236
  196. package/bin/agent-api-service-host.mjs +0 -209
  197. package/bin/agent-api-service-process.mjs +0 -171
  198. package/bin/agent-api-session-registry.mjs +0 -428
  199. package/bin/agent-api-tool-broker.cjs +0 -248
  200. package/bin/agent-api-turn-controller.mjs +0 -461
  201. package/bin/agent-api-usage-journal.cjs +0 -259
  202. package/bin/agent-resume-snapshot.cjs +0 -241
  203. package/bin/agentspine-king-goal-inbox.mjs +0 -111
  204. package/bin/agentspine-king-goal-intake.mjs +0 -106
  205. package/bin/approval-rejection-stop.cjs +0 -15
  206. package/bin/assistant-message-offload-policy.cjs +0 -284
  207. package/bin/baseline-skill-performance-policy.cjs +0 -39
  208. package/bin/bash-search-scope-policy.cjs +0 -49
  209. package/bin/codebase-search-runtime.cjs +0 -23
  210. package/bin/cognitive-action-checkpoint.cjs +0 -1104
  211. package/bin/cognitive-attention-delivery.cjs +0 -76
  212. package/bin/cognitive-attention-policy.cjs +0 -143
  213. package/bin/cognitive-attention-runtime.cjs +0 -91
  214. package/bin/cognitive-context-projection.cjs +0 -73
  215. package/bin/cognitive-cross-portal-acceptance.cjs +0 -443
  216. package/bin/cognitive-effective-view.cjs +0 -77
  217. package/bin/cognitive-focus-projection.cjs +0 -206
  218. package/bin/cognitive-focus-scope.cjs +0 -37
  219. package/bin/cognitive-goal-autostart-policy.cjs +0 -72
  220. package/bin/cognitive-goal-time-trigger-controller.cjs +0 -146
  221. package/bin/cognitive-memory-adapter.cjs +0 -282
  222. package/bin/cognitive-memory-command.cjs +0 -293
  223. package/bin/cognitive-memory-provider.cjs +0 -92
  224. package/bin/cognitive-salience-policy.cjs +0 -159
  225. package/bin/cognitive-state-store.cjs +0 -508
  226. package/bin/cognitive-turn-lifecycle.cjs +0 -624
  227. package/bin/cognitive-work-focus.cjs +0 -180
  228. package/bin/compaction-history-archive.cjs +0 -166
  229. package/bin/compaction-history-startup.cjs +0 -50
  230. package/bin/compaction-model-policy.cjs +0 -31
  231. package/bin/compaction-stage-policy.cjs +0 -21
  232. package/bin/compaction-transaction-policy.cjs +0 -122
  233. package/bin/config-write-dedup-policy.cjs +0 -27
  234. package/bin/context-budget-ledger.cjs +0 -31
  235. package/bin/context-doctor-policy.cjs +0 -70
  236. package/bin/context-insight-policy.cjs +0 -36
  237. package/bin/context-performance-policy.cjs +0 -19
  238. package/bin/context-pressure-policy.cjs +0 -20
  239. package/bin/cron-run-output.cjs +0 -45
  240. package/bin/cron-run-store.cjs +0 -145
  241. package/bin/curiosity-scout-policy.cjs +0 -49
  242. package/bin/default-model-output-budget-policy.cjs +0 -28
  243. package/bin/durable-task-resume-policy.cjs +0 -130
  244. package/bin/durable-task-resume-runtime.cjs +0 -117
  245. package/bin/durable-task-resume-store.cjs +0 -88
  246. package/bin/editable-tool-approval-policy.cjs +0 -540
  247. package/bin/editable-tool-approval-runtime.cjs +0 -99
  248. package/bin/effective-system-prompt-cache-policy.cjs +0 -33
  249. package/bin/error-memory-performance-policy.cjs +0 -113
  250. package/bin/file-observation-policy.cjs +0 -133
  251. package/bin/foreground-output-capture-policy.cjs +0 -41
  252. package/bin/generated-source-health.cjs +0 -142
  253. package/bin/glob-pattern-policy.cjs +0 -13
  254. package/bin/goal-completion-evidence-policy.cjs +0 -120
  255. package/bin/grep-output-limit-policy.cjs +0 -39
  256. package/bin/historical-media-projection-policy.cjs +0 -48
  257. package/bin/history-offload-pressure-policy.cjs +0 -33
  258. package/bin/html-to-research-markdown.cjs +0 -147
  259. package/bin/identity-context-policy.cjs +0 -764
  260. package/bin/identity-journal-policy.cjs +0 -107
  261. package/bin/input-draft-persistence.cjs +0 -77
  262. package/bin/king-tui-function-contract.json +0 -33
  263. package/bin/launcher-restart-policy.cjs +0 -150
  264. package/bin/live-response-repetition-guard.cjs +0 -196
  265. package/bin/llm-config-log-dedup-policy.cjs +0 -76
  266. package/bin/loop-event-record-policy.cjs +0 -174
  267. package/bin/managed-context-startup-policy.cjs +0 -27
  268. package/bin/media-activity-layout-policy.cjs +0 -34
  269. package/bin/media-auto-retrieval-policy.cjs +0 -90
  270. package/bin/media-result-policy.cjs +0 -59
  271. package/bin/micro-compaction-policy.cjs +0 -145
  272. package/bin/mistake-relevance-policy.cjs +0 -319
  273. package/bin/model-retry-progress-policy.cjs +0 -46
  274. package/bin/native-large-file-io.cjs +0 -42
  275. package/bin/native-runtime-cache.cjs +0 -76
  276. package/bin/natural-presence-policy.cjs +0 -28
  277. package/bin/noninteractive-shell-env-policy.cjs +0 -19
  278. package/bin/observer-hooks.cjs +0 -14
  279. package/bin/outbound-claim-provenance.cjs +0 -150
  280. package/bin/oversized-context-offload-policy.cjs +0 -86
  281. package/bin/pending-media-policy.cjs +0 -182
  282. package/bin/pending-token-estimate-policy.cjs +0 -41
  283. package/bin/personal-memory-consent-policy.cjs +0 -72
  284. package/bin/personal-memory-performance-policy.cjs +0 -12
  285. package/bin/personality-choice-policy.cjs +0 -101
  286. package/bin/personality-memory-adapter.cjs +0 -379
  287. package/bin/personality-mode.cjs +0 -46
  288. package/bin/personality-setup-policy.cjs +0 -197
  289. package/bin/proactive-compaction-policy.cjs +0 -25
  290. package/bin/profile-identity-resolution.cjs +0 -136
  291. package/bin/profile-runtime.cjs +0 -318
  292. package/bin/profile-tool-exclusion-policy.cjs +0 -37
  293. package/bin/programmatic-context-isolation.cjs +0 -25
  294. package/bin/programmatic-tool-runtime.mjs +0 -627
  295. package/bin/provider-idle-timeout-policy.cjs +0 -14
  296. package/bin/provider-model-refresh-deadline.cjs +0 -53
  297. package/bin/provider-model-refresh-policy.cjs +0 -107
  298. package/bin/rate-limit-recovery-policy.cjs +0 -47
  299. package/bin/read-batch-policy.cjs +0 -32
  300. package/bin/read-continuation-policy.cjs +0 -59
  301. package/bin/recurring-cron-history-policy.cjs +0 -124
  302. package/bin/relationship-continuity-policy.cjs +0 -143
  303. package/bin/relationship-curiosity-policy.cjs +0 -107
  304. package/bin/relationship-learning-policy.cjs +0 -168
  305. package/bin/release-artifact-freeze-policy.cjs +0 -30
  306. package/bin/reload-plugin-bootstrap.cjs +0 -18
  307. package/bin/reload-queue-policy.cjs +0 -38
  308. package/bin/repeated-assistant-response-policy.cjs +0 -232
  309. package/bin/repeated-injection-projection.cjs +0 -107
  310. package/bin/repeated-user-message-projection.cjs +0 -8
  311. package/bin/research-page-result.cjs +0 -74
  312. package/bin/retry-checkpoint-policy.cjs +0 -13
  313. package/bin/runtime-exit-ledger.cjs +0 -144
  314. package/bin/scoped-cron-run-policy.cjs +0 -358
  315. package/bin/session-checkpoint-policy.cjs +0 -25
  316. package/bin/session-compaction-policy.cjs +0 -84
  317. package/bin/session-replay-policy.cjs +0 -20
  318. package/bin/session-replay-window-policy.cjs +0 -40
  319. package/bin/session-resume-checkpoint.cjs +0 -254
  320. package/bin/session-scrollback-archive.cjs +0 -229
  321. package/bin/skill-activation-performance-policy.cjs +0 -69
  322. package/bin/skill-listing-performance-policy.cjs +0 -92
  323. package/bin/soul-organization-policy.cjs +0 -78
  324. package/bin/soul-preservation-policy.cjs +0 -20
  325. package/bin/startup-preferences.cjs +0 -131
  326. package/bin/streaming-flush-performance-policy.cjs +0 -28
  327. package/bin/structured-agent-swarm-output.cjs +0 -325
  328. package/bin/structured-subagent-output.cjs +0 -252
  329. package/bin/subagent-context-fork-policy.cjs +0 -155
  330. package/bin/subagent-max-tokens-handoff-policy.cjs +0 -69
  331. package/bin/subagent-parent-responsiveness.cjs +0 -19
  332. package/bin/subagent-skill-policy.cjs +0 -206
  333. package/bin/subagent-timeout-policy.cjs +0 -182
  334. package/bin/subagent-tool-policy.cjs +0 -60
  335. package/bin/subagent-usage-rollup-policy.cjs +0 -29
  336. package/bin/system-prompt-context-policy.cjs +0 -124
  337. package/bin/system-prompt-token-cache-policy.cjs +0 -60
  338. package/bin/telegram-addressed-focus.cjs +0 -55
  339. package/bin/telegram-addressed-priority.cjs +0 -12
  340. package/bin/telegram-approval-relay.cjs +0 -290
  341. package/bin/telegram-bot-priority.cjs +0 -17
  342. package/bin/telegram-console-status-policy.cjs +0 -174
  343. package/bin/telegram-context-projection-policy.cjs +0 -141
  344. package/bin/telegram-delivery-lifecycle.cjs +0 -125
  345. package/bin/telegram-direct-focus-policy.cjs +0 -273
  346. package/bin/telegram-mcp-compatibility.cjs +0 -49
  347. package/bin/telegram-media-delivery-policy.cjs +0 -42
  348. package/bin/telegram-private-conversation-policy.cjs +0 -185
  349. package/bin/telegram-queue-handoff-policy.cjs +0 -73
  350. package/bin/telegram-remote-status-policy.cjs +0 -120
  351. package/bin/telegram-session-queue-runtime.mjs +0 -306
  352. package/bin/telegram-text-chunk-policy.cjs +0 -63
  353. package/bin/telegram-truncated-reply-policy.cjs +0 -37
  354. package/bin/telegram-urgent-policy.cjs +0 -45
  355. package/bin/telemetry-spool-policy.cjs +0 -57
  356. package/bin/thinking-activity-status-policy.cjs +0 -132
  357. package/bin/thinking-only-guard.cjs +0 -80
  358. package/bin/todo-list-turn-policy.cjs +0 -131
  359. package/bin/tool-call-loop-policy.cjs +0 -51
  360. package/bin/tool-file-persistence.cjs +0 -141
  361. package/bin/tool-result-offload-policy.cjs +0 -359
  362. package/bin/tool-result-offload-telemetry.cjs +0 -12
  363. package/bin/tool-schema-token-cache-policy.cjs +0 -41
  364. package/bin/tool-stream-preview-policy.cjs +0 -9
  365. package/bin/tui-functional-contract.cjs +0 -55
  366. package/bin/turn-tool-performance-policy.cjs +0 -486
  367. package/bin/usage-cache-efficiency-policy.cjs +0 -26
  368. package/bin/user-home-path-policy.cjs +0 -13
  369. package/bin/user-message-offload-policy.cjs +0 -103
  370. package/bin/user-prompt-hook-origin-policy.cjs +0 -34
  371. package/bin/user-tool-record-policy.cjs +0 -7
  372. package/bin/validated-learning-insight-policy.cjs +0 -58
  373. package/bin/validated-learning-outcome-trace.cjs +0 -107
  374. package/bin/validated-learning-performance-policy.cjs +0 -53
  375. package/bin/validated-learning-signal.cjs +0 -463
  376. package/bin/windows-bash-dialect-policy.cjs +0 -25
  377. package/bin/windows-node-crash-dump.cjs +0 -110
  378. package/bin/write-continuation-policy.cjs +0 -69
  379. package/codebase-index/README.md +0 -82
  380. package/codebase-index/codebase_index.py +0 -470
  381. package/standard-skills/agent-browser/SKILL.md +0 -19
  382. package/standard-skills/agent-browser/references/runtime.md +0 -8
  383. package/standard-skills/blun-session-inspector/SKILL.md +0 -41
  384. package/standard-skills/blun-session-inspector/scripts/inspect-session.cjs +0 -437
  385. package/standard-skills/design-taste-frontend/SKILL.md +0 -1206
  386. package/standard-skills/full-output-enforcement/SKILL.md +0 -49
  387. package/standard-skills/high-end-visual-design/SKILL.md +0 -98
  388. package/standard-skills/image-to-code/SKILL.md +0 -1228
  389. package/standard-skills/industrial-brutalist-ui/SKILL.md +0 -92
  390. package/standard-skills/minimalist-ui/SKILL.md +0 -85
  391. package/standard-skills/motion-design-taste/SKILL.md +0 -74
  392. package/standard-skills/playwright-testing/SKILL.md +0 -19
  393. package/standard-skills/playwright-testing/references/runtime.md +0 -7
  394. package/standard-skills/premortem/SKILL.md +0 -148
  395. package/standard-skills/redesign-existing-projects/SKILL.md +0 -178
  396. package/standard-skills/research-evidence/SKILL.md +0 -39
  397. package/standard-skills/research-evidence/references/evidence-format.md +0 -104
  398. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +0 -260
  399. package/standard-skills/research-evidence/scripts/score-report.cjs +0 -130
  400. package/standard-skills/screenshot-lesen/SKILL.md +0 -52
  401. package/standard-skills/stitch-design-taste/DESIGN.md +0 -121
  402. package/standard-skills/stitch-design-taste/SKILL.md +0 -184
  403. package/standard-skills/telegram-channel/SKILL.md +0 -18
  404. package/standard-skills/telegram-channel/references/runtime.md +0 -7
  405. package/standard-skills/venture-flywheel/SKILL.md +0 -32
  406. package/standard-skills/venture-flywheel/identity/project-identity.cjs +0 -146
  407. package/standard-skills/venture-flywheel/policy/capability-engine.cjs +0 -114
  408. package/standard-skills/venture-flywheel/policy/repository-trust.cjs +0 -229
  409. package/standard-skills/venture-flywheel/references/BEISPIELE-phase0.md +0 -146
  410. package/standard-skills/venture-flywheel/references/CAPABILITY-MAP.md +0 -34
  411. package/standard-skills/venture-flywheel/references/SPEC-phase0-identity-trust.md +0 -77
  412. package/standard-skills/venture-flywheel/references/SPEC-phase0-state-events.md +0 -93
  413. package/standard-skills/venture-flywheel/schemas/capability-decision.schema.json +0 -13
  414. package/standard-skills/venture-flywheel/schemas/execution-event.schema.json +0 -44
  415. package/standard-skills/venture-flywheel/schemas/project-identity.schema.json +0 -32
  416. package/standard-skills/venture-flywheel/schemas/repository-trust.schema.json +0 -57
  417. package/standard-skills/venture-flywheel/schemas/run-transition.schema.json +0 -59
  418. package/standard-skills/venture-flywheel/state/execution-event.cjs +0 -191
  419. package/standard-skills/venture-flywheel/state/task-state-machine.cjs +0 -190
  420. package/standard-skills/web-lesen/SKILL.md +0 -73
  421. package/standard-skills/web-lesen/scripts/crawl_public.py +0 -379
  422. package/standard-skills/windows-mcp/SKILL.md +0 -19
  423. package/standard-skills/windows-mcp/references/runtime.md +0 -9
  424. package/telegram-plugin/DELIVERY.md +0 -36
  425. package/telegram-plugin/bin/telegram-approval-relay.cjs +0 -290
  426. package/telegram-plugin/bin/telegram-console-status-policy.cjs +0 -175
  427. package/telegram-plugin/bin/telegram-delivery-lifecycle.cjs +0 -125
  428. package/telegram-plugin/bin/telegram-direct-reply-policy.cjs +0 -48
  429. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +0 -122
  430. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +0 -186
  431. package/telegram-plugin/bin/telegram-remote-status-policy.cjs +0 -121
  432. package/telegram-plugin/bin/telegram-reply-parts.cjs +0 -149
  433. package/telegram-plugin/bin/telegram-text-chunk-policy.cjs +0 -63
  434. package/telegram-plugin/bin/telegram-typing-keepalive.cjs +0 -89
  435. package/telegram-plugin/compat/mcp-server-fa511cd1.mjs +0 -73825
  436. /package/{bin → scripts}/fix-node-pty-perms.js +0 -0
@@ -0,0 +1,1441 @@
1
+ #!/usr/bin/env python3
2
+ """Fail-closed reference receiver for CMS localization callbacks.
3
+
4
+ The host owns authentication, message authorities, and atomic CMS operations.
5
+ This module parses each exact callback, verifies it before invoking the host,
6
+ and signs an acknowledgement only after the host confirms the same binding.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import importlib.util
13
+ import json
14
+ import re
15
+ import sys
16
+ import unicodedata
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Mapping, Protocol, Sequence
20
+
21
+
22
+ MAX_REQUEST_BYTES = 4_000_000
23
+ MAX_RESPONSE_BYTES = 65_536
24
+ RECEIVER_PATH = "/v1/localization/callback"
25
+ RECEIVER_ERROR_SCHEMA = "blun.cms-localization-receiver-error.v1"
26
+ HEADER_NAME = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$")
27
+ TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
28
+ SHA256 = re.compile(r"^[0-9a-f]{64}$")
29
+ SIGNATURE_VALUE = re.compile(r"^[A-Za-z0-9_.:/+=-]{1,4096}$")
30
+ ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
31
+ PUBLICATION_FIELDS = {
32
+ "schema", "delivery_id", "event_id", "site_id", "website_version",
33
+ "plan_id", "source_id", "source_revision", "source_sequence",
34
+ "source_sha256", "localizations",
35
+ }
36
+ LOCALIZATION_FIELDS = {
37
+ "locale", "target_text", "target_sha256", "approval_id",
38
+ "approval_expires_at", "release_evidence",
39
+ }
40
+ TOMBSTONE_FIELDS = {
41
+ "schema", "delivery_id", "tombstone_id", "event_id", "site_id",
42
+ "website_version", "plan_id", "source_id", "source_sequence",
43
+ "publication_delivery_id", "publication_payload_sha256", "locales",
44
+ }
45
+
46
+
47
+ def _load_module(name: str, path: Path):
48
+ spec = importlib.util.spec_from_file_location(name, path)
49
+ if spec is None or spec.loader is None:
50
+ raise RuntimeError(f"cannot load required CMS receiver dependency: {path.name}")
51
+ module = importlib.util.module_from_spec(spec)
52
+ sys.modules[spec.name] = module
53
+ spec.loader.exec_module(module)
54
+ return module
55
+
56
+
57
+ _ROOT = Path(__file__).resolve().parents[1]
58
+ _CMS = _load_module(
59
+ "blun_website_localization_receiver_cms",
60
+ _ROOT / "integrations" / "website_localization_cms.py",
61
+ )
62
+
63
+
64
+ class CMSReceiverBlocked(RuntimeError):
65
+ """Stable, content-free receiver failure suitable for an HTTP boundary."""
66
+
67
+ def __init__(self, code: str, *, retryable: bool, http_status: int):
68
+ if not isinstance(code, str) or ERROR_CODE.fullmatch(code) is None:
69
+ raise ValueError("CMS receiver error code is invalid")
70
+ if not isinstance(retryable, bool):
71
+ raise ValueError("CMS receiver retryability must be boolean")
72
+ if isinstance(http_status, bool) or http_status not in {400, 401, 409, 500, 503}:
73
+ raise ValueError("CMS receiver HTTP status is invalid")
74
+ super().__init__(code)
75
+ self.code = code
76
+ self.retryable = retryable
77
+ self.http_status = http_status
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class VerifiedPublication:
82
+ delivery_id: str
83
+ payload_sha256: str
84
+ payload: dict[str, Any]
85
+ signature: Any
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class PublicationExpectation:
90
+ event_id: str
91
+ site_id: str
92
+ website_version: str
93
+ plan_id: str
94
+ source_id: str
95
+ source_revision: str
96
+ source_sequence: int
97
+ source_sha256: str
98
+ required_locales: tuple[str, ...]
99
+ content_type: str
100
+ commercial_profile: str | None
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class VerifiedTombstone:
105
+ delivery_id: str
106
+ payload_sha256: str
107
+ payload: dict[str, Any]
108
+ signature: Any
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class TombstoneExpectation:
113
+ tombstone_id: str
114
+ event_id: str
115
+ site_id: str
116
+ website_version: str
117
+ plan_id: str
118
+ source_id: str
119
+ source_sequence: int
120
+ publication_delivery_id: str
121
+ publication_payload_sha256: str
122
+ locales: tuple[str, ...]
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class VerifiedHealthProbe:
127
+ probe_id: str
128
+ contract_sha256: str
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class CMSReceiverHTTPResponse:
133
+ status: int
134
+ headers: tuple[tuple[str, str], ...]
135
+ body: bytes
136
+
137
+
138
+ class CMSMessageAuthority(Protocol):
139
+ def sign(self, payload: bytes) -> Any: ...
140
+ def verify(self, payload: bytes, signature: Any) -> bool: ...
141
+
142
+
143
+ class PublicationCommitter(Protocol):
144
+ def __call__(self, publication: VerifiedPublication) -> Mapping[str, Any]: ...
145
+
146
+
147
+ class TombstoneCommitter(Protocol):
148
+ def __call__(self, tombstone: VerifiedTombstone) -> Mapping[str, Any]: ...
149
+
150
+
151
+ class HealthAuthenticator(Protocol):
152
+ def __call__(self, headers: Mapping[str, str]) -> bool: ...
153
+
154
+
155
+ class HealthChecker(Protocol):
156
+ def __call__(self, probe: VerifiedHealthProbe) -> Mapping[str, Any]: ...
157
+
158
+
159
+ class PublicationExpectationResolver(Protocol):
160
+ def __call__(
161
+ self, publication: VerifiedPublication,
162
+ ) -> PublicationExpectation | Mapping[str, Any]: ...
163
+
164
+
165
+ class TombstoneExpectationResolver(Protocol):
166
+ def __call__(
167
+ self, tombstone: VerifiedTombstone,
168
+ ) -> TombstoneExpectation | Mapping[str, Any]: ...
169
+
170
+
171
+ def _canonical_json(value: Any, *, maximum: int) -> bytes:
172
+ try:
173
+ encoded = json.dumps(
174
+ value,
175
+ ensure_ascii=False,
176
+ allow_nan=False,
177
+ sort_keys=True,
178
+ separators=(",", ":"),
179
+ ).encode("utf-8")
180
+ except (TypeError, ValueError, RecursionError):
181
+ raise CMSReceiverBlocked(
182
+ "receiver.json_invalid", retryable=False, http_status=400,
183
+ ) from None
184
+ if not encoded or len(encoded) > maximum:
185
+ raise CMSReceiverBlocked(
186
+ "receiver.message_size", retryable=False, http_status=400,
187
+ )
188
+ return encoded
189
+
190
+
191
+ def _pairs(items):
192
+ result = {}
193
+ for key, value in items:
194
+ if key in result:
195
+ raise ValueError("duplicate JSON key")
196
+ result[key] = value
197
+ return result
198
+
199
+
200
+ def _constant(value):
201
+ raise ValueError("non-finite JSON number")
202
+
203
+
204
+ def _parse_body(body: Any) -> tuple[dict[str, Any], bytes]:
205
+ if not isinstance(body, bytes) or not body or len(body) > MAX_REQUEST_BYTES:
206
+ raise CMSReceiverBlocked(
207
+ "receiver.message_size", retryable=False, http_status=400,
208
+ )
209
+ try:
210
+ text = body.decode("utf-8")
211
+ if text.startswith("\ufeff"):
212
+ raise ValueError("BOM rejected")
213
+ value = json.loads(
214
+ text,
215
+ object_pairs_hook=_pairs,
216
+ parse_constant=_constant,
217
+ )
218
+ except (UnicodeDecodeError, json.JSONDecodeError, ValueError, RecursionError):
219
+ raise CMSReceiverBlocked(
220
+ "receiver.json_invalid", retryable=False, http_status=400,
221
+ ) from None
222
+ if not isinstance(value, dict):
223
+ raise CMSReceiverBlocked(
224
+ "receiver.request_invalid", retryable=False, http_status=400,
225
+ )
226
+ canonical = _canonical_json(value, maximum=MAX_REQUEST_BYTES)
227
+ if canonical != body:
228
+ raise CMSReceiverBlocked(
229
+ "receiver.request_noncanonical", retryable=False, http_status=400,
230
+ )
231
+ return value, canonical
232
+
233
+
234
+ def _headers(value: Any) -> dict[str, str]:
235
+ if isinstance(value, Mapping):
236
+ items = tuple(value.items())
237
+ elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
238
+ items = tuple(value)
239
+ else:
240
+ raise CMSReceiverBlocked(
241
+ "receiver.headers_invalid", retryable=False, http_status=400,
242
+ )
243
+ headers: dict[str, str] = {}
244
+ for item in items:
245
+ if (
246
+ not isinstance(item, Sequence)
247
+ or isinstance(item, (str, bytes, bytearray))
248
+ or len(item) != 2
249
+ ):
250
+ raise CMSReceiverBlocked(
251
+ "receiver.headers_invalid", retryable=False, http_status=400,
252
+ )
253
+ name, content = item
254
+ if (
255
+ not isinstance(name, str)
256
+ or HEADER_NAME.fullmatch(name) is None
257
+ or not isinstance(content, str)
258
+ or "\r" in content
259
+ or "\n" in content
260
+ ):
261
+ raise CMSReceiverBlocked(
262
+ "receiver.headers_invalid", retryable=False, http_status=400,
263
+ )
264
+ normalized = name.lower()
265
+ if normalized in headers:
266
+ raise CMSReceiverBlocked(
267
+ "receiver.headers_ambiguous", retryable=False, http_status=400,
268
+ )
269
+ headers[normalized] = content.strip()
270
+ content_type = headers.get("content-type", "").lower().replace(" ", "")
271
+ if content_type != "application/json;charset=utf-8":
272
+ raise CMSReceiverBlocked(
273
+ "receiver.content_type", retryable=False, http_status=400,
274
+ )
275
+ if "transfer-encoding" in headers:
276
+ raise CMSReceiverBlocked(
277
+ "receiver.framing_invalid", retryable=False, http_status=400,
278
+ )
279
+ return headers
280
+
281
+
282
+ def _signature(value: Any) -> Any:
283
+ if not isinstance(value, dict) or set(value) != {
284
+ "algorithm", "key_id", "signature",
285
+ }:
286
+ raise CMSReceiverBlocked(
287
+ "receiver.signature_invalid", retryable=False, http_status=401,
288
+ )
289
+ if (
290
+ not isinstance(value["algorithm"], str)
291
+ or TOKEN.fullmatch(value["algorithm"]) is None
292
+ or not isinstance(value["key_id"], str)
293
+ or TOKEN.fullmatch(value["key_id"]) is None
294
+ or not isinstance(value["signature"], str)
295
+ or SIGNATURE_VALUE.fullmatch(value["signature"]) is None
296
+ ):
297
+ raise CMSReceiverBlocked(
298
+ "receiver.signature_invalid", retryable=False, http_status=401,
299
+ )
300
+ return _CMS.CMSMessageSignature(
301
+ value["algorithm"], value["key_id"], value["signature"],
302
+ )
303
+
304
+
305
+ def _timestamp(value: Any) -> float:
306
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
307
+ raise CMSReceiverBlocked(
308
+ "receiver.publication_invalid", retryable=False, http_status=400,
309
+ )
310
+ result = float(value)
311
+ if result < 0 or result != result or result in {float("inf"), float("-inf")}:
312
+ raise CMSReceiverBlocked(
313
+ "receiver.publication_invalid", retryable=False, http_status=400,
314
+ )
315
+ return result
316
+
317
+
318
+ def _verify_publication(value: Any, payload_sha256: str, *, now: float) -> bytes:
319
+ if not isinstance(value, dict) or set(value) != PUBLICATION_FIELDS:
320
+ raise CMSReceiverBlocked(
321
+ "receiver.publication_invalid", retryable=False, http_status=400,
322
+ )
323
+ token_fields = (
324
+ "delivery_id", "event_id", "site_id", "website_version", "plan_id",
325
+ "source_id", "source_revision",
326
+ )
327
+ if (
328
+ value.get("schema") != _CMS.PUBLICATION_SCHEMA
329
+ or any(
330
+ not isinstance(value.get(field), str)
331
+ or TOKEN.fullmatch(value[field]) is None
332
+ for field in token_fields
333
+ )
334
+ or SHA256.fullmatch(str(value.get("source_sha256"))) is None
335
+ or isinstance(value.get("source_sequence"), bool)
336
+ or not isinstance(value.get("source_sequence"), int)
337
+ or value["source_sequence"] <= 0
338
+ ):
339
+ raise CMSReceiverBlocked(
340
+ "receiver.publication_invalid", retryable=False, http_status=400,
341
+ )
342
+ localizations = value.get("localizations")
343
+ if not isinstance(localizations, list) or not localizations:
344
+ raise CMSReceiverBlocked(
345
+ "receiver.bundle_incomplete", retryable=False, http_status=409,
346
+ )
347
+ locales: list[str] = []
348
+ for item in localizations:
349
+ if not isinstance(item, dict) or set(item) != LOCALIZATION_FIELDS:
350
+ raise CMSReceiverBlocked(
351
+ "receiver.localization_invalid", retryable=False, http_status=400,
352
+ )
353
+ locale = item.get("locale")
354
+ target = item.get("target_text")
355
+ target_sha256 = item.get("target_sha256")
356
+ approval_id = item.get("approval_id")
357
+ if (
358
+ not isinstance(locale, str)
359
+ or TOKEN.fullmatch(locale) is None
360
+ or not isinstance(target, str)
361
+ or not target
362
+ or not unicodedata.is_normalized("NFC", target)
363
+ or SHA256.fullmatch(str(target_sha256)) is None
364
+ or hashlib.sha256(target.encode("utf-8")).hexdigest() != target_sha256
365
+ or not isinstance(approval_id, str)
366
+ or TOKEN.fullmatch(approval_id) is None
367
+ or _timestamp(item.get("approval_expires_at")) <= now
368
+ or not _CMS._valid_release_evidence(
369
+ item.get("release_evidence"),
370
+ locale=locale,
371
+ target_sha256=target_sha256,
372
+ approval_id=approval_id,
373
+ )
374
+ ):
375
+ raise CMSReceiverBlocked(
376
+ "receiver.localization_invalid", retryable=False, http_status=400,
377
+ )
378
+ locales.append(locale)
379
+ if locales != sorted(set(locales)):
380
+ raise CMSReceiverBlocked(
381
+ "receiver.bundle_invalid", retryable=False, http_status=409,
382
+ )
383
+ payload_bytes = _canonical_json(value, maximum=MAX_REQUEST_BYTES)
384
+ if hashlib.sha256(payload_bytes).hexdigest() != payload_sha256:
385
+ raise CMSReceiverBlocked(
386
+ "receiver.payload_binding", retryable=False, http_status=409,
387
+ )
388
+ unsigned = {key: content for key, content in value.items() if key != "delivery_id"}
389
+ expected_delivery_id = "blun-cms-delivery-" + hashlib.sha256(
390
+ _canonical_json(unsigned, maximum=MAX_REQUEST_BYTES)
391
+ ).hexdigest()
392
+ if value["delivery_id"] != expected_delivery_id:
393
+ raise CMSReceiverBlocked(
394
+ "receiver.delivery_binding", retryable=False, http_status=409,
395
+ )
396
+ return payload_bytes
397
+
398
+
399
+ def _verify_expectation(
400
+ publication: dict[str, Any],
401
+ expectation: PublicationExpectation | Mapping[str, Any],
402
+ ) -> None:
403
+ if isinstance(expectation, Mapping):
404
+ fields = tuple(PublicationExpectation.__dataclass_fields__)
405
+ if set(expectation) != set(fields):
406
+ raise ValueError("expectation is invalid")
407
+ try:
408
+ expectation = PublicationExpectation(**dict(expectation))
409
+ except TypeError:
410
+ raise ValueError("expectation is invalid") from None
411
+ if not isinstance(expectation, PublicationExpectation):
412
+ raise TypeError("expectation must be PublicationExpectation or an exact mapping")
413
+ token_fields = (
414
+ "event_id", "site_id", "website_version", "plan_id", "source_id",
415
+ "source_revision", "content_type",
416
+ )
417
+ if (
418
+ any(
419
+ not isinstance(getattr(expectation, field), str)
420
+ or TOKEN.fullmatch(getattr(expectation, field)) is None
421
+ for field in token_fields
422
+ )
423
+ or expectation.content_type not in _CMS._PLANNER.CONTENT_TYPES
424
+ or isinstance(expectation.source_sequence, bool)
425
+ or not isinstance(expectation.source_sequence, int)
426
+ or expectation.source_sequence <= 0
427
+ or not isinstance(expectation.source_sha256, str)
428
+ or SHA256.fullmatch(expectation.source_sha256) is None
429
+ or not isinstance(expectation.required_locales, tuple)
430
+ or not expectation.required_locales
431
+ or expectation.required_locales
432
+ != tuple(sorted(set(expectation.required_locales)))
433
+ or any(
434
+ not isinstance(locale, str) or TOKEN.fullmatch(locale) is None
435
+ for locale in expectation.required_locales
436
+ )
437
+ or (
438
+ expectation.commercial_profile is not None
439
+ and (
440
+ not isinstance(expectation.commercial_profile, str)
441
+ or TOKEN.fullmatch(expectation.commercial_profile) is None
442
+ )
443
+ )
444
+ or (expectation.content_type == "commercial")
445
+ != (expectation.commercial_profile is not None)
446
+ ):
447
+ raise ValueError("expectation is invalid")
448
+ expected_binding = {
449
+ "event_id": expectation.event_id,
450
+ "site_id": expectation.site_id,
451
+ "website_version": expectation.website_version,
452
+ "plan_id": expectation.plan_id,
453
+ "source_id": expectation.source_id,
454
+ "source_revision": expectation.source_revision,
455
+ "source_sequence": expectation.source_sequence,
456
+ "source_sha256": expectation.source_sha256,
457
+ }
458
+ if any(publication.get(field) != value for field, value in expected_binding.items()):
459
+ raise CMSReceiverBlocked(
460
+ "receiver.source_binding", retryable=False, http_status=409,
461
+ )
462
+ localizations = publication["localizations"]
463
+ if tuple(item["locale"] for item in localizations) != expectation.required_locales:
464
+ raise CMSReceiverBlocked(
465
+ "receiver.bundle_incomplete", retryable=False, http_status=409,
466
+ )
467
+ for item in localizations:
468
+ evidence = item["release_evidence"]
469
+ if (
470
+ evidence["content_type"] != expectation.content_type
471
+ or evidence["commercial_profile"] != expectation.commercial_profile
472
+ ):
473
+ raise CMSReceiverBlocked(
474
+ "receiver.release_scope", retryable=False, http_status=409,
475
+ )
476
+
477
+
478
+ def _verify_tombstone(value: Any, payload_sha256: str) -> bytes:
479
+ if not isinstance(value, dict) or set(value) != TOMBSTONE_FIELDS:
480
+ raise CMSReceiverBlocked(
481
+ "receiver.tombstone_invalid", retryable=False, http_status=400,
482
+ )
483
+ token_fields = (
484
+ "delivery_id", "tombstone_id", "event_id", "site_id",
485
+ "website_version", "plan_id", "source_id", "publication_delivery_id",
486
+ )
487
+ locales = value.get("locales")
488
+ if (
489
+ value.get("schema") != _CMS.TOMBSTONE_DELIVERY_SCHEMA
490
+ or any(
491
+ not isinstance(value.get(field), str)
492
+ or TOKEN.fullmatch(value[field]) is None
493
+ for field in token_fields
494
+ )
495
+ or isinstance(value.get("source_sequence"), bool)
496
+ or not isinstance(value.get("source_sequence"), int)
497
+ or value["source_sequence"] <= 0
498
+ or not isinstance(value.get("publication_payload_sha256"), str)
499
+ or SHA256.fullmatch(value["publication_payload_sha256"]) is None
500
+ or not isinstance(locales, list)
501
+ or not locales
502
+ or locales != sorted(set(locales))
503
+ or any(
504
+ not isinstance(locale, str) or TOKEN.fullmatch(locale) is None
505
+ for locale in locales
506
+ )
507
+ ):
508
+ raise CMSReceiverBlocked(
509
+ "receiver.tombstone_invalid", retryable=False, http_status=400,
510
+ )
511
+ payload_bytes = _canonical_json(value, maximum=MAX_REQUEST_BYTES)
512
+ if hashlib.sha256(payload_bytes).hexdigest() != payload_sha256:
513
+ raise CMSReceiverBlocked(
514
+ "receiver.payload_binding", retryable=False, http_status=409,
515
+ )
516
+ unsigned = {key: content for key, content in value.items() if key != "delivery_id"}
517
+ expected_delivery_id = "blun-cms-tombstone-" + hashlib.sha256(
518
+ _canonical_json(unsigned, maximum=MAX_REQUEST_BYTES)
519
+ ).hexdigest()
520
+ if value["delivery_id"] != expected_delivery_id:
521
+ raise CMSReceiverBlocked(
522
+ "receiver.delivery_binding", retryable=False, http_status=409,
523
+ )
524
+ return payload_bytes
525
+
526
+
527
+ def _verify_tombstone_expectation(
528
+ tombstone: dict[str, Any],
529
+ expectation: TombstoneExpectation | Mapping[str, Any],
530
+ ) -> None:
531
+ if isinstance(expectation, Mapping):
532
+ fields = tuple(TombstoneExpectation.__dataclass_fields__)
533
+ if set(expectation) != set(fields):
534
+ raise ValueError("expectation is invalid")
535
+ try:
536
+ expectation = TombstoneExpectation(**dict(expectation))
537
+ except TypeError:
538
+ raise ValueError("expectation is invalid") from None
539
+ if not isinstance(expectation, TombstoneExpectation):
540
+ raise TypeError(
541
+ "expectation must be TombstoneExpectation or an exact mapping"
542
+ )
543
+ token_fields = (
544
+ "tombstone_id", "event_id", "site_id", "website_version", "plan_id",
545
+ "source_id", "publication_delivery_id",
546
+ )
547
+ if (
548
+ any(
549
+ not isinstance(getattr(expectation, field), str)
550
+ or TOKEN.fullmatch(getattr(expectation, field)) is None
551
+ for field in token_fields
552
+ )
553
+ or isinstance(expectation.source_sequence, bool)
554
+ or not isinstance(expectation.source_sequence, int)
555
+ or expectation.source_sequence <= 0
556
+ or not isinstance(expectation.publication_payload_sha256, str)
557
+ or SHA256.fullmatch(expectation.publication_payload_sha256) is None
558
+ or not isinstance(expectation.locales, tuple)
559
+ or not expectation.locales
560
+ or expectation.locales != tuple(sorted(set(expectation.locales)))
561
+ or any(
562
+ not isinstance(locale, str) or TOKEN.fullmatch(locale) is None
563
+ for locale in expectation.locales
564
+ )
565
+ ):
566
+ raise ValueError("expectation is invalid")
567
+ expected = {
568
+ "tombstone_id": expectation.tombstone_id,
569
+ "event_id": expectation.event_id,
570
+ "site_id": expectation.site_id,
571
+ "website_version": expectation.website_version,
572
+ "plan_id": expectation.plan_id,
573
+ "source_id": expectation.source_id,
574
+ "source_sequence": expectation.source_sequence,
575
+ "publication_delivery_id": expectation.publication_delivery_id,
576
+ "publication_payload_sha256": expectation.publication_payload_sha256,
577
+ "locales": list(expectation.locales),
578
+ }
579
+ if any(tombstone.get(field) != content for field, content in expected.items()):
580
+ raise CMSReceiverBlocked(
581
+ "receiver.tombstone_binding", retryable=False, http_status=409,
582
+ )
583
+
584
+
585
+ def _verify_publication_transport(
586
+ body: bytes,
587
+ headers: Any,
588
+ publication_authority: CMSMessageAuthority,
589
+ *,
590
+ now: float | int,
591
+ ) -> VerifiedPublication:
592
+ if not callable(getattr(publication_authority, "verify", None)):
593
+ raise TypeError("publication_authority must provide verify")
594
+ now = _timestamp(now)
595
+ envelope, _ = _parse_body(body)
596
+ parsed_headers = _headers(headers)
597
+ if set(envelope) != {"schema", "payload_sha256", "publication", "signature"}:
598
+ raise CMSReceiverBlocked(
599
+ "receiver.request_invalid", retryable=False, http_status=400,
600
+ )
601
+ payload_sha256 = envelope.get("payload_sha256")
602
+ if (
603
+ envelope.get("schema") != _CMS.PUBLICATION_HTTP_REQUEST_SCHEMA
604
+ or not isinstance(payload_sha256, str)
605
+ or SHA256.fullmatch(payload_sha256) is None
606
+ ):
607
+ raise CMSReceiverBlocked(
608
+ "receiver.request_invalid", retryable=False, http_status=400,
609
+ )
610
+ payload_bytes = _verify_publication(
611
+ envelope.get("publication"), payload_sha256, now=now,
612
+ )
613
+ publication = envelope["publication"]
614
+ bindings = {
615
+ "delivery_id": publication["delivery_id"],
616
+ "payload_sha256": payload_sha256,
617
+ }
618
+ for name, binding in _CMS.PUBLICATION_HTTP_BINDING_HEADERS:
619
+ if parsed_headers.get(name.lower()) != bindings.get(binding):
620
+ raise CMSReceiverBlocked(
621
+ "receiver.header_binding", retryable=False, http_status=409,
622
+ )
623
+ declared_length = parsed_headers.get("content-length")
624
+ if declared_length is not None and (
625
+ not declared_length.isascii()
626
+ or not declared_length.isdecimal()
627
+ or int(declared_length) != len(body)
628
+ ):
629
+ raise CMSReceiverBlocked(
630
+ "receiver.framing_invalid", retryable=False, http_status=400,
631
+ )
632
+ signature = _signature(envelope.get("signature"))
633
+ try:
634
+ verified = publication_authority.verify(payload_bytes, signature) is True
635
+ except Exception:
636
+ verified = False
637
+ if not verified:
638
+ raise CMSReceiverBlocked(
639
+ "receiver.signature_invalid", retryable=False, http_status=401,
640
+ )
641
+ return VerifiedPublication(
642
+ delivery_id=publication["delivery_id"],
643
+ payload_sha256=payload_sha256,
644
+ payload=json.loads(payload_bytes.decode("utf-8")),
645
+ signature=signature,
646
+ )
647
+
648
+
649
+ def verify_publication_request(
650
+ body: bytes,
651
+ headers: Any,
652
+ publication_authority: CMSMessageAuthority,
653
+ expectation: PublicationExpectation,
654
+ *,
655
+ now: float | int,
656
+ ) -> VerifiedPublication:
657
+ """Verify one exact publication callback without performing a write."""
658
+
659
+ publication = _verify_publication_transport(
660
+ body, headers, publication_authority, now=now,
661
+ )
662
+ _verify_expectation(publication.payload, expectation)
663
+ return publication
664
+
665
+
666
+ def _verify_tombstone_transport(
667
+ body: bytes,
668
+ headers: Any,
669
+ publication_authority: CMSMessageAuthority,
670
+ ) -> VerifiedTombstone:
671
+ if not callable(getattr(publication_authority, "verify", None)):
672
+ raise TypeError("publication_authority must provide verify")
673
+ envelope, _ = _parse_body(body)
674
+ parsed_headers = _headers(headers)
675
+ if set(envelope) != {"schema", "payload_sha256", "tombstone", "signature"}:
676
+ raise CMSReceiverBlocked(
677
+ "receiver.request_invalid", retryable=False, http_status=400,
678
+ )
679
+ payload_sha256 = envelope.get("payload_sha256")
680
+ if (
681
+ envelope.get("schema") != _CMS.TOMBSTONE_HTTP_REQUEST_SCHEMA
682
+ or not isinstance(payload_sha256, str)
683
+ or SHA256.fullmatch(payload_sha256) is None
684
+ ):
685
+ raise CMSReceiverBlocked(
686
+ "receiver.request_invalid", retryable=False, http_status=400,
687
+ )
688
+ payload_bytes = _verify_tombstone(envelope.get("tombstone"), payload_sha256)
689
+ tombstone = envelope["tombstone"]
690
+ bindings = {
691
+ "delivery_id": tombstone["delivery_id"],
692
+ "payload_sha256": payload_sha256,
693
+ }
694
+ for name, binding in _CMS.PUBLICATION_HTTP_BINDING_HEADERS:
695
+ if parsed_headers.get(name.lower()) != bindings.get(binding):
696
+ raise CMSReceiverBlocked(
697
+ "receiver.header_binding", retryable=False, http_status=409,
698
+ )
699
+ declared_length = parsed_headers.get("content-length")
700
+ if declared_length is not None and (
701
+ not declared_length.isascii()
702
+ or not declared_length.isdecimal()
703
+ or int(declared_length) != len(body)
704
+ ):
705
+ raise CMSReceiverBlocked(
706
+ "receiver.framing_invalid", retryable=False, http_status=400,
707
+ )
708
+ signature = _signature(envelope.get("signature"))
709
+ try:
710
+ verified = publication_authority.verify(payload_bytes, signature) is True
711
+ except Exception:
712
+ verified = False
713
+ if not verified:
714
+ raise CMSReceiverBlocked(
715
+ "receiver.signature_invalid", retryable=False, http_status=401,
716
+ )
717
+ return VerifiedTombstone(
718
+ delivery_id=tombstone["delivery_id"],
719
+ payload_sha256=payload_sha256,
720
+ payload=json.loads(payload_bytes.decode("utf-8")),
721
+ signature=signature,
722
+ )
723
+
724
+
725
+ def verify_tombstone_request(
726
+ body: bytes,
727
+ headers: Any,
728
+ publication_authority: CMSMessageAuthority,
729
+ expectation: TombstoneExpectation,
730
+ ) -> VerifiedTombstone:
731
+ """Verify one exact tombstone callback without deleting CMS content."""
732
+
733
+ tombstone = _verify_tombstone_transport(
734
+ body, headers, publication_authority,
735
+ )
736
+ _verify_tombstone_expectation(tombstone.payload, expectation)
737
+ return tombstone
738
+
739
+
740
+ def verify_health_request(
741
+ body: bytes,
742
+ headers: Any,
743
+ authenticate: Callable[[Mapping[str, str]], bool],
744
+ *,
745
+ contract_sha256: str,
746
+ ) -> VerifiedHealthProbe:
747
+ """Authenticate and verify one content-free publisher health challenge."""
748
+
749
+ if not callable(authenticate):
750
+ raise TypeError("authenticate must be callable")
751
+ if (
752
+ not isinstance(contract_sha256, str)
753
+ or SHA256.fullmatch(contract_sha256) is None
754
+ ):
755
+ raise ValueError("contract_sha256 is invalid")
756
+ envelope, _ = _parse_body(body)
757
+ parsed_headers = _headers(headers)
758
+ if set(envelope) != {"schema", "probe"}:
759
+ raise CMSReceiverBlocked(
760
+ "receiver.health_request_invalid", retryable=False, http_status=400,
761
+ )
762
+ probe = envelope.get("probe")
763
+ if (
764
+ envelope.get("schema") != _CMS.PUBLICATION_HEALTH_HTTP_REQUEST_SCHEMA
765
+ or not isinstance(probe, dict)
766
+ or set(probe) != {"schema", "probe_id", "contract_sha256"}
767
+ or probe.get("schema") != _CMS.PUBLICATION_HEALTH_SCHEMA
768
+ or not isinstance(probe.get("probe_id"), str)
769
+ or not 16 <= len(probe["probe_id"]) <= 256
770
+ or TOKEN.fullmatch(probe["probe_id"]) is None
771
+ or not isinstance(probe.get("contract_sha256"), str)
772
+ or SHA256.fullmatch(probe["contract_sha256"]) is None
773
+ ):
774
+ raise CMSReceiverBlocked(
775
+ "receiver.health_request_invalid", retryable=False, http_status=400,
776
+ )
777
+ bindings = {
778
+ "probe_id": probe["probe_id"],
779
+ "contract_sha256": probe["contract_sha256"],
780
+ }
781
+ for name, binding in _CMS.PUBLICATION_HEALTH_HTTP_BINDING_HEADERS:
782
+ if parsed_headers.get(name.lower()) != bindings.get(binding):
783
+ raise CMSReceiverBlocked(
784
+ "receiver.health_header_binding", retryable=False, http_status=409,
785
+ )
786
+ declared_length = parsed_headers.get("content-length")
787
+ if declared_length is not None and (
788
+ not declared_length.isascii()
789
+ or not declared_length.isdecimal()
790
+ or int(declared_length) != len(body)
791
+ ):
792
+ raise CMSReceiverBlocked(
793
+ "receiver.framing_invalid", retryable=False, http_status=400,
794
+ )
795
+ try:
796
+ authenticated = authenticate(dict(parsed_headers)) is True
797
+ except Exception:
798
+ raise CMSReceiverBlocked(
799
+ "receiver.authentication_failed", retryable=True, http_status=503,
800
+ ) from None
801
+ if not authenticated:
802
+ raise CMSReceiverBlocked(
803
+ "receiver.authentication_invalid", retryable=False, http_status=401,
804
+ )
805
+ if probe["contract_sha256"] != contract_sha256:
806
+ raise CMSReceiverBlocked(
807
+ "receiver.health_contract_binding", retryable=False, http_status=409,
808
+ )
809
+ return VerifiedHealthProbe(
810
+ probe_id=probe["probe_id"],
811
+ contract_sha256=probe["contract_sha256"],
812
+ )
813
+
814
+
815
+ def _signed_acknowledgement(
816
+ *,
817
+ delivery_id: str,
818
+ payload_sha256: str,
819
+ acknowledgement_schema: str,
820
+ status: str,
821
+ response_schema: str,
822
+ authority: CMSMessageAuthority,
823
+ ) -> CMSReceiverHTTPResponse:
824
+ acknowledgement = {
825
+ "schema": acknowledgement_schema,
826
+ "delivery_id": delivery_id,
827
+ "payload_sha256": payload_sha256,
828
+ "status": status,
829
+ }
830
+ acknowledgement_bytes = _canonical_json(
831
+ acknowledgement, maximum=MAX_RESPONSE_BYTES,
832
+ )
833
+ try:
834
+ signature = authority.sign(acknowledgement_bytes)
835
+ signature_mapping = {
836
+ "algorithm": signature.algorithm,
837
+ "key_id": signature.key_id,
838
+ "signature": signature.signature,
839
+ }
840
+ normalized_signature = _signature(signature_mapping)
841
+ verified = authority.verify(
842
+ acknowledgement_bytes, normalized_signature,
843
+ ) is True
844
+ except Exception:
845
+ raise CMSReceiverBlocked(
846
+ "receiver.acknowledgement_signing", retryable=True, http_status=503,
847
+ ) from None
848
+ if not verified:
849
+ raise CMSReceiverBlocked(
850
+ "receiver.acknowledgement_signing", retryable=True, http_status=503,
851
+ )
852
+ response = {
853
+ "schema": response_schema,
854
+ "acknowledgement": acknowledgement,
855
+ "signature": signature_mapping,
856
+ }
857
+ response_body = _canonical_json(response, maximum=MAX_RESPONSE_BYTES)
858
+ return CMSReceiverHTTPResponse(
859
+ status=200,
860
+ headers=(
861
+ ("Content-Type", "application/json; charset=utf-8"),
862
+ ("Content-Length", str(len(response_body))),
863
+ ("Cache-Control", "no-store"),
864
+ ),
865
+ body=response_body,
866
+ )
867
+
868
+
869
+ def _signed_health_acknowledgement(
870
+ probe: VerifiedHealthProbe,
871
+ authority: CMSMessageAuthority,
872
+ ) -> CMSReceiverHTTPResponse:
873
+ acknowledgement = {
874
+ "schema": _CMS.PUBLICATION_HEALTH_ACK_SCHEMA,
875
+ "probe_id": probe.probe_id,
876
+ "contract_sha256": probe.contract_sha256,
877
+ "status": "healthy",
878
+ }
879
+ acknowledgement_bytes = _canonical_json(
880
+ acknowledgement, maximum=MAX_RESPONSE_BYTES,
881
+ )
882
+ try:
883
+ signature = authority.sign(acknowledgement_bytes)
884
+ signature_mapping = {
885
+ "algorithm": signature.algorithm,
886
+ "key_id": signature.key_id,
887
+ "signature": signature.signature,
888
+ }
889
+ normalized_signature = _signature(signature_mapping)
890
+ verified = authority.verify(
891
+ acknowledgement_bytes, normalized_signature,
892
+ ) is True
893
+ except Exception:
894
+ raise CMSReceiverBlocked(
895
+ "receiver.acknowledgement_signing", retryable=True, http_status=503,
896
+ ) from None
897
+ if not verified:
898
+ raise CMSReceiverBlocked(
899
+ "receiver.acknowledgement_signing", retryable=True, http_status=503,
900
+ )
901
+ response = {
902
+ "schema": _CMS.PUBLICATION_HEALTH_HTTP_RESPONSE_SCHEMA,
903
+ "acknowledgement": acknowledgement,
904
+ "signature": signature_mapping,
905
+ }
906
+ response_body = _canonical_json(response, maximum=MAX_RESPONSE_BYTES)
907
+ return CMSReceiverHTTPResponse(
908
+ status=200,
909
+ headers=(
910
+ ("Content-Type", "application/json; charset=utf-8"),
911
+ ("Content-Length", str(len(response_body))),
912
+ ("Cache-Control", "no-store"),
913
+ ),
914
+ body=response_body,
915
+ )
916
+
917
+
918
+ def _receive_verified_publication(
919
+ publication: VerifiedPublication,
920
+ acknowledgement_authority: CMSMessageAuthority,
921
+ commit: Callable[[VerifiedPublication], Mapping[str, Any]],
922
+ ) -> CMSReceiverHTTPResponse:
923
+ try:
924
+ receipt = commit(publication)
925
+ except Exception:
926
+ raise CMSReceiverBlocked(
927
+ "receiver.commit_failed", retryable=True, http_status=503,
928
+ ) from None
929
+ expected_receipt = {
930
+ "delivery_id": publication.delivery_id,
931
+ "payload_sha256": publication.payload_sha256,
932
+ "status": "committed",
933
+ }
934
+ if receipt != expected_receipt:
935
+ raise CMSReceiverBlocked(
936
+ "receiver.commit_unconfirmed", retryable=True, http_status=503,
937
+ )
938
+ return _signed_acknowledgement(
939
+ delivery_id=publication.delivery_id,
940
+ payload_sha256=publication.payload_sha256,
941
+ acknowledgement_schema=_CMS.ACK_SCHEMA,
942
+ status="accepted",
943
+ response_schema=_CMS.PUBLICATION_HTTP_RESPONSE_SCHEMA,
944
+ authority=acknowledgement_authority,
945
+ )
946
+
947
+
948
+ def _receive_verified_tombstone(
949
+ tombstone: VerifiedTombstone,
950
+ acknowledgement_authority: CMSMessageAuthority,
951
+ delete: Callable[[VerifiedTombstone], Mapping[str, Any]],
952
+ ) -> CMSReceiverHTTPResponse:
953
+ try:
954
+ receipt = delete(tombstone)
955
+ except Exception:
956
+ raise CMSReceiverBlocked(
957
+ "receiver.delete_failed", retryable=True, http_status=503,
958
+ ) from None
959
+ expected_receipt = {
960
+ "delivery_id": tombstone.delivery_id,
961
+ "payload_sha256": tombstone.payload_sha256,
962
+ "status": "deleted",
963
+ }
964
+ if receipt != expected_receipt:
965
+ raise CMSReceiverBlocked(
966
+ "receiver.delete_unconfirmed", retryable=True, http_status=503,
967
+ )
968
+ return _signed_acknowledgement(
969
+ delivery_id=tombstone.delivery_id,
970
+ payload_sha256=tombstone.payload_sha256,
971
+ acknowledgement_schema=_CMS.TOMBSTONE_ACK_SCHEMA,
972
+ status="deleted",
973
+ response_schema=_CMS.TOMBSTONE_HTTP_RESPONSE_SCHEMA,
974
+ authority=acknowledgement_authority,
975
+ )
976
+
977
+
978
+ def receive_publication(
979
+ body: bytes,
980
+ headers: Any,
981
+ publication_authority: CMSMessageAuthority,
982
+ acknowledgement_authority: CMSMessageAuthority,
983
+ expectation: PublicationExpectation,
984
+ commit: Callable[[VerifiedPublication], Mapping[str, Any]],
985
+ *,
986
+ now: float | int,
987
+ ) -> CMSReceiverHTTPResponse:
988
+ """Verify, commit through the host, then return one signed acknowledgement."""
989
+
990
+ if not callable(commit):
991
+ raise TypeError("commit must be callable")
992
+ if (
993
+ not callable(getattr(acknowledgement_authority, "sign", None))
994
+ or not callable(getattr(acknowledgement_authority, "verify", None))
995
+ ):
996
+ raise TypeError("acknowledgement_authority must provide sign and verify")
997
+ publication = verify_publication_request(
998
+ body, headers, publication_authority, expectation, now=now,
999
+ )
1000
+ return _receive_verified_publication(
1001
+ publication, acknowledgement_authority, commit,
1002
+ )
1003
+
1004
+
1005
+ def receive_tombstone(
1006
+ body: bytes,
1007
+ headers: Any,
1008
+ publication_authority: CMSMessageAuthority,
1009
+ acknowledgement_authority: CMSMessageAuthority,
1010
+ expectation: TombstoneExpectation,
1011
+ delete: Callable[[VerifiedTombstone], Mapping[str, Any]],
1012
+ ) -> CMSReceiverHTTPResponse:
1013
+ """Verify, delete through the host, then return one signed acknowledgement."""
1014
+
1015
+ if not callable(delete):
1016
+ raise TypeError("delete must be callable")
1017
+ if (
1018
+ not callable(getattr(acknowledgement_authority, "sign", None))
1019
+ or not callable(getattr(acknowledgement_authority, "verify", None))
1020
+ ):
1021
+ raise TypeError("acknowledgement_authority must provide sign and verify")
1022
+ tombstone = verify_tombstone_request(
1023
+ body, headers, publication_authority, expectation,
1024
+ )
1025
+ return _receive_verified_tombstone(
1026
+ tombstone, acknowledgement_authority, delete,
1027
+ )
1028
+
1029
+
1030
+ def receive_publication_resolved(
1031
+ body: bytes,
1032
+ headers: Any,
1033
+ publication_authority: CMSMessageAuthority,
1034
+ acknowledgement_authority: CMSMessageAuthority,
1035
+ resolve_expectation: Callable[
1036
+ [VerifiedPublication], PublicationExpectation | Mapping[str, Any]
1037
+ ],
1038
+ commit: Callable[[VerifiedPublication], Mapping[str, Any]],
1039
+ *,
1040
+ now: float | int,
1041
+ ) -> CMSReceiverHTTPResponse:
1042
+ """Verify a publication before resolving its current host expectation."""
1043
+
1044
+ if not callable(resolve_expectation):
1045
+ raise TypeError("resolve_expectation must be callable")
1046
+ if not callable(commit):
1047
+ raise TypeError("commit must be callable")
1048
+ if (
1049
+ not callable(getattr(acknowledgement_authority, "sign", None))
1050
+ or not callable(getattr(acknowledgement_authority, "verify", None))
1051
+ ):
1052
+ raise TypeError("acknowledgement_authority must provide sign and verify")
1053
+ publication = _verify_publication_transport(
1054
+ body, headers, publication_authority, now=now,
1055
+ )
1056
+ resolver_input = VerifiedPublication(
1057
+ delivery_id=publication.delivery_id,
1058
+ payload_sha256=publication.payload_sha256,
1059
+ payload=json.loads(_canonical_json(
1060
+ publication.payload, maximum=MAX_REQUEST_BYTES,
1061
+ ).decode("utf-8")),
1062
+ signature=publication.signature,
1063
+ )
1064
+ try:
1065
+ expectation = resolve_expectation(resolver_input)
1066
+ except Exception:
1067
+ raise CMSReceiverBlocked(
1068
+ "receiver.expectation_failed", retryable=True, http_status=503,
1069
+ ) from None
1070
+ try:
1071
+ _verify_expectation(publication.payload, expectation)
1072
+ except (TypeError, ValueError):
1073
+ raise CMSReceiverBlocked(
1074
+ "receiver.expectation_invalid", retryable=True, http_status=503,
1075
+ ) from None
1076
+ return _receive_verified_publication(
1077
+ publication, acknowledgement_authority, commit,
1078
+ )
1079
+
1080
+
1081
+ def receive_tombstone_resolved(
1082
+ body: bytes,
1083
+ headers: Any,
1084
+ publication_authority: CMSMessageAuthority,
1085
+ acknowledgement_authority: CMSMessageAuthority,
1086
+ resolve_expectation: Callable[
1087
+ [VerifiedTombstone], TombstoneExpectation | Mapping[str, Any]
1088
+ ],
1089
+ delete: Callable[[VerifiedTombstone], Mapping[str, Any]],
1090
+ ) -> CMSReceiverHTTPResponse:
1091
+ """Verify a tombstone before resolving its current host expectation."""
1092
+
1093
+ if not callable(resolve_expectation):
1094
+ raise TypeError("resolve_expectation must be callable")
1095
+ if not callable(delete):
1096
+ raise TypeError("delete must be callable")
1097
+ if (
1098
+ not callable(getattr(acknowledgement_authority, "sign", None))
1099
+ or not callable(getattr(acknowledgement_authority, "verify", None))
1100
+ ):
1101
+ raise TypeError("acknowledgement_authority must provide sign and verify")
1102
+ tombstone = _verify_tombstone_transport(
1103
+ body, headers, publication_authority,
1104
+ )
1105
+ resolver_input = VerifiedTombstone(
1106
+ delivery_id=tombstone.delivery_id,
1107
+ payload_sha256=tombstone.payload_sha256,
1108
+ payload=json.loads(_canonical_json(
1109
+ tombstone.payload, maximum=MAX_REQUEST_BYTES,
1110
+ ).decode("utf-8")),
1111
+ signature=tombstone.signature,
1112
+ )
1113
+ try:
1114
+ expectation = resolve_expectation(resolver_input)
1115
+ except Exception:
1116
+ raise CMSReceiverBlocked(
1117
+ "receiver.expectation_failed", retryable=True, http_status=503,
1118
+ ) from None
1119
+ try:
1120
+ _verify_tombstone_expectation(tombstone.payload, expectation)
1121
+ except (TypeError, ValueError):
1122
+ raise CMSReceiverBlocked(
1123
+ "receiver.expectation_invalid", retryable=True, http_status=503,
1124
+ ) from None
1125
+ return _receive_verified_tombstone(
1126
+ tombstone, acknowledgement_authority, delete,
1127
+ )
1128
+
1129
+
1130
+ def receive_health(
1131
+ body: bytes,
1132
+ headers: Any,
1133
+ authenticate: Callable[[Mapping[str, str]], bool],
1134
+ acknowledgement_authority: CMSMessageAuthority,
1135
+ check: Callable[[VerifiedHealthProbe], Mapping[str, Any]],
1136
+ *,
1137
+ contract_sha256: str,
1138
+ ) -> CMSReceiverHTTPResponse:
1139
+ """Authenticate, check the host, then sign one content-free health reply."""
1140
+
1141
+ if not callable(check):
1142
+ raise TypeError("check must be callable")
1143
+ if (
1144
+ not callable(getattr(acknowledgement_authority, "sign", None))
1145
+ or not callable(getattr(acknowledgement_authority, "verify", None))
1146
+ ):
1147
+ raise TypeError("acknowledgement_authority must provide sign and verify")
1148
+ probe = verify_health_request(
1149
+ body, headers, authenticate, contract_sha256=contract_sha256,
1150
+ )
1151
+ try:
1152
+ receipt = check(probe)
1153
+ except Exception:
1154
+ raise CMSReceiverBlocked(
1155
+ "receiver.health_check_failed", retryable=True, http_status=503,
1156
+ ) from None
1157
+ expected_receipt = {
1158
+ "probe_id": probe.probe_id,
1159
+ "contract_sha256": probe.contract_sha256,
1160
+ "status": "healthy",
1161
+ }
1162
+ if receipt != expected_receipt:
1163
+ raise CMSReceiverBlocked(
1164
+ "receiver.health_unconfirmed", retryable=True, http_status=503,
1165
+ )
1166
+ return _signed_health_acknowledgement(probe, acknowledgement_authority)
1167
+
1168
+
1169
+ class CMSReceiverApplication:
1170
+ """HTTPS-only WSGI boundary for the complete reference receiver."""
1171
+
1172
+ def __init__(
1173
+ self,
1174
+ publication_authority: CMSMessageAuthority,
1175
+ acknowledgement_authority: CMSMessageAuthority,
1176
+ authenticate: Callable[[Mapping[str, str]], bool],
1177
+ resolve_publication_expectation: Callable[
1178
+ [VerifiedPublication], PublicationExpectation | Mapping[str, Any]
1179
+ ],
1180
+ commit: Callable[[VerifiedPublication], Mapping[str, Any]],
1181
+ resolve_tombstone_expectation: Callable[
1182
+ [VerifiedTombstone], TombstoneExpectation | Mapping[str, Any]
1183
+ ],
1184
+ delete: Callable[[VerifiedTombstone], Mapping[str, Any]],
1185
+ check: Callable[[VerifiedHealthProbe], Mapping[str, Any]],
1186
+ *,
1187
+ contract_sha256: str,
1188
+ clock: Callable[[], float | int],
1189
+ path: str = RECEIVER_PATH,
1190
+ require_https: bool = True,
1191
+ ):
1192
+ callbacks = {
1193
+ "authenticate": authenticate,
1194
+ "resolve_publication_expectation": resolve_publication_expectation,
1195
+ "commit": commit,
1196
+ "resolve_tombstone_expectation": resolve_tombstone_expectation,
1197
+ "delete": delete,
1198
+ "check": check,
1199
+ "clock": clock,
1200
+ }
1201
+ for name, callback in callbacks.items():
1202
+ if not callable(callback):
1203
+ raise TypeError(f"{name} must be callable")
1204
+ if not callable(getattr(publication_authority, "verify", None)):
1205
+ raise TypeError("publication_authority must provide verify")
1206
+ if (
1207
+ not callable(getattr(acknowledgement_authority, "sign", None))
1208
+ or not callable(getattr(acknowledgement_authority, "verify", None))
1209
+ ):
1210
+ raise TypeError("acknowledgement_authority must provide sign and verify")
1211
+ if (
1212
+ not isinstance(contract_sha256, str)
1213
+ or SHA256.fullmatch(contract_sha256) is None
1214
+ ):
1215
+ raise ValueError("contract_sha256 is invalid")
1216
+ if (
1217
+ not isinstance(path, str)
1218
+ or not path.isascii()
1219
+ or not path.startswith("/")
1220
+ or not 2 <= len(path) <= 256
1221
+ or any(character in path for character in "?#\r\n")
1222
+ ):
1223
+ raise ValueError("path is invalid")
1224
+ if not isinstance(require_https, bool):
1225
+ raise TypeError("require_https must be boolean")
1226
+ self.publication_authority = publication_authority
1227
+ self.acknowledgement_authority = acknowledgement_authority
1228
+ self.authenticate = authenticate
1229
+ self.resolve_publication_expectation = resolve_publication_expectation
1230
+ self.commit = commit
1231
+ self.resolve_tombstone_expectation = resolve_tombstone_expectation
1232
+ self.delete = delete
1233
+ self.check = check
1234
+ self.contract_sha256 = contract_sha256
1235
+ self.clock = clock
1236
+ self.path = path
1237
+ self.require_https = require_https
1238
+
1239
+ @staticmethod
1240
+ def _wsgi_headers(environ: Mapping[str, Any]) -> tuple[tuple[str, str], ...]:
1241
+ items = []
1242
+ for key, value in environ.items():
1243
+ if key == "CONTENT_TYPE":
1244
+ name = "Content-Type"
1245
+ elif key == "CONTENT_LENGTH":
1246
+ name = "Content-Length"
1247
+ elif isinstance(key, str) and key.startswith("HTTP_"):
1248
+ name = key[5:].replace("_", "-")
1249
+ else:
1250
+ continue
1251
+ items.append((name, value))
1252
+ if len(items) > 64 or any(
1253
+ not isinstance(name, str)
1254
+ or not isinstance(value, str)
1255
+ or len(name) + len(value) > 16_384
1256
+ for name, value in items
1257
+ ):
1258
+ raise CMSReceiverBlocked(
1259
+ "receiver.headers_invalid", retryable=False, http_status=400,
1260
+ )
1261
+ return tuple(items)
1262
+
1263
+ @staticmethod
1264
+ def _send(
1265
+ start_response: Callable[..., Any],
1266
+ response: CMSReceiverHTTPResponse,
1267
+ ) -> list[bytes]:
1268
+ phrases = {200: "OK"}
1269
+ headers = list(response.headers)
1270
+ headers.extend((
1271
+ ("X-Content-Type-Options", "nosniff"),
1272
+ ("Referrer-Policy", "no-referrer"),
1273
+ ))
1274
+ start_response(
1275
+ f"{response.status} {phrases.get(response.status, 'Error')}", headers,
1276
+ )
1277
+ return [response.body]
1278
+
1279
+ @staticmethod
1280
+ def _blocked(
1281
+ start_response: Callable[..., Any],
1282
+ status: int,
1283
+ code: str,
1284
+ *,
1285
+ retryable: bool,
1286
+ ) -> list[bytes]:
1287
+ phrases = {
1288
+ 400: "Bad Request",
1289
+ 401: "Unauthorized",
1290
+ 404: "Not Found",
1291
+ 405: "Method Not Allowed",
1292
+ 409: "Conflict",
1293
+ 411: "Length Required",
1294
+ 413: "Content Too Large",
1295
+ 415: "Unsupported Media Type",
1296
+ 500: "Internal Server Error",
1297
+ 503: "Service Unavailable",
1298
+ }
1299
+ body = _canonical_json({
1300
+ "schema": RECEIVER_ERROR_SCHEMA,
1301
+ "status": "BLOCK",
1302
+ "error": code,
1303
+ "retryable": retryable,
1304
+ }, maximum=MAX_RESPONSE_BYTES)
1305
+ start_response(f"{status} {phrases[status]}", (
1306
+ ("Content-Type", "application/json; charset=utf-8"),
1307
+ ("Content-Length", str(len(body))),
1308
+ ("Cache-Control", "no-store"),
1309
+ ("X-Content-Type-Options", "nosniff"),
1310
+ ("Referrer-Policy", "no-referrer"),
1311
+ ))
1312
+ return [body]
1313
+
1314
+ def __call__(
1315
+ self,
1316
+ environ: Mapping[str, Any],
1317
+ start_response: Callable[..., Any],
1318
+ ) -> list[bytes]:
1319
+ if not isinstance(environ, dict):
1320
+ return self._blocked(
1321
+ start_response, 500, "receiver.environment_invalid", retryable=True,
1322
+ )
1323
+ if environ.get("PATH_INFO") != self.path:
1324
+ return self._blocked(
1325
+ start_response, 404, "receiver.path_not_found", retryable=False,
1326
+ )
1327
+ if environ.get("REQUEST_METHOD") != "POST":
1328
+ return self._blocked(
1329
+ start_response, 405, "receiver.method_not_allowed", retryable=False,
1330
+ )
1331
+ if self.require_https and environ.get("wsgi.url_scheme") != "https":
1332
+ return self._blocked(
1333
+ start_response, 400, "receiver.https_required", retryable=False,
1334
+ )
1335
+ if environ.get("QUERY_STRING") not in {None, ""}:
1336
+ return self._blocked(
1337
+ start_response, 400, "receiver.query_rejected", retryable=False,
1338
+ )
1339
+ if environ.get("HTTP_TRANSFER_ENCODING"):
1340
+ return self._blocked(
1341
+ start_response, 400, "receiver.framing_invalid", retryable=False,
1342
+ )
1343
+ content_type = environ.get("CONTENT_TYPE")
1344
+ if not isinstance(content_type, str) or (
1345
+ content_type.lower().replace(" ", "")
1346
+ != "application/json;charset=utf-8"
1347
+ ):
1348
+ return self._blocked(
1349
+ start_response, 415, "receiver.content_type", retryable=False,
1350
+ )
1351
+ length = environ.get("CONTENT_LENGTH")
1352
+ if (
1353
+ not isinstance(length, str)
1354
+ or not length.isascii()
1355
+ or not length.isdecimal()
1356
+ ):
1357
+ return self._blocked(
1358
+ start_response, 411, "receiver.content_length_required",
1359
+ retryable=False,
1360
+ )
1361
+ size = int(length)
1362
+ if size <= 0:
1363
+ return self._blocked(
1364
+ start_response, 400, "receiver.message_size", retryable=False,
1365
+ )
1366
+ if size > MAX_REQUEST_BYTES:
1367
+ return self._blocked(
1368
+ start_response, 413, "receiver.message_size", retryable=False,
1369
+ )
1370
+ stream = environ.get("wsgi.input")
1371
+ try:
1372
+ body = stream.read(size)
1373
+ except Exception:
1374
+ body = None
1375
+ if not isinstance(body, bytes) or len(body) != size:
1376
+ return self._blocked(
1377
+ start_response, 400, "receiver.body_invalid", retryable=False,
1378
+ )
1379
+ try:
1380
+ raw_headers = self._wsgi_headers(environ)
1381
+ parsed_headers = _headers(raw_headers)
1382
+ try:
1383
+ authenticated = self.authenticate(dict(parsed_headers)) is True
1384
+ except Exception:
1385
+ raise CMSReceiverBlocked(
1386
+ "receiver.authentication_failed",
1387
+ retryable=True,
1388
+ http_status=503,
1389
+ ) from None
1390
+ if not authenticated:
1391
+ raise CMSReceiverBlocked(
1392
+ "receiver.authentication_invalid",
1393
+ retryable=False,
1394
+ http_status=401,
1395
+ )
1396
+ envelope, _ = _parse_body(body)
1397
+ schema = envelope.get("schema")
1398
+ if schema == _CMS.PUBLICATION_HTTP_REQUEST_SCHEMA:
1399
+ response = receive_publication_resolved(
1400
+ body,
1401
+ raw_headers,
1402
+ self.publication_authority,
1403
+ self.acknowledgement_authority,
1404
+ self.resolve_publication_expectation,
1405
+ self.commit,
1406
+ now=self.clock(),
1407
+ )
1408
+ elif schema == _CMS.TOMBSTONE_HTTP_REQUEST_SCHEMA:
1409
+ response = receive_tombstone_resolved(
1410
+ body,
1411
+ raw_headers,
1412
+ self.publication_authority,
1413
+ self.acknowledgement_authority,
1414
+ self.resolve_tombstone_expectation,
1415
+ self.delete,
1416
+ )
1417
+ elif schema == _CMS.PUBLICATION_HEALTH_HTTP_REQUEST_SCHEMA:
1418
+ response = receive_health(
1419
+ body,
1420
+ raw_headers,
1421
+ lambda _: True,
1422
+ self.acknowledgement_authority,
1423
+ self.check,
1424
+ contract_sha256=self.contract_sha256,
1425
+ )
1426
+ else:
1427
+ raise CMSReceiverBlocked(
1428
+ "receiver.operation_invalid", retryable=False, http_status=400,
1429
+ )
1430
+ return self._send(start_response, response)
1431
+ except CMSReceiverBlocked as error:
1432
+ return self._blocked(
1433
+ start_response,
1434
+ error.http_status,
1435
+ error.code,
1436
+ retryable=error.retryable,
1437
+ )
1438
+ except Exception:
1439
+ return self._blocked(
1440
+ start_response, 500, "receiver.internal", retryable=True,
1441
+ )