@wildo-ai/saas-backend-lib 1.1.0 → 1.1.2

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 (265) hide show
  1. package/dist/esm/base-classes/services/controllers/controller-resource.backend.service.d.ts +14 -3
  2. package/dist/esm/base-classes/services/controllers/controller-resource.backend.service.d.ts.map +1 -1
  3. package/dist/esm/base-classes/services/controllers/controller-resource.backend.service.js +115 -22
  4. package/dist/esm/base-classes/services/controllers/controller-resource.backend.service.js.map +1 -1
  5. package/dist/esm/base-classes/services/controllers/controllers-registry-handler.backend.service.d.ts +3 -1
  6. package/dist/esm/base-classes/services/controllers/controllers-registry-handler.backend.service.d.ts.map +1 -1
  7. package/dist/esm/base-classes/services/controllers/controllers-registry-handler.backend.service.js +8 -3
  8. package/dist/esm/base-classes/services/controllers/controllers-registry-handler.backend.service.js.map +1 -1
  9. package/dist/esm/base-classes/services/repositories/field-clearing-partition.backend.d.ts +11 -0
  10. package/dist/esm/base-classes/services/repositories/field-clearing-partition.backend.d.ts.map +1 -1
  11. package/dist/esm/base-classes/services/repositories/field-clearing-partition.backend.js +11 -2
  12. package/dist/esm/base-classes/services/repositories/field-clearing-partition.backend.js.map +1 -1
  13. package/dist/esm/base-classes/services/repositories/http-api/http-api-resource-repository.backend.service.d.ts +1 -0
  14. package/dist/esm/base-classes/services/repositories/http-api/http-api-resource-repository.backend.service.d.ts.map +1 -1
  15. package/dist/esm/base-classes/services/repositories/http-api/http-api-resource-repository.backend.service.js +6 -1
  16. package/dist/esm/base-classes/services/repositories/http-api/http-api-resource-repository.backend.service.js.map +1 -1
  17. package/dist/esm/base-classes/services/repositories/http-api/odata-query-compiler.backend.d.ts.map +1 -1
  18. package/dist/esm/base-classes/services/repositories/http-api/odata-query-compiler.backend.js +16 -0
  19. package/dist/esm/base-classes/services/repositories/http-api/odata-query-compiler.backend.js.map +1 -1
  20. package/dist/esm/base-classes/services/repositories/infrastructure-tier-contract.backend.d.ts +20 -6
  21. package/dist/esm/base-classes/services/repositories/infrastructure-tier-contract.backend.d.ts.map +1 -1
  22. package/dist/esm/base-classes/services/repositories/infrastructure-tier-contract.backend.js +88 -32
  23. package/dist/esm/base-classes/services/repositories/infrastructure-tier-contract.backend.js.map +1 -1
  24. package/dist/esm/base-classes/services/repositories/introspection/introspection-resource-repository.backend.service.d.ts +3 -1
  25. package/dist/esm/base-classes/services/repositories/introspection/introspection-resource-repository.backend.service.d.ts.map +1 -1
  26. package/dist/esm/base-classes/services/repositories/introspection/introspection-resource-repository.backend.service.js +32 -7
  27. package/dist/esm/base-classes/services/repositories/introspection/introspection-resource-repository.backend.service.js.map +1 -1
  28. package/dist/esm/base-classes/services/repositories/introspection/introspection-resource-source.backend.port.d.ts +8 -5
  29. package/dist/esm/base-classes/services/repositories/introspection/introspection-resource-source.backend.port.d.ts.map +1 -1
  30. package/dist/esm/base-classes/services/repositories/introspection/introspection-resource-source.backend.port.js.map +1 -1
  31. package/dist/esm/base-classes/services/repositories/postgresql-resource-repository.backend.service.d.ts +31 -0
  32. package/dist/esm/base-classes/services/repositories/postgresql-resource-repository.backend.service.d.ts.map +1 -1
  33. package/dist/esm/base-classes/services/repositories/postgresql-resource-repository.backend.service.js +127 -5
  34. package/dist/esm/base-classes/services/repositories/postgresql-resource-repository.backend.service.js.map +1 -1
  35. package/dist/esm/base-classes/services/repositories/repositories-registry-handler.backend.service.d.ts +71 -1
  36. package/dist/esm/base-classes/services/repositories/repositories-registry-handler.backend.service.d.ts.map +1 -1
  37. package/dist/esm/base-classes/services/repositories/repositories-registry-handler.backend.service.js +62 -2
  38. package/dist/esm/base-classes/services/repositories/repositories-registry-handler.backend.service.js.map +1 -1
  39. package/dist/esm/base-classes/services/repositories/repository-resource.backend.service.d.ts +21 -0
  40. package/dist/esm/base-classes/services/repositories/repository-resource.backend.service.d.ts.map +1 -1
  41. package/dist/esm/base-classes/services/repositories/repository-resource.backend.service.js +126 -4
  42. package/dist/esm/base-classes/services/repositories/repository-resource.backend.service.js.map +1 -1
  43. package/dist/esm/base-classes/services/services/resource-operation-implementation-coverage.backend.utils.d.ts +3 -26
  44. package/dist/esm/base-classes/services/services/resource-operation-implementation-coverage.backend.utils.d.ts.map +1 -1
  45. package/dist/esm/base-classes/services/services/resource-operation-implementation-coverage.backend.utils.js +3 -68
  46. package/dist/esm/base-classes/services/services/resource-operation-implementation-coverage.backend.utils.js.map +1 -1
  47. package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.d.ts.map +1 -1
  48. package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.js +4 -2
  49. package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.js.map +1 -1
  50. package/dist/esm/billing/services/custom-impl/billing-account.mutations.custom-impl.backend.service.js +6 -6
  51. package/dist/esm/billing/services/custom-impl/billing-account.mutations.custom-impl.backend.service.js.map +1 -1
  52. package/dist/esm/compliance/audit-trails/services/audit-event-dispatcher.backend.service.d.ts +38 -0
  53. package/dist/esm/compliance/audit-trails/services/audit-event-dispatcher.backend.service.d.ts.map +1 -1
  54. package/dist/esm/compliance/audit-trails/services/audit-event-dispatcher.backend.service.js +40 -0
  55. package/dist/esm/compliance/audit-trails/services/audit-event-dispatcher.backend.service.js.map +1 -1
  56. package/dist/esm/compliance/audit-trails/services/audit-logs.backend.service.d.ts +20 -0
  57. package/dist/esm/compliance/audit-trails/services/audit-logs.backend.service.d.ts.map +1 -1
  58. package/dist/esm/compliance/audit-trails/services/audit-logs.backend.service.js +28 -0
  59. package/dist/esm/compliance/audit-trails/services/audit-logs.backend.service.js.map +1 -1
  60. package/dist/esm/compliance/audit-trails/services/siem-export-config-cache-invalidation.custom-impl.backend.service.d.ts +12 -0
  61. package/dist/esm/compliance/audit-trails/services/siem-export-config-cache-invalidation.custom-impl.backend.service.d.ts.map +1 -0
  62. package/dist/esm/compliance/audit-trails/services/siem-export-config-cache-invalidation.custom-impl.backend.service.js +118 -0
  63. package/dist/esm/compliance/audit-trails/services/siem-export-config-cache-invalidation.custom-impl.backend.service.js.map +1 -0
  64. package/dist/esm/compliance/audit-trails/services/siem-webhook-delivery.backend.service.d.ts.map +1 -1
  65. package/dist/esm/compliance/audit-trails/services/siem-webhook-delivery.backend.service.js +8 -5
  66. package/dist/esm/compliance/audit-trails/services/siem-webhook-delivery.backend.service.js.map +1 -1
  67. package/dist/esm/compliance/audit-trails/services/utils/ocsf-activity-mapping.backend.utils.d.ts.map +1 -1
  68. package/dist/esm/compliance/audit-trails/services/utils/ocsf-activity-mapping.backend.utils.js +14 -0
  69. package/dist/esm/compliance/audit-trails/services/utils/ocsf-activity-mapping.backend.utils.js.map +1 -1
  70. package/dist/esm/config/custom/application-config-controller.backend.service.d.ts.map +1 -1
  71. package/dist/esm/config/custom/application-config-controller.backend.service.js +7 -1
  72. package/dist/esm/config/custom/application-config-controller.backend.service.js.map +1 -1
  73. package/dist/esm/config/models/app-config.backend.schemas.d.ts +4 -0
  74. package/dist/esm/config/models/app-config.backend.schemas.d.ts.map +1 -1
  75. package/dist/esm/config/services/app-configuration.backend.utils.d.ts +10 -1
  76. package/dist/esm/config/services/app-configuration.backend.utils.d.ts.map +1 -1
  77. package/dist/esm/config/services/app-configuration.backend.utils.js +13 -2
  78. package/dist/esm/config/services/app-configuration.backend.utils.js.map +1 -1
  79. package/dist/esm/data-seeding/services/shared/data-seed-scope-iteration.shared.utils.d.ts +2 -2
  80. package/dist/esm/data-seeding/services/shared/data-seed-scope-iteration.shared.utils.js +2 -2
  81. package/dist/esm/data-seeding/services/shared/data-seed-scope-iteration.shared.utils.js.map +1 -1
  82. package/dist/esm/databases/models/filter.backend.schemas.js +2 -2
  83. package/dist/esm/databases/models/filter.backend.schemas.js.map +1 -1
  84. package/dist/esm/databases/services/aggregation/aggregation-authorization.backend.service.d.ts.map +1 -1
  85. package/dist/esm/databases/services/aggregation/aggregation-authorization.backend.service.js +20 -1
  86. package/dist/esm/databases/services/aggregation/aggregation-authorization.backend.service.js.map +1 -1
  87. package/dist/esm/databases/services/redis.service.d.ts +27 -0
  88. package/dist/esm/databases/services/redis.service.d.ts.map +1 -1
  89. package/dist/esm/databases/services/redis.service.js +29 -0
  90. package/dist/esm/databases/services/redis.service.js.map +1 -1
  91. package/dist/esm/errors/services/error-handler.backend.service.d.ts.map +1 -1
  92. package/dist/esm/errors/services/error-handler.backend.service.js +5 -0
  93. package/dist/esm/errors/services/error-handler.backend.service.js.map +1 -1
  94. package/dist/esm/external-data/controllers/external-data-admin.backend.contracts.d.ts +41 -0
  95. package/dist/esm/external-data/controllers/external-data-admin.backend.contracts.d.ts.map +1 -0
  96. package/dist/esm/external-data/controllers/external-data-admin.backend.contracts.js +40 -0
  97. package/dist/esm/external-data/controllers/external-data-admin.backend.contracts.js.map +1 -0
  98. package/dist/esm/external-data/controllers/external-data-admin.backend.controller.d.ts +64 -0
  99. package/dist/esm/external-data/controllers/external-data-admin.backend.controller.d.ts.map +1 -0
  100. package/dist/esm/external-data/controllers/external-data-admin.backend.controller.js +193 -0
  101. package/dist/esm/external-data/controllers/external-data-admin.backend.controller.js.map +1 -0
  102. package/dist/esm/external-data/external-data-pipeline-run.backend.service.d.ts +13 -2
  103. package/dist/esm/external-data/external-data-pipeline-run.backend.service.d.ts.map +1 -1
  104. package/dist/esm/external-data/external-data-pipeline-run.backend.service.js +8 -2
  105. package/dist/esm/external-data/external-data-pipeline-run.backend.service.js.map +1 -1
  106. package/dist/esm/external-data/external-data-transform.backend.d.ts.map +1 -1
  107. package/dist/esm/external-data/external-data-transform.backend.js +7 -1
  108. package/dist/esm/external-data/external-data-transform.backend.js.map +1 -1
  109. package/dist/esm/external-data/models/external-data-pipeline-state.backend.schemas.d.ts +2 -0
  110. package/dist/esm/external-data/models/external-data-pipeline-state.backend.schemas.d.ts.map +1 -1
  111. package/dist/esm/external-data/models/external-data-pipeline-state.backend.schemas.js +13 -0
  112. package/dist/esm/external-data/models/external-data-pipeline-state.backend.schemas.js.map +1 -1
  113. package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts +14 -0
  114. package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts.map +1 -1
  115. package/dist/esm/external-providers/services/external-providers-registry.backend.service.js +6 -0
  116. package/dist/esm/external-providers/services/external-providers-registry.backend.service.js.map +1 -1
  117. package/dist/esm/files/controllers/file.controller.d.ts +24 -1
  118. package/dist/esm/files/controllers/file.controller.d.ts.map +1 -1
  119. package/dist/esm/files/controllers/file.controller.js +66 -2
  120. package/dist/esm/files/controllers/file.controller.js.map +1 -1
  121. package/dist/esm/files/handlers/file-upload-route-locals.backend.types.d.ts +17 -0
  122. package/dist/esm/files/handlers/file-upload-route-locals.backend.types.d.ts.map +1 -0
  123. package/dist/esm/files/handlers/file-upload-route-locals.backend.types.js +1 -0
  124. package/dist/esm/files/handlers/file-upload-route-locals.backend.types.js.map +1 -0
  125. package/dist/esm/files/handlers/resource-file-upload-handler.backend.service.d.ts +5 -1
  126. package/dist/esm/files/handlers/resource-file-upload-handler.backend.service.d.ts.map +1 -1
  127. package/dist/esm/files/handlers/resource-file-upload-handler.backend.service.js +42 -3
  128. package/dist/esm/files/handlers/resource-file-upload-handler.backend.service.js.map +1 -1
  129. package/dist/esm/files/services/file-operation-handler.backend.service.d.ts +18 -0
  130. package/dist/esm/files/services/file-operation-handler.backend.service.d.ts.map +1 -1
  131. package/dist/esm/files/services/file-operation-handler.backend.service.js +36 -28
  132. package/dist/esm/files/services/file-operation-handler.backend.service.js.map +1 -1
  133. package/dist/esm/files/services/file-upload-grant.backend.service.d.ts +163 -0
  134. package/dist/esm/files/services/file-upload-grant.backend.service.d.ts.map +1 -0
  135. package/dist/esm/files/services/file-upload-grant.backend.service.js +442 -0
  136. package/dist/esm/files/services/file-upload-grant.backend.service.js.map +1 -0
  137. package/dist/esm/files/services/files.backend.service.d.ts +19 -0
  138. package/dist/esm/files/services/files.backend.service.d.ts.map +1 -1
  139. package/dist/esm/files/services/files.backend.service.js +78 -15
  140. package/dist/esm/files/services/files.backend.service.js.map +1 -1
  141. package/dist/esm/files/utils/file-uploader.utils.d.ts.map +1 -1
  142. package/dist/esm/files/utils/file-uploader.utils.js +12 -0
  143. package/dist/esm/files/utils/file-uploader.utils.js.map +1 -1
  144. package/dist/esm/flows-actors/a2a/services/a2a-task-cancellation.backend.service.js +5 -5
  145. package/dist/esm/flows-actors/a2a/services/a2a-task-cancellation.backend.service.js.map +1 -1
  146. package/dist/esm/flows-actors/services/agents.backend.service.d.ts +79 -0
  147. package/dist/esm/flows-actors/services/agents.backend.service.d.ts.map +1 -1
  148. package/dist/esm/flows-actors/services/agents.backend.service.js +319 -42
  149. package/dist/esm/flows-actors/services/agents.backend.service.js.map +1 -1
  150. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts +151 -41
  151. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts.map +1 -1
  152. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js +300 -139
  153. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js.map +1 -1
  154. package/dist/esm/index.d.ts +4 -1
  155. package/dist/esm/index.d.ts.map +1 -1
  156. package/dist/esm/index.js +14 -1
  157. package/dist/esm/index.js.map +1 -1
  158. package/dist/esm/inject/models/container.d.ts.map +1 -1
  159. package/dist/esm/inject/models/container.js +4 -0
  160. package/dist/esm/inject/models/container.js.map +1 -1
  161. package/dist/esm/inject/models/service-types.d.ts +3 -0
  162. package/dist/esm/inject/models/service-types.d.ts.map +1 -1
  163. package/dist/esm/inject/models/service-types.js +2 -0
  164. package/dist/esm/inject/models/service-types.js.map +1 -1
  165. package/dist/esm/mcp/controllers/mcp.controller.d.ts.map +1 -1
  166. package/dist/esm/mcp/controllers/mcp.controller.js +12 -4
  167. package/dist/esm/mcp/controllers/mcp.controller.js.map +1 -1
  168. package/dist/esm/mcp/models/mcp-tool.backend.schemas.d.ts +34 -0
  169. package/dist/esm/mcp/models/mcp-tool.backend.schemas.d.ts.map +1 -1
  170. package/dist/esm/mcp/models/mcp-tool.backend.schemas.js +42 -0
  171. package/dist/esm/mcp/models/mcp-tool.backend.schemas.js.map +1 -1
  172. package/dist/esm/mcp/services/mcp-server.backend.service.d.ts +29 -1
  173. package/dist/esm/mcp/services/mcp-server.backend.service.d.ts.map +1 -1
  174. package/dist/esm/mcp/services/mcp-server.backend.service.js +131 -3
  175. package/dist/esm/mcp/services/mcp-server.backend.service.js.map +1 -1
  176. package/dist/esm/mcp/services/mcp-tool-adapter.backend.service.d.ts +64 -2
  177. package/dist/esm/mcp/services/mcp-tool-adapter.backend.service.d.ts.map +1 -1
  178. package/dist/esm/mcp/services/mcp-tool-adapter.backend.service.js +130 -6
  179. package/dist/esm/mcp/services/mcp-tool-adapter.backend.service.js.map +1 -1
  180. package/dist/esm/notifications/services/notifications-dispatcher.backend.service.d.ts +9 -0
  181. package/dist/esm/notifications/services/notifications-dispatcher.backend.service.d.ts.map +1 -1
  182. package/dist/esm/notifications/services/notifications-dispatcher.backend.service.js +11 -36
  183. package/dist/esm/notifications/services/notifications-dispatcher.backend.service.js.map +1 -1
  184. package/dist/esm/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.backend.service.d.ts.map +1 -1
  185. package/dist/esm/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.backend.service.js +9 -7
  186. package/dist/esm/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.backend.service.js.map +1 -1
  187. package/dist/esm/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.utils.d.ts +5 -5
  188. package/dist/esm/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.utils.d.ts.map +1 -1
  189. package/dist/esm/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.utils.js +14 -13
  190. package/dist/esm/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.utils.js.map +1 -1
  191. package/dist/esm/notifications/webhooks/outbound/m2m-webhook-signed-request.backend.utils.d.ts +2 -1
  192. package/dist/esm/notifications/webhooks/outbound/m2m-webhook-signed-request.backend.utils.d.ts.map +1 -1
  193. package/dist/esm/notifications/webhooks/outbound/m2m-webhook-signed-request.backend.utils.js +2 -2
  194. package/dist/esm/notifications/webhooks/outbound/m2m-webhook-signed-request.backend.utils.js.map +1 -1
  195. package/dist/esm/notifications/webhooks/outbound/services/webhook-config-test-endpoint.custom-impl.backend.service.d.ts.map +1 -1
  196. package/dist/esm/notifications/webhooks/outbound/services/webhook-config-test-endpoint.custom-impl.backend.service.js +9 -7
  197. package/dist/esm/notifications/webhooks/outbound/services/webhook-config-test-endpoint.custom-impl.backend.service.js.map +1 -1
  198. package/dist/esm/organizations/services/organization-unit-custom-implementation.backend.service.d.ts.map +1 -1
  199. package/dist/esm/organizations/services/organization-unit-custom-implementation.backend.service.js +4 -1
  200. package/dist/esm/organizations/services/organization-unit-custom-implementation.backend.service.js.map +1 -1
  201. package/dist/esm/requests/service/websocket.service.d.ts.map +1 -1
  202. package/dist/esm/requests/service/websocket.service.js +56 -7
  203. package/dist/esm/requests/service/websocket.service.js.map +1 -1
  204. package/dist/esm/resources/models/custom-service-implementations.backend.definition.d.ts.map +1 -1
  205. package/dist/esm/resources/models/custom-service-implementations.backend.definition.js +2 -0
  206. package/dist/esm/resources/models/custom-service-implementations.backend.definition.js.map +1 -1
  207. package/dist/esm/resources/services/resource-id.backend.service.js +3 -3
  208. package/dist/esm/resources/services/resource-id.backend.service.js.map +1 -1
  209. package/dist/esm/resources/services/resources-registry.backend.service.js +1 -1
  210. package/dist/esm/resources/services/resources-registry.backend.service.js.map +1 -1
  211. package/dist/esm/runtime/services/execution-context-creator.backend.service.d.ts.map +1 -1
  212. package/dist/esm/runtime/services/execution-context-creator.backend.service.js +13 -7
  213. package/dist/esm/runtime/services/execution-context-creator.backend.service.js.map +1 -1
  214. package/dist/esm/runtime/services/start-application.backend.service.d.ts +28 -0
  215. package/dist/esm/runtime/services/start-application.backend.service.d.ts.map +1 -1
  216. package/dist/esm/runtime/services/start-application.backend.service.js +82 -0
  217. package/dist/esm/runtime/services/start-application.backend.service.js.map +1 -1
  218. package/dist/esm/security/authentications/models/jwt.backend.schemas.d.ts +3 -23
  219. package/dist/esm/security/authentications/models/jwt.backend.schemas.d.ts.map +1 -1
  220. package/dist/esm/security/authentications/models/jwt.backend.schemas.js +20 -45
  221. package/dist/esm/security/authentications/models/jwt.backend.schemas.js.map +1 -1
  222. package/dist/esm/security/authentications/services/auth-methods/auth-method-management.backend.service.d.ts +9 -1
  223. package/dist/esm/security/authentications/services/auth-methods/auth-method-management.backend.service.d.ts.map +1 -1
  224. package/dist/esm/security/authentications/services/auth-methods/auth-method-management.backend.service.js +9 -1
  225. package/dist/esm/security/authentications/services/auth-methods/auth-method-management.backend.service.js.map +1 -1
  226. package/dist/esm/security/authentications/services/auth-methods/auth-mfa.backend.service.d.ts.map +1 -1
  227. package/dist/esm/security/authentications/services/auth-methods/auth-mfa.backend.service.js +5 -3
  228. package/dist/esm/security/authentications/services/auth-methods/auth-mfa.backend.service.js.map +1 -1
  229. package/dist/esm/security/authentications/services/auth-token-issuer.backend.service.d.ts +25 -1
  230. package/dist/esm/security/authentications/services/auth-token-issuer.backend.service.d.ts.map +1 -1
  231. package/dist/esm/security/authentications/services/auth-token-issuer.backend.service.js +55 -4
  232. package/dist/esm/security/authentications/services/auth-token-issuer.backend.service.js.map +1 -1
  233. package/dist/esm/security/authorizations/initiator-organization-confinement.backend.d.ts.map +1 -1
  234. package/dist/esm/security/authorizations/initiator-organization-confinement.backend.js +24 -1
  235. package/dist/esm/security/authorizations/initiator-organization-confinement.backend.js.map +1 -1
  236. package/dist/esm/security/authorizations/privileged-access-review.backend.utils.d.ts +117 -0
  237. package/dist/esm/security/authorizations/privileged-access-review.backend.utils.d.ts.map +1 -0
  238. package/dist/esm/security/authorizations/privileged-access-review.backend.utils.js +104 -0
  239. package/dist/esm/security/authorizations/privileged-access-review.backend.utils.js.map +1 -0
  240. package/dist/esm/security/authorizations/services/authorizations.backend.service.d.ts +101 -6
  241. package/dist/esm/security/authorizations/services/authorizations.backend.service.d.ts.map +1 -1
  242. package/dist/esm/security/authorizations/services/authorizations.backend.service.js +210 -30
  243. package/dist/esm/security/authorizations/services/authorizations.backend.service.js.map +1 -1
  244. package/dist/esm/security/consumable-tokens/batches/consumable-tokens-cleanup.batch.backend.service.d.ts +18 -1
  245. package/dist/esm/security/consumable-tokens/batches/consumable-tokens-cleanup.batch.backend.service.d.ts.map +1 -1
  246. package/dist/esm/security/consumable-tokens/batches/consumable-tokens-cleanup.batch.backend.service.js +110 -74
  247. package/dist/esm/security/consumable-tokens/batches/consumable-tokens-cleanup.batch.backend.service.js.map +1 -1
  248. package/dist/esm/security/consumable-tokens/consumable-token-models.exports.d.ts +22 -0
  249. package/dist/esm/security/consumable-tokens/consumable-token-models.exports.d.ts.map +1 -0
  250. package/dist/esm/security/consumable-tokens/consumable-token-models.exports.js +22 -0
  251. package/dist/esm/security/consumable-tokens/consumable-token-models.exports.js.map +1 -0
  252. package/dist/esm/security/consumable-tokens/models/consumable-tokens.backend.resources-config.schemas.d.ts +17 -3
  253. package/dist/esm/security/consumable-tokens/models/consumable-tokens.backend.resources-config.schemas.d.ts.map +1 -1
  254. package/dist/esm/security/consumable-tokens/models/consumable-tokens.backend.resources-config.schemas.js +12 -0
  255. package/dist/esm/security/consumable-tokens/models/consumable-tokens.backend.resources-config.schemas.js.map +1 -1
  256. package/dist/esm/security/consumable-tokens/services/consumable-token.backend.service.d.ts +20 -0
  257. package/dist/esm/security/consumable-tokens/services/consumable-token.backend.service.d.ts.map +1 -1
  258. package/dist/esm/security/consumable-tokens/services/consumable-token.backend.service.js +58 -5
  259. package/dist/esm/security/consumable-tokens/services/consumable-token.backend.service.js.map +1 -1
  260. package/dist/esm/security/jwt/services/jwt.backend.service.d.ts.map +1 -1
  261. package/dist/esm/security/jwt/services/jwt.backend.service.js +1 -18
  262. package/dist/esm/security/jwt/services/jwt.backend.service.js.map +1 -1
  263. package/dist/tsconfig.build.tsbuildinfo +1 -1
  264. package/package.json +15 -9
  265. package/dist/esm/.builder.pid +0 -9
@@ -1 +1 @@
1
- {"version":3,"file":"m2m-webhook-delivery.batch.backend.service.d.ts","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,aAAa,EACd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAEL,KAAK,sCAAsC,EAC5C,MAAM,4FAA4F,CAAC;AAmBpG;;;;;GAKG;AACH,eAAO,MAAM,kCAAkC;;;;;;iBAM7C,CAAC;AAEH,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAE9F,eAAO,MAAM,mCAAmC;;;;;;;;;;;;iBAiB9C,CAAC;AAEH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC;AAMhG,eAAO,MAAM,8BAA8B,EAAE,sCAAsC,CACjF,OAAO,kCAAkC,EACzC,OAAO,mCAAmC,CAsU1C,CAAC"}
1
+ {"version":3,"file":"m2m-webhook-delivery.batch.backend.service.d.ts","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,aAAa,EAEd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAEL,KAAK,sCAAsC,EAC5C,MAAM,4FAA4F,CAAC;AAmBpG;;;;;GAKG;AACH,eAAO,MAAM,kCAAkC;;;;;;iBAM7C,CAAC;AAEH,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAE9F,eAAO,MAAM,mCAAmC;;;;;;;;;;;;iBAiB9C,CAAC;AAEH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC;AAMhG,eAAO,MAAM,8BAA8B,EAAE,sCAAsC,CACjF,OAAO,kCAAkC,EACzC,OAAO,mCAAmC,CAwU1C,CAAC"}
@@ -28,7 +28,7 @@
28
28
  * (precedent: same pattern for an event-triggered custom batch).
29
29
  */
30
30
  import { z } from 'zod';
31
- import { AuthScopeType, } from '@wildo-ai/saas-models';
31
+ import { AuthScopeType, M2M_WEBHOOK_DELIVERY_CONTRACT, } from '@wildo-ai/saas-models';
32
32
  import { createCustomBatchExecutionService, } from '../../../../base-classes/services/batches/batches-cronjobs-custom-execution.service.js';
33
33
  import { ENGINE_M2M_WEBHOOK_DELIVERY_BATCH_MANIFEST } from '../../../../base-classes/services/batches/engine-custom-batch-manifests.js';
34
34
  import { describeCaughtError, projectCaughtErrorLogFields } from '../../../../errors/utils/error-log-level-classification.backend.utils.js';
@@ -204,12 +204,14 @@ export const m2mWebhookDeliveryBatchService = createCustomBatchExecutionService(
204
204
  // refreshed per attempt and so cannot serve as a replay marker.
205
205
  // See `m2m-webhook-dispatcher.backend.service.ts` (`signatureJti`
206
206
  // creation site) for the full consumer-contract JSDoc.
207
- jti: row.signatureJti,
208
- bodyHash: row.payloadHash,
209
- bodyHashAlg: M2M_WEBHOOK_BODY_HASH_ALGORITHM,
210
- eventChannel: row.channel,
211
- resourceIdentifier: row.resourceIdentifier,
212
- operationIdentifier: row.operationIdentifier,
207
+ // Claim NAMES come from the published receiver contract so the customer
208
+ // documentation and this signing site cannot drift apart.
209
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.deliveryId]: row.signatureJti,
210
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.bodyHash]: row.payloadHash,
211
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.bodyHashAlgorithm]: M2M_WEBHOOK_BODY_HASH_ALGORITHM,
212
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.eventChannel]: row.channel,
213
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.resourceIdentifier]: row.resourceIdentifier,
214
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.operationIdentifier]: row.operationIdentifier,
213
215
  // `iss`, `iat`, `exp`, `aud`, `kid` defaults come from
214
216
  // `JwtPrivateKeySignOptions[APPLICATION_WEBHOOK]`. We don't
215
217
  // override them here — the consumer-facing contract is the
@@ -1 +1 @@
1
- {"version":3,"file":"m2m-webhook-delivery.batch.backend.service.js","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * M2M Webhook Delivery Batch Worker\n *\n * Event-triggered custom batch — one job per delivery row enqueued by\n * `M2MWebhookDispatcherBackendService.enqueueDelivery`. The worker:\n * 1. Re-reads the row from `M2MWebhookDeliveryLog` (ground truth —\n * we never trust in-flight queue state for the body or signature).\n * 2. Marks it `IN_FLIGHT` (incrementing `attemptCount`).\n * 3. Builds the JWT signature with `JwtPrivateKeys.APPLICATION_WEBHOOK`.\n * 4. POSTs the body with a 30s `AbortController` timeout.\n * 5. Classifies the response and records the terminal/intermediate\n * transition on the row.\n *\n * Status classification (D6):\n * - 2xx → DELIVERED (terminal success).\n * - 4xx, excluding 408 / 429 → FAILED (terminal — non-retryable).\n * - 408 / 429 / 5xx / network → mark failed attempt; if\n * `attemptCount >= MAX_ATTEMPTS`,\n * transition to FAILED.\n *\n * Custom batches are factory-created rather than constructor-injected. This\n * batch therefore declares its exact services through `resolveDependencies`;\n * the executor resolves them lazily for each invocation and `doHandle`\n * receives only the typed named result.\n *\n * @see m2m-webhook-organization-delivery.md — Phase 3 / Step 10\n * @see notifications/email/services/email-batch-send.batch.backend.service.ts\n * (precedent: same pattern for an event-triggered custom batch).\n */\n\nimport { z } from 'zod';\nimport {\n AuthScopeType,\n} from '@wildo-ai/saas-models';\nimport {\n createCustomBatchExecutionService,\n type BatchesCronjobsCustomExecution_Service,\n} from '~wildo-backend-lib/base-classes/services/batches/batches-cronjobs-custom-execution.service';\nimport { ENGINE_M2M_WEBHOOK_DELIVERY_BATCH_MANIFEST } from '~wildo-backend-lib/base-classes/services/batches/engine-custom-batch-manifests';\nimport { describeCaughtError, projectCaughtErrorLogFields } from '~wildo-backend-lib/errors/utils/error-log-level-classification.backend.utils';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport type { JwtBackendService } from '~wildo-backend-lib/security/jwt/services/jwt.backend.service';\nimport { JwtPrivateKeys } from '~wildo-backend-lib/security/authentications/models/jwt.backend.schemas';\nimport type { MetricsService } from '~wildo-backend-lib/monitoring/observability/services/metrics.service';\nimport type { M2MWebhookDeliveryLogRepository } from '~wildo-backend-lib/notifications/webhooks/outbound/services/m2m-webhook-delivery-log.repository';\nimport {\n MAX_ATTEMPTS,\n classifyResponseStatus,\n computeNextAttemptAt,\n} from './m2m-webhook-delivery.batch.utils';\nimport { M2M_WEBHOOK_BODY_HASH_ALGORITHM, sendSignedWebhookRequest } from '../m2m-webhook-signed-request.backend.utils';\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Input / Output schemas\n// ═══════════════════════════════════════════════════════════════════════════\n\n/**\n * The dispatcher writes the row first, then enqueues this lightweight\n * payload. Keeping the payload minimal (id + scope) makes the worker\n * resilient to queue / DB race conditions: re-reading the row is the\n * source of truth.\n */\nexport const M2MWebhookDeliveryBatchInputSchema = z.object({\n scope: z.object({\n type: z.enum(AuthScopeType),\n id: z.string().min(1),\n }),\n deliveryLogId: z.string().min(1),\n});\n\nexport type M2MWebhookDeliveryBatchInput = z.infer<typeof M2MWebhookDeliveryBatchInputSchema>;\n\nexport const M2MWebhookDeliveryBatchOutputSchema = z.object({\n deliveryLogId: z.string(),\n /**\n * High-level outcome string. Stored only in the worker output for\n * cron / log inspection; the persisted truth lives on the row's\n * `status` field.\n */\n outcome: z.enum([\n 'delivered',\n 'retry_scheduled',\n 'failed_terminal',\n 'skipped_missing_row',\n 'skipped_already_leased',\n ]),\n attemptCount: z.number().int().nonnegative(),\n responseStatus: z.number().int().optional(),\n durationMs: z.number().int().nonnegative(),\n});\n\nexport type M2MWebhookDeliveryBatchOutput = z.infer<typeof M2MWebhookDeliveryBatchOutputSchema>;\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Batch Service\n// ═══════════════════════════════════════════════════════════════════════════\n\nexport const m2mWebhookDeliveryBatchService: BatchesCronjobsCustomExecution_Service<\n typeof M2MWebhookDeliveryBatchInputSchema,\n typeof M2MWebhookDeliveryBatchOutputSchema\n> = createCustomBatchExecutionService({\n input: M2MWebhookDeliveryBatchInputSchema,\n output: M2MWebhookDeliveryBatchOutputSchema,\n batchCronjob_Context: {\n // Anchored on the `APPLICATION` primary scope rather than a fake\n // `ORGANIZATION_M2M_WEBHOOK_DELIVERY_LOG` operation: an event-triggered\n // polymorphic worker has no honest \"primary\" resource (each enqueued\n // row carries its own `scope: { type, id }` and routes to either\n // `ORGANIZATION_M2M_WEBHOOK_DELIVERY_LOG` or\n // `APPLICATION_M2M_WEBHOOK_DELIVERY_LOG` via\n // `resolveDeliveryLogResourceType` in\n // `m2m-webhook-delivery-log.repository.ts`).\n //\n // The bootstrap path inside\n // `ExecutionContextCreatorBackendService.createForInternalCustomBatch`\n // resolves an `APPLICATION.READ` operation, populates the application\n // id, then swaps in `notifications:m2m-webhook-delivery` via\n // `withOperation()`. Audit trails surface the honest batch identity\n // and authorization runs against `APP_ADMIN_SUPER_ADMIN @ APPLICATION`\n // (see `createInternalSystemRoles()`), exactly as before, but without\n // pretending the worker mutates the org-variant log on every job —\n // including for app-scoped deliveries where that anchor was a\n // flat-out lie. Per-row resource-type resolution (org vs app) stays\n // inside the worker and is driven off the per-row `input.scope.type`.\n kind: ENGINE_M2M_WEBHOOK_DELIVERY_BATCH_MANIFEST.kind,\n operationIdentifier: ENGINE_M2M_WEBHOOK_DELIVERY_BATCH_MANIFEST.operationIdentifier,\n primaryScope: ENGINE_M2M_WEBHOOK_DELIVERY_BATCH_MANIFEST.primaryScope,\n },\n resolveDependencies: ({ get }) => ({\n repository: get<M2MWebhookDeliveryLogRepository>(\n SAAS_SERVICE_TYPES.M2MWebhookDeliveryLogRepository,\n ),\n jwtService: get<JwtBackendService>(SAAS_SERVICE_TYPES.JwtBackendService),\n metricsService: get<MetricsService>(SAAS_SERVICE_TYPES.MetricsService),\n }),\n doHandle: async (input, { logger, executionContext, dependencies }) => {\n const startedAt = Date.now();\n const logDebug = logger.debugByType('M2MWebhookDeliveryBatch');\n const {\n repository: repo,\n jwtService,\n metricsService,\n } = dependencies;\n\n /**\n * Single funnel for every `doHandle` return-point. Mirrors the\n * batch output back to the caller while firing the Phase 5 /\n * Step 17 outcome counter exactly once per worker invocation.\n *\n * Kept inline (not extracted to a module-level helper) because it\n * closes over `metricsService` and `input.scope.type` — lifting it\n * would need re-threading both via parameters on every call site\n * and would dilute the fire-exactly-once contract this funnel\n * enforces.\n *\n * Metric emission is error-isolated for the same reason the audit\n * helper swallows its failures: observability MUST NEVER fail the\n * main flow. By the time `recordAndReturn` runs, the row has\n * already been persisted with the correct terminal/intermediate\n * status — letting an OpenTelemetry exporter glitch abort the\n * batch invocation here would leave the custom-batch runner\n * reporting the run as failed even though the real delivery\n * completed and the truth-source row reflects success.\n */\n const recordAndReturn = (\n output: M2MWebhookDeliveryBatchOutput,\n ): M2MWebhookDeliveryBatchOutput => {\n try {\n metricsService.recordM2MWebhookDeliveryOutcome({\n scope: input.scope.type,\n outcome: output.outcome,\n });\n } catch (error) {\n logger.warn('Failed to record M2M webhook delivery outcome metric', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n outcome: output.outcome,\n ...projectCaughtErrorLogFields(error),\n });\n }\n return output;\n };\n\n // ── Load the row (ground truth) ────────────────────────────\n const row = await repo.read(input.scope, executionContext, input.deliveryLogId);\n if (!row) {\n // Possible if the row was purged between enqueue and worker\n // execution (admin `PURGE_OLD`). Quiet skip — there is nothing\n // to deliver and no caller to surface the absence to.\n logger.warn('Webhook delivery row not found; skipping', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'skipped_missing_row',\n attemptCount: 0,\n durationMs: Date.now() - startedAt,\n });\n }\n\n const nextAttemptCount = (row.attemptCount ?? 0) + 1;\n\n // ── Lease the row (atomic CAS) ─────────────────────────────\n // `markInFlight` is a compare-and-swap on `status: PENDING`. If\n // another worker already leased the row (cron retry sweep + queue\n // redelivery race), the CAS returns false and we exit cleanly —\n // double-POSTs and double-counted attempts would corrupt the\n // retry budget and the consumer's idempotency guarantees.\n const leased = await repo.markInFlight(\n input.scope,\n executionContext,\n input.deliveryLogId,\n nextAttemptCount,\n );\n if (!leased) {\n logger.warn('Webhook delivery row already leased by another worker; skipping', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n currentStatus: row.status,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'skipped_already_leased',\n attemptCount: row.attemptCount ?? 0,\n durationMs: Date.now() - startedAt,\n });\n }\n\n // ── Build the JWT signature (D5: claims-JWT) ───────────────\n // The body itself is NOT in the JWT — only its sha256 hash. This\n // keeps the header small (some HTTP intermediaries cap header\n // size) and lets consumers verify integrity independently of the\n // raw body bytes (which they can re-hash).\n const claims = {\n // `jti` is the consumer-facing dedup key. It is stable across\n // every retry of THIS delivery row (same value re-signed each\n // attempt) and unique across rows. Consumers MUST dedup on\n // `jti` — the rest of the token (`iat`, `exp`, signature) is\n // refreshed per attempt and so cannot serve as a replay marker.\n // See `m2m-webhook-dispatcher.backend.service.ts` (`signatureJti`\n // creation site) for the full consumer-contract JSDoc.\n jti: row.signatureJti,\n bodyHash: row.payloadHash,\n bodyHashAlg: M2M_WEBHOOK_BODY_HASH_ALGORITHM,\n eventChannel: row.channel,\n resourceIdentifier: row.resourceIdentifier,\n operationIdentifier: row.operationIdentifier,\n // `iss`, `iat`, `exp`, `aud`, `kid` defaults come from\n // `JwtPrivateKeySignOptions[APPLICATION_WEBHOOK]`. We don't\n // override them here — the consumer-facing contract is the\n // signing options table, not this call site.\n };\n\n let signature: string;\n try {\n signature = await jwtService.sign(claims, JwtPrivateKeys.APPLICATION_WEBHOOK);\n } catch (error) {\n // Signing failure is a configuration problem (missing key) —\n // re-trying won't help until the operator fixes secrets, so we\n // mark the row as terminally failed and let admin RETRY pick it\n // up after the fix.\n const reason = describeCaughtError(error);\n logger.error('Failed to sign webhook payload — marking delivery FAILED', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n attempt: nextAttemptCount,\n error,\n });\n await repo.markFinallyFailed(input.scope, executionContext, input.deliveryLogId, {\n failureReason: `JWT signing failed: ${reason}`,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'failed_terminal',\n attemptCount: nextAttemptCount,\n durationMs: Date.now() - startedAt,\n });\n }\n\n // ── POST the body ──────────────────────────────────────────\n // Shared with the `webhookConfig.testEndpoint` probe (see\n // `../m2m-webhook-signed-request.backend.utils`). The admin-facing\n // connectivity test must exercise THIS transport, not a parallel one,\n // or a green test would prove nothing about real deliveries.\n const bodyBytes =\n typeof row.payload === 'string' ? row.payload : JSON.stringify(row.payload);\n const { responseStatus, responseBodyTruncated, networkError } = await sendSignedWebhookRequest({\n url: row.httpUrl,\n body: bodyBytes,\n signature,\n });\n\n // Job-level duration: spans the row re-read and lease as well as the POST,\n // so it stays the batch's own measurement rather than the transport's.\n const durationMs = Date.now() - startedAt;\n\n // ── Classify + persist transition ──────────────────────────\n if (responseStatus !== undefined) {\n const verdict = classifyResponseStatus(responseStatus);\n\n if (verdict === 'delivered') {\n await repo.markDelivered(input.scope, executionContext, input.deliveryLogId, {\n responseStatus,\n responseBodyTruncated,\n });\n logDebug('Webhook delivered', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n httpStatus: responseStatus,\n durationMs,\n attempt: nextAttemptCount,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'delivered',\n attemptCount: nextAttemptCount,\n responseStatus,\n durationMs,\n });\n }\n\n if (verdict === 'fatal') {\n await repo.markFinallyFailed(input.scope, executionContext, input.deliveryLogId, {\n responseStatus,\n responseBodyTruncated,\n failureReason: `non-retryable HTTP status ${responseStatus}`,\n });\n logger.warn('Webhook delivery failed (non-retryable)', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n httpStatus: responseStatus,\n durationMs,\n attempt: nextAttemptCount,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'failed_terminal',\n attemptCount: nextAttemptCount,\n responseStatus,\n durationMs,\n });\n }\n\n // verdict === 'retry' — fall through to the retry branch below.\n }\n\n // Retryable failure (5xx, 408, 429, network/timeout). Decide\n // between scheduling another attempt and giving up.\n const failureReason =\n networkError ?? `retryable HTTP status ${responseStatus ?? 'unknown'}`;\n\n if (nextAttemptCount >= MAX_ATTEMPTS) {\n await repo.markFinallyFailed(input.scope, executionContext, input.deliveryLogId, {\n responseStatus,\n responseBodyTruncated,\n failureReason: `${failureReason} (retry cap ${MAX_ATTEMPTS} reached)`,\n });\n logger.warn('Webhook delivery failed (retry cap reached)', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n httpStatus: responseStatus,\n durationMs,\n attempt: nextAttemptCount,\n failureReason,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'failed_terminal',\n attemptCount: nextAttemptCount,\n responseStatus,\n durationMs,\n });\n }\n\n const nextAttemptAt = computeNextAttemptAt(nextAttemptCount);\n await repo.markFailedAttempt(input.scope, executionContext, input.deliveryLogId, {\n responseStatus,\n responseBodyTruncated,\n failureReason,\n nextAttemptAt,\n });\n logDebug('Webhook delivery retry scheduled', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n httpStatus: responseStatus,\n durationMs,\n attempt: nextAttemptCount,\n nextAttemptAt: nextAttemptAt.toISOString(),\n failureReason,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'retry_scheduled',\n attemptCount: nextAttemptCount,\n responseStatus,\n durationMs,\n });\n },\n});\n\n// Batch reference identifier lives in the declarative manifest registry\n// (`engine-custom-batch-manifests`); import `M2M_WEBHOOK_DELIVERY_BATCH_REF`\n// from there to keep a single source of truth across runtime wiring,\n// queue dispatch, and specification cross-validation.\n"]}
1
+ {"version":3,"file":"m2m-webhook-delivery.batch.backend.service.js","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * M2M Webhook Delivery Batch Worker\n *\n * Event-triggered custom batch — one job per delivery row enqueued by\n * `M2MWebhookDispatcherBackendService.enqueueDelivery`. The worker:\n * 1. Re-reads the row from `M2MWebhookDeliveryLog` (ground truth —\n * we never trust in-flight queue state for the body or signature).\n * 2. Marks it `IN_FLIGHT` (incrementing `attemptCount`).\n * 3. Builds the JWT signature with `JwtPrivateKeys.APPLICATION_WEBHOOK`.\n * 4. POSTs the body with a 30s `AbortController` timeout.\n * 5. Classifies the response and records the terminal/intermediate\n * transition on the row.\n *\n * Status classification (D6):\n * - 2xx → DELIVERED (terminal success).\n * - 4xx, excluding 408 / 429 → FAILED (terminal — non-retryable).\n * - 408 / 429 / 5xx / network → mark failed attempt; if\n * `attemptCount >= MAX_ATTEMPTS`,\n * transition to FAILED.\n *\n * Custom batches are factory-created rather than constructor-injected. This\n * batch therefore declares its exact services through `resolveDependencies`;\n * the executor resolves them lazily for each invocation and `doHandle`\n * receives only the typed named result.\n *\n * @see m2m-webhook-organization-delivery.md — Phase 3 / Step 10\n * @see notifications/email/services/email-batch-send.batch.backend.service.ts\n * (precedent: same pattern for an event-triggered custom batch).\n */\n\nimport { z } from 'zod';\nimport {\n AuthScopeType,\n M2M_WEBHOOK_DELIVERY_CONTRACT,\n} from '@wildo-ai/saas-models';\nimport {\n createCustomBatchExecutionService,\n type BatchesCronjobsCustomExecution_Service,\n} from '~wildo-backend-lib/base-classes/services/batches/batches-cronjobs-custom-execution.service';\nimport { ENGINE_M2M_WEBHOOK_DELIVERY_BATCH_MANIFEST } from '~wildo-backend-lib/base-classes/services/batches/engine-custom-batch-manifests';\nimport { describeCaughtError, projectCaughtErrorLogFields } from '~wildo-backend-lib/errors/utils/error-log-level-classification.backend.utils';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport type { JwtBackendService } from '~wildo-backend-lib/security/jwt/services/jwt.backend.service';\nimport { JwtPrivateKeys } from '~wildo-backend-lib/security/authentications/models/jwt.backend.schemas';\nimport type { MetricsService } from '~wildo-backend-lib/monitoring/observability/services/metrics.service';\nimport type { M2MWebhookDeliveryLogRepository } from '~wildo-backend-lib/notifications/webhooks/outbound/services/m2m-webhook-delivery-log.repository';\nimport {\n MAX_ATTEMPTS,\n classifyResponseStatus,\n computeNextAttemptAt,\n} from './m2m-webhook-delivery.batch.utils';\nimport { M2M_WEBHOOK_BODY_HASH_ALGORITHM, sendSignedWebhookRequest } from '../m2m-webhook-signed-request.backend.utils';\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Input / Output schemas\n// ═══════════════════════════════════════════════════════════════════════════\n\n/**\n * The dispatcher writes the row first, then enqueues this lightweight\n * payload. Keeping the payload minimal (id + scope) makes the worker\n * resilient to queue / DB race conditions: re-reading the row is the\n * source of truth.\n */\nexport const M2MWebhookDeliveryBatchInputSchema = z.object({\n scope: z.object({\n type: z.enum(AuthScopeType),\n id: z.string().min(1),\n }),\n deliveryLogId: z.string().min(1),\n});\n\nexport type M2MWebhookDeliveryBatchInput = z.infer<typeof M2MWebhookDeliveryBatchInputSchema>;\n\nexport const M2MWebhookDeliveryBatchOutputSchema = z.object({\n deliveryLogId: z.string(),\n /**\n * High-level outcome string. Stored only in the worker output for\n * cron / log inspection; the persisted truth lives on the row's\n * `status` field.\n */\n outcome: z.enum([\n 'delivered',\n 'retry_scheduled',\n 'failed_terminal',\n 'skipped_missing_row',\n 'skipped_already_leased',\n ]),\n attemptCount: z.number().int().nonnegative(),\n responseStatus: z.number().int().optional(),\n durationMs: z.number().int().nonnegative(),\n});\n\nexport type M2MWebhookDeliveryBatchOutput = z.infer<typeof M2MWebhookDeliveryBatchOutputSchema>;\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Batch Service\n// ═══════════════════════════════════════════════════════════════════════════\n\nexport const m2mWebhookDeliveryBatchService: BatchesCronjobsCustomExecution_Service<\n typeof M2MWebhookDeliveryBatchInputSchema,\n typeof M2MWebhookDeliveryBatchOutputSchema\n> = createCustomBatchExecutionService({\n input: M2MWebhookDeliveryBatchInputSchema,\n output: M2MWebhookDeliveryBatchOutputSchema,\n batchCronjob_Context: {\n // Anchored on the `APPLICATION` primary scope rather than a fake\n // `ORGANIZATION_M2M_WEBHOOK_DELIVERY_LOG` operation: an event-triggered\n // polymorphic worker has no honest \"primary\" resource (each enqueued\n // row carries its own `scope: { type, id }` and routes to either\n // `ORGANIZATION_M2M_WEBHOOK_DELIVERY_LOG` or\n // `APPLICATION_M2M_WEBHOOK_DELIVERY_LOG` via\n // `resolveDeliveryLogResourceType` in\n // `m2m-webhook-delivery-log.repository.ts`).\n //\n // The bootstrap path inside\n // `ExecutionContextCreatorBackendService.createForInternalCustomBatch`\n // resolves an `APPLICATION.READ` operation, populates the application\n // id, then swaps in `notifications:m2m-webhook-delivery` via\n // `withOperation()`. Audit trails surface the honest batch identity\n // and authorization runs against `APP_ADMIN_SUPER_ADMIN @ APPLICATION`\n // (see `createInternalSystemRoles()`), exactly as before, but without\n // pretending the worker mutates the org-variant log on every job —\n // including for app-scoped deliveries where that anchor was a\n // flat-out lie. Per-row resource-type resolution (org vs app) stays\n // inside the worker and is driven off the per-row `input.scope.type`.\n kind: ENGINE_M2M_WEBHOOK_DELIVERY_BATCH_MANIFEST.kind,\n operationIdentifier: ENGINE_M2M_WEBHOOK_DELIVERY_BATCH_MANIFEST.operationIdentifier,\n primaryScope: ENGINE_M2M_WEBHOOK_DELIVERY_BATCH_MANIFEST.primaryScope,\n },\n resolveDependencies: ({ get }) => ({\n repository: get<M2MWebhookDeliveryLogRepository>(\n SAAS_SERVICE_TYPES.M2MWebhookDeliveryLogRepository,\n ),\n jwtService: get<JwtBackendService>(SAAS_SERVICE_TYPES.JwtBackendService),\n metricsService: get<MetricsService>(SAAS_SERVICE_TYPES.MetricsService),\n }),\n doHandle: async (input, { logger, executionContext, dependencies }) => {\n const startedAt = Date.now();\n const logDebug = logger.debugByType('M2MWebhookDeliveryBatch');\n const {\n repository: repo,\n jwtService,\n metricsService,\n } = dependencies;\n\n /**\n * Single funnel for every `doHandle` return-point. Mirrors the\n * batch output back to the caller while firing the Phase 5 /\n * Step 17 outcome counter exactly once per worker invocation.\n *\n * Kept inline (not extracted to a module-level helper) because it\n * closes over `metricsService` and `input.scope.type` — lifting it\n * would need re-threading both via parameters on every call site\n * and would dilute the fire-exactly-once contract this funnel\n * enforces.\n *\n * Metric emission is error-isolated for the same reason the audit\n * helper swallows its failures: observability MUST NEVER fail the\n * main flow. By the time `recordAndReturn` runs, the row has\n * already been persisted with the correct terminal/intermediate\n * status — letting an OpenTelemetry exporter glitch abort the\n * batch invocation here would leave the custom-batch runner\n * reporting the run as failed even though the real delivery\n * completed and the truth-source row reflects success.\n */\n const recordAndReturn = (\n output: M2MWebhookDeliveryBatchOutput,\n ): M2MWebhookDeliveryBatchOutput => {\n try {\n metricsService.recordM2MWebhookDeliveryOutcome({\n scope: input.scope.type,\n outcome: output.outcome,\n });\n } catch (error) {\n logger.warn('Failed to record M2M webhook delivery outcome metric', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n outcome: output.outcome,\n ...projectCaughtErrorLogFields(error),\n });\n }\n return output;\n };\n\n // ── Load the row (ground truth) ────────────────────────────\n const row = await repo.read(input.scope, executionContext, input.deliveryLogId);\n if (!row) {\n // Possible if the row was purged between enqueue and worker\n // execution (admin `PURGE_OLD`). Quiet skip — there is nothing\n // to deliver and no caller to surface the absence to.\n logger.warn('Webhook delivery row not found; skipping', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'skipped_missing_row',\n attemptCount: 0,\n durationMs: Date.now() - startedAt,\n });\n }\n\n const nextAttemptCount = (row.attemptCount ?? 0) + 1;\n\n // ── Lease the row (atomic CAS) ─────────────────────────────\n // `markInFlight` is a compare-and-swap on `status: PENDING`. If\n // another worker already leased the row (cron retry sweep + queue\n // redelivery race), the CAS returns false and we exit cleanly —\n // double-POSTs and double-counted attempts would corrupt the\n // retry budget and the consumer's idempotency guarantees.\n const leased = await repo.markInFlight(\n input.scope,\n executionContext,\n input.deliveryLogId,\n nextAttemptCount,\n );\n if (!leased) {\n logger.warn('Webhook delivery row already leased by another worker; skipping', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n currentStatus: row.status,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'skipped_already_leased',\n attemptCount: row.attemptCount ?? 0,\n durationMs: Date.now() - startedAt,\n });\n }\n\n // ── Build the JWT signature (D5: claims-JWT) ───────────────\n // The body itself is NOT in the JWT — only its sha256 hash. This\n // keeps the header small (some HTTP intermediaries cap header\n // size) and lets consumers verify integrity independently of the\n // raw body bytes (which they can re-hash).\n const claims = {\n // `jti` is the consumer-facing dedup key. It is stable across\n // every retry of THIS delivery row (same value re-signed each\n // attempt) and unique across rows. Consumers MUST dedup on\n // `jti` — the rest of the token (`iat`, `exp`, signature) is\n // refreshed per attempt and so cannot serve as a replay marker.\n // See `m2m-webhook-dispatcher.backend.service.ts` (`signatureJti`\n // creation site) for the full consumer-contract JSDoc.\n // Claim NAMES come from the published receiver contract so the customer\n // documentation and this signing site cannot drift apart.\n [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.deliveryId]: row.signatureJti,\n [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.bodyHash]: row.payloadHash,\n [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.bodyHashAlgorithm]: M2M_WEBHOOK_BODY_HASH_ALGORITHM,\n [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.eventChannel]: row.channel,\n [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.resourceIdentifier]: row.resourceIdentifier,\n [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.operationIdentifier]: row.operationIdentifier,\n // `iss`, `iat`, `exp`, `aud`, `kid` defaults come from\n // `JwtPrivateKeySignOptions[APPLICATION_WEBHOOK]`. We don't\n // override them here — the consumer-facing contract is the\n // signing options table, not this call site.\n };\n\n let signature: string;\n try {\n signature = await jwtService.sign(claims, JwtPrivateKeys.APPLICATION_WEBHOOK);\n } catch (error) {\n // Signing failure is a configuration problem (missing key) —\n // re-trying won't help until the operator fixes secrets, so we\n // mark the row as terminally failed and let admin RETRY pick it\n // up after the fix.\n const reason = describeCaughtError(error);\n logger.error('Failed to sign webhook payload — marking delivery FAILED', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n attempt: nextAttemptCount,\n error,\n });\n await repo.markFinallyFailed(input.scope, executionContext, input.deliveryLogId, {\n failureReason: `JWT signing failed: ${reason}`,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'failed_terminal',\n attemptCount: nextAttemptCount,\n durationMs: Date.now() - startedAt,\n });\n }\n\n // ── POST the body ──────────────────────────────────────────\n // Shared with the `webhookConfig.testEndpoint` probe (see\n // `../m2m-webhook-signed-request.backend.utils`). The admin-facing\n // connectivity test must exercise THIS transport, not a parallel one,\n // or a green test would prove nothing about real deliveries.\n const bodyBytes =\n typeof row.payload === 'string' ? row.payload : JSON.stringify(row.payload);\n const { responseStatus, responseBodyTruncated, networkError } = await sendSignedWebhookRequest({\n url: row.httpUrl,\n body: bodyBytes,\n signature,\n });\n\n // Job-level duration: spans the row re-read and lease as well as the POST,\n // so it stays the batch's own measurement rather than the transport's.\n const durationMs = Date.now() - startedAt;\n\n // ── Classify + persist transition ──────────────────────────\n if (responseStatus !== undefined) {\n const verdict = classifyResponseStatus(responseStatus);\n\n if (verdict === 'delivered') {\n await repo.markDelivered(input.scope, executionContext, input.deliveryLogId, {\n responseStatus,\n responseBodyTruncated,\n });\n logDebug('Webhook delivered', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n httpStatus: responseStatus,\n durationMs,\n attempt: nextAttemptCount,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'delivered',\n attemptCount: nextAttemptCount,\n responseStatus,\n durationMs,\n });\n }\n\n if (verdict === 'fatal') {\n await repo.markFinallyFailed(input.scope, executionContext, input.deliveryLogId, {\n responseStatus,\n responseBodyTruncated,\n failureReason: `non-retryable HTTP status ${responseStatus}`,\n });\n logger.warn('Webhook delivery failed (non-retryable)', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n httpStatus: responseStatus,\n durationMs,\n attempt: nextAttemptCount,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'failed_terminal',\n attemptCount: nextAttemptCount,\n responseStatus,\n durationMs,\n });\n }\n\n // verdict === 'retry' — fall through to the retry branch below.\n }\n\n // Retryable failure (5xx, 408, 429, network/timeout). Decide\n // between scheduling another attempt and giving up.\n const failureReason =\n networkError ?? `retryable HTTP status ${responseStatus ?? 'unknown'}`;\n\n if (nextAttemptCount >= MAX_ATTEMPTS) {\n await repo.markFinallyFailed(input.scope, executionContext, input.deliveryLogId, {\n responseStatus,\n responseBodyTruncated,\n failureReason: `${failureReason} (retry cap ${MAX_ATTEMPTS} reached)`,\n });\n logger.warn('Webhook delivery failed (retry cap reached)', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n httpStatus: responseStatus,\n durationMs,\n attempt: nextAttemptCount,\n failureReason,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'failed_terminal',\n attemptCount: nextAttemptCount,\n responseStatus,\n durationMs,\n });\n }\n\n const nextAttemptAt = computeNextAttemptAt(nextAttemptCount);\n await repo.markFailedAttempt(input.scope, executionContext, input.deliveryLogId, {\n responseStatus,\n responseBodyTruncated,\n failureReason,\n nextAttemptAt,\n });\n logDebug('Webhook delivery retry scheduled', {\n deliveryLogId: input.deliveryLogId,\n scopeType: input.scope.type,\n scopeId: input.scope.id,\n endpointId: row.endpointId,\n httpUrl: row.httpUrl,\n signatureJti: row.signatureJti,\n httpStatus: responseStatus,\n durationMs,\n attempt: nextAttemptCount,\n nextAttemptAt: nextAttemptAt.toISOString(),\n failureReason,\n });\n return recordAndReturn({\n deliveryLogId: input.deliveryLogId,\n outcome: 'retry_scheduled',\n attemptCount: nextAttemptCount,\n responseStatus,\n durationMs,\n });\n },\n});\n\n// Batch reference identifier lives in the declarative manifest registry\n// (`engine-custom-batch-manifests`); import `M2M_WEBHOOK_DELIVERY_BATCH_REF`\n// from there to keep a single source of truth across runtime wiring,\n// queue dispatch, and specification cross-validation.\n"]}
@@ -16,17 +16,17 @@
16
16
  * @see m2m-webhook-delivery.batch.backend.service.ts (consumes these).
17
17
  */
18
18
  /** Maximum number of POST attempts before terminal FAILED. */
19
- export declare const MAX_ATTEMPTS = 5;
19
+ export declare const MAX_ATTEMPTS: number;
20
20
  /** Hard timeout on each outbound POST attempt. */
21
- export declare const REQUEST_TIMEOUT_MS = 30000;
21
+ export declare const REQUEST_TIMEOUT_MS: number;
22
22
  /** Cap on the body slice persisted as `responseBodyTruncated` (D3). */
23
23
  export declare const RESPONSE_BODY_TRUNCATION_BYTES: number;
24
24
  /** User-Agent advertised on every outbound webhook POST. */
25
- export declare const WEBHOOK_USER_AGENT = "Wildo-Webhook/1";
25
+ export declare const WEBHOOK_USER_AGENT: string;
26
26
  /**
27
27
  * Exponential backoff in seconds, capped at MAX_ATTEMPTS-1 retries.
28
- * Index = next attempt count (1 = first retry slot, etc.). Mirrors the
29
- * "1m, 5m, 30m, 2h" cadence sketched in the plan §D6.
28
+ * Index = next attempt count (1 = first retry slot, etc.). The
29
+ * "1m, 5m, 30m, 2h" cadence sketched in the plan §D6 lives on the contract.
30
30
  */
31
31
  export declare const BACKOFF_SCHEDULE_SECONDS: readonly number[];
32
32
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"m2m-webhook-delivery.batch.utils.d.ts","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,8DAA8D;AAC9D,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B,kDAAkD;AAClD,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC,uEAAuE;AACvE,eAAO,MAAM,8BAA8B,QAAW,CAAC;AAEvD,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,oBAAoB,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAK5C,CAAC;AAIX;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,GACb,WAAW,GAAG,OAAO,GAAG,OAAO,CASjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA0BjD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAInE"}
1
+ {"version":3,"file":"m2m-webhook-delivery.batch.utils.d.ts","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAWH,8DAA8D;AAC9D,eAAO,MAAM,YAAY,EAAE,MAAkD,CAAC;AAE9E,kDAAkD;AAClD,eAAO,MAAM,kBAAkB,EAAE,MAAuD,CAAC;AAEzF,uEAAuE;AACvE,eAAO,MAAM,8BAA8B,EAAE,MAAkE,CAAC;AAEhH,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,EAAE,MAAgD,CAAC;AAElF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAAyD,CAAC;AAIhH;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,GACb,WAAW,GAAG,OAAO,GAAG,OAAO,CASjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA0BjD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAInE"}
@@ -15,26 +15,27 @@
15
15
  *
16
16
  * @see m2m-webhook-delivery.batch.backend.service.ts (consumes these).
17
17
  */
18
+ import { M2M_WEBHOOK_DELIVERY_CONTRACT } from '@wildo-ai/saas-models';
18
19
  // ─── Tunables (D6 lock-in) ─────────────────────────────────────
20
+ //
21
+ // Every value below is DERIVED from `M2M_WEBHOOK_DELIVERY_CONTRACT` in
22
+ // `@wildo-ai/saas-models`, which is also what the application's customer
23
+ // documentation projects. Keep the local names: they are the vocabulary the
24
+ // worker, its tests and the delivery-log repository already speak.
19
25
  /** Maximum number of POST attempts before terminal FAILED. */
20
- export const MAX_ATTEMPTS = 5;
26
+ export const MAX_ATTEMPTS = M2M_WEBHOOK_DELIVERY_CONTRACT.maxAttempts;
21
27
  /** Hard timeout on each outbound POST attempt. */
22
- export const REQUEST_TIMEOUT_MS = 30_000;
28
+ export const REQUEST_TIMEOUT_MS = M2M_WEBHOOK_DELIVERY_CONTRACT.requestTimeoutMs;
23
29
  /** Cap on the body slice persisted as `responseBodyTruncated` (D3). */
24
- export const RESPONSE_BODY_TRUNCATION_BYTES = 4 * 1024;
30
+ export const RESPONSE_BODY_TRUNCATION_BYTES = M2M_WEBHOOK_DELIVERY_CONTRACT.responseBodyTruncationBytes;
25
31
  /** User-Agent advertised on every outbound webhook POST. */
26
- export const WEBHOOK_USER_AGENT = 'Wildo-Webhook/1';
32
+ export const WEBHOOK_USER_AGENT = M2M_WEBHOOK_DELIVERY_CONTRACT.userAgent;
27
33
  /**
28
34
  * Exponential backoff in seconds, capped at MAX_ATTEMPTS-1 retries.
29
- * Index = next attempt count (1 = first retry slot, etc.). Mirrors the
30
- * "1m, 5m, 30m, 2h" cadence sketched in the plan §D6.
35
+ * Index = next attempt count (1 = first retry slot, etc.). The
36
+ * "1m, 5m, 30m, 2h" cadence sketched in the plan §D6 lives on the contract.
31
37
  */
32
- export const BACKOFF_SCHEDULE_SECONDS = [
33
- 60,
34
- 300,
35
- 1800,
36
- 7200,
37
- ];
38
+ export const BACKOFF_SCHEDULE_SECONDS = M2M_WEBHOOK_DELIVERY_CONTRACT.backoffScheduleSeconds;
38
39
  // ─── Pure decision functions ───────────────────────────────────
39
40
  /**
40
41
  * 4xx codes considered transient. 408 = request timeout; 429 = rate
@@ -43,7 +44,7 @@ export const BACKOFF_SCHEDULE_SECONDS = [
43
44
  * the exact same payload), so a 4xx will repeat forever.
44
45
  */
45
46
  export function isRetryable4xx(status) {
46
- return status === 408 || status === 429;
47
+ return M2M_WEBHOOK_DELIVERY_CONTRACT.retryableHttpStatuses.includes(status);
47
48
  }
48
49
  /**
49
50
  * Classify a response status into a delivery verdict.
@@ -1 +1 @@
1
- {"version":3,"file":"m2m-webhook-delivery.batch.utils.js","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure helpers for the M2M webhook delivery batch worker.\n *\n * Why a separate module rather than exporting from the batch service?\n * The batch service file imports the framework's batches/services\n * machinery (via `createCustomBatchExecutionService`,\n * `engine-custom-batch-manifests`, the JWT service, etc.), which\n * transitively pulls in the full DI graph. That's fine in production —\n * the worker IS the consumer of those modules — but it makes the unit\n * tests on the pure decision logic (status classification, body\n * truncation, backoff scheduling) impossible to load in isolation\n * (Vite chokes on the connector packages and full DI). Splitting the\n * helpers into their own zero-dep file keeps the test suite cheap and\n * surface-targeted.\n *\n * @see m2m-webhook-delivery.batch.backend.service.ts (consumes these).\n */\n\n// ─── Tunables (D6 lock-in) ─────────────────────────────────────\n\n/** Maximum number of POST attempts before terminal FAILED. */\nexport const MAX_ATTEMPTS = 5;\n\n/** Hard timeout on each outbound POST attempt. */\nexport const REQUEST_TIMEOUT_MS = 30_000;\n\n/** Cap on the body slice persisted as `responseBodyTruncated` (D3). */\nexport const RESPONSE_BODY_TRUNCATION_BYTES = 4 * 1024;\n\n/** User-Agent advertised on every outbound webhook POST. */\nexport const WEBHOOK_USER_AGENT = 'Wildo-Webhook/1';\n\n/**\n * Exponential backoff in seconds, capped at MAX_ATTEMPTS-1 retries.\n * Index = next attempt count (1 = first retry slot, etc.). Mirrors the\n * \"1m, 5m, 30m, 2h\" cadence sketched in the plan §D6.\n */\nexport const BACKOFF_SCHEDULE_SECONDS: readonly number[] = [\n 60,\n 300,\n 1800,\n 7200,\n] as const;\n\n// ─── Pure decision functions ───────────────────────────────────\n\n/**\n * 4xx codes considered transient. 408 = request timeout; 429 = rate\n * limit. Everything else in 4xx is consumer-facing \"you sent something\n * wrong\" and should not be retried — the body is fixed (we always send\n * the exact same payload), so a 4xx will repeat forever.\n */\nexport function isRetryable4xx(status: number): boolean {\n return status === 408 || status === 429;\n}\n\n/**\n * Classify a response status into a delivery verdict.\n *\n * - 1xx: informational, never the final response — `fetch()` already\n * waits past these, so observing a 1xx here means a misbehaving\n * intermediate. Treat as `retry` (redirect-style oddity, harmless to\n * re-POST since the body is fixed).\n * - 2xx: terminal success (`delivered`).\n * - 3xx: webhooks must NOT redirect. A 3xx is a configuration error\n * on the consumer side (wrong URL → CDN/proxy redirect) and will\n * loop forever if retried. Treat as `fatal` so the row terminates\n * with a clear failure reason and the operator can fix the URL.\n * - 408 / 429: transient (`retry`).\n * - other 4xx: caller-error — body is fixed, retry will repeat\n * forever (`fatal`).\n * - 5xx: server-side transient (`retry`).\n * - anything else (very rare, but e.g. odd proxy codes): `retry` to\n * err on the side of redelivery rather than silent data loss.\n */\nexport function classifyResponseStatus(\n status: number,\n): 'delivered' | 'retry' | 'fatal' {\n if (status >= 100 && status < 200) return 'retry';\n if (status >= 200 && status < 300) return 'delivered';\n if (status >= 300 && status < 400) return 'fatal';\n if (status >= 400 && status < 500) {\n return isRetryable4xx(status) ? 'retry' : 'fatal';\n }\n // 5xx and anything else.\n return 'retry';\n}\n\n/**\n * Byte-precise truncation: caps the persisted slice at\n * `RESPONSE_BODY_TRUNCATION_BYTES` measured in **UTF-8 bytes**, not\n * JavaScript `.length` (which counts UTF-16 code units). A 4 KiB cap\n * applied to `.length` lets a payload of 4096 emoji past the cap (each\n * counts as 1 UTF-16 unit but encodes to 4 bytes), and conversely\n * truncates dense multi-byte text earlier than the budget. Truncates\n * on a UTF-8 boundary so the persisted slice is always valid UTF-8.\n */\nexport function truncateBody(body: string): string {\n // Fast path: ASCII-only bodies bypass the encoder.\n if (body.length <= RESPONSE_BODY_TRUNCATION_BYTES && /^[\\x00-\\x7F]*$/.test(body)) {\n return body;\n }\n const encoder = new TextEncoder();\n const bytes = encoder.encode(body);\n if (bytes.length <= RESPONSE_BODY_TRUNCATION_BYTES) return body;\n\n // Walk back from the byte cap to the start of the most recent UTF-8\n // code point. UTF-8 continuation bytes have the high bits `10xxxxxx`\n // — any byte at offset N where (bytes[N] & 0xC0) === 0x80 is a\n // continuation byte, meaning N is mid-sequence. The first byte of a\n // multi-byte sequence is `0xxxxxxx` (1-byte / ASCII), `110xxxxx`\n // (2-byte), `1110xxxx` (3-byte), or `11110xxx` (4-byte).\n //\n // Slicing at a continuation boundary leaves stray bytes that\n // `TextDecoder` (non-fatal) replaces with U+FFFD — and U+FFFD is\n // 3 bytes UTF-8, so the replacement can blow past the byte cap.\n // Cutting cleanly at a code-point boundary guarantees the result's\n // UTF-8 length is `<=` the cap.\n let cut = RESPONSE_BODY_TRUNCATION_BYTES;\n while (cut > 0 && (bytes[cut] & 0xc0) === 0x80) {\n cut--;\n }\n return new TextDecoder('utf-8', { fatal: false }).decode(bytes.subarray(0, cut));\n}\n\n/**\n * Compute the next attempt timestamp for a transient failure.\n *\n * `nextAttemptCount` is the post-increment value (1 = scheduling the\n * second attempt after the first failure). Indexes past the schedule\n * length clamp to the last entry so we never return `undefined` /\n * `NaN` delays for an unexpected attempt count.\n */\nexport function computeNextAttemptAt(nextAttemptCount: number): Date {\n const idx = Math.min(nextAttemptCount - 1, BACKOFF_SCHEDULE_SECONDS.length - 1);\n const delaySeconds = BACKOFF_SCHEDULE_SECONDS[Math.max(0, idx)];\n return new Date(Date.now() + delaySeconds * 1000);\n}\n"]}
1
+ {"version":3,"file":"m2m-webhook-delivery.batch.utils.js","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/batches/m2m-webhook-delivery.batch.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure helpers for the M2M webhook delivery batch worker.\n *\n * Why a separate module rather than exporting from the batch service?\n * The batch service file imports the framework's batches/services\n * machinery (via `createCustomBatchExecutionService`,\n * `engine-custom-batch-manifests`, the JWT service, etc.), which\n * transitively pulls in the full DI graph. That's fine in production —\n * the worker IS the consumer of those modules — but it makes the unit\n * tests on the pure decision logic (status classification, body\n * truncation, backoff scheduling) impossible to load in isolation\n * (Vite chokes on the connector packages and full DI). Splitting the\n * helpers into their own zero-dep file keeps the test suite cheap and\n * surface-targeted.\n *\n * @see m2m-webhook-delivery.batch.backend.service.ts (consumes these).\n */\n\nimport { M2M_WEBHOOK_DELIVERY_CONTRACT } from '@wildo-ai/saas-models';\n\n// ─── Tunables (D6 lock-in) ─────────────────────────────────────\n//\n// Every value below is DERIVED from `M2M_WEBHOOK_DELIVERY_CONTRACT` in\n// `@wildo-ai/saas-models`, which is also what the application's customer\n// documentation projects. Keep the local names: they are the vocabulary the\n// worker, its tests and the delivery-log repository already speak.\n\n/** Maximum number of POST attempts before terminal FAILED. */\nexport const MAX_ATTEMPTS: number = M2M_WEBHOOK_DELIVERY_CONTRACT.maxAttempts;\n\n/** Hard timeout on each outbound POST attempt. */\nexport const REQUEST_TIMEOUT_MS: number = M2M_WEBHOOK_DELIVERY_CONTRACT.requestTimeoutMs;\n\n/** Cap on the body slice persisted as `responseBodyTruncated` (D3). */\nexport const RESPONSE_BODY_TRUNCATION_BYTES: number = M2M_WEBHOOK_DELIVERY_CONTRACT.responseBodyTruncationBytes;\n\n/** User-Agent advertised on every outbound webhook POST. */\nexport const WEBHOOK_USER_AGENT: string = M2M_WEBHOOK_DELIVERY_CONTRACT.userAgent;\n\n/**\n * Exponential backoff in seconds, capped at MAX_ATTEMPTS-1 retries.\n * Index = next attempt count (1 = first retry slot, etc.). The\n * \"1m, 5m, 30m, 2h\" cadence sketched in the plan §D6 lives on the contract.\n */\nexport const BACKOFF_SCHEDULE_SECONDS: readonly number[] = M2M_WEBHOOK_DELIVERY_CONTRACT.backoffScheduleSeconds;\n\n// ─── Pure decision functions ───────────────────────────────────\n\n/**\n * 4xx codes considered transient. 408 = request timeout; 429 = rate\n * limit. Everything else in 4xx is consumer-facing \"you sent something\n * wrong\" and should not be retried — the body is fixed (we always send\n * the exact same payload), so a 4xx will repeat forever.\n */\nexport function isRetryable4xx(status: number): boolean {\n return (M2M_WEBHOOK_DELIVERY_CONTRACT.retryableHttpStatuses as readonly number[]).includes(status);\n}\n\n/**\n * Classify a response status into a delivery verdict.\n *\n * - 1xx: informational, never the final response — `fetch()` already\n * waits past these, so observing a 1xx here means a misbehaving\n * intermediate. Treat as `retry` (redirect-style oddity, harmless to\n * re-POST since the body is fixed).\n * - 2xx: terminal success (`delivered`).\n * - 3xx: webhooks must NOT redirect. A 3xx is a configuration error\n * on the consumer side (wrong URL → CDN/proxy redirect) and will\n * loop forever if retried. Treat as `fatal` so the row terminates\n * with a clear failure reason and the operator can fix the URL.\n * - 408 / 429: transient (`retry`).\n * - other 4xx: caller-error — body is fixed, retry will repeat\n * forever (`fatal`).\n * - 5xx: server-side transient (`retry`).\n * - anything else (very rare, but e.g. odd proxy codes): `retry` to\n * err on the side of redelivery rather than silent data loss.\n */\nexport function classifyResponseStatus(\n status: number,\n): 'delivered' | 'retry' | 'fatal' {\n if (status >= 100 && status < 200) return 'retry';\n if (status >= 200 && status < 300) return 'delivered';\n if (status >= 300 && status < 400) return 'fatal';\n if (status >= 400 && status < 500) {\n return isRetryable4xx(status) ? 'retry' : 'fatal';\n }\n // 5xx and anything else.\n return 'retry';\n}\n\n/**\n * Byte-precise truncation: caps the persisted slice at\n * `RESPONSE_BODY_TRUNCATION_BYTES` measured in **UTF-8 bytes**, not\n * JavaScript `.length` (which counts UTF-16 code units). A 4 KiB cap\n * applied to `.length` lets a payload of 4096 emoji past the cap (each\n * counts as 1 UTF-16 unit but encodes to 4 bytes), and conversely\n * truncates dense multi-byte text earlier than the budget. Truncates\n * on a UTF-8 boundary so the persisted slice is always valid UTF-8.\n */\nexport function truncateBody(body: string): string {\n // Fast path: ASCII-only bodies bypass the encoder.\n if (body.length <= RESPONSE_BODY_TRUNCATION_BYTES && /^[\\x00-\\x7F]*$/.test(body)) {\n return body;\n }\n const encoder = new TextEncoder();\n const bytes = encoder.encode(body);\n if (bytes.length <= RESPONSE_BODY_TRUNCATION_BYTES) return body;\n\n // Walk back from the byte cap to the start of the most recent UTF-8\n // code point. UTF-8 continuation bytes have the high bits `10xxxxxx`\n // — any byte at offset N where (bytes[N] & 0xC0) === 0x80 is a\n // continuation byte, meaning N is mid-sequence. The first byte of a\n // multi-byte sequence is `0xxxxxxx` (1-byte / ASCII), `110xxxxx`\n // (2-byte), `1110xxxx` (3-byte), or `11110xxx` (4-byte).\n //\n // Slicing at a continuation boundary leaves stray bytes that\n // `TextDecoder` (non-fatal) replaces with U+FFFD — and U+FFFD is\n // 3 bytes UTF-8, so the replacement can blow past the byte cap.\n // Cutting cleanly at a code-point boundary guarantees the result's\n // UTF-8 length is `<=` the cap.\n let cut = RESPONSE_BODY_TRUNCATION_BYTES;\n while (cut > 0 && (bytes[cut] & 0xc0) === 0x80) {\n cut--;\n }\n return new TextDecoder('utf-8', { fatal: false }).decode(bytes.subarray(0, cut));\n}\n\n/**\n * Compute the next attempt timestamp for a transient failure.\n *\n * `nextAttemptCount` is the post-increment value (1 = scheduling the\n * second attempt after the first failure). Indexes past the schedule\n * length clamp to the last entry so we never return `undefined` /\n * `NaN` delays for an unexpected attempt count.\n */\nexport function computeNextAttemptAt(nextAttemptCount: number): Date {\n const idx = Math.min(nextAttemptCount - 1, BACKOFF_SCHEDULE_SECONDS.length - 1);\n const delaySeconds = BACKOFF_SCHEDULE_SECONDS[Math.max(0, idx)];\n return new Date(Date.now() + delaySeconds * 1000);\n}\n"]}
@@ -23,8 +23,9 @@
23
23
  * @see batches/m2m-webhook-delivery.batch.backend.service.ts — real deliveries.
24
24
  * @see services/webhook-config-test-endpoint.custom-impl.backend.service.ts — synthetic probe.
25
25
  */
26
+ import { M2M_WEBHOOK_DELIVERY_CONTRACT } from '@wildo-ai/saas-models';
26
27
  /** Consumer-facing digest algorithm carried in every outbound webhook JWT. */
27
- export declare const M2M_WEBHOOK_BODY_HASH_ALGORITHM: "sha256";
28
+ export declare const M2M_WEBHOOK_BODY_HASH_ALGORITHM: typeof M2M_WEBHOOK_DELIVERY_CONTRACT.bodyHashAlgorithm;
28
29
  /**
29
30
  * Outcome of one attempt at the wire level.
30
31
  *
@@ -1 +1 @@
1
- {"version":3,"file":"m2m-webhook-signed-request.backend.utils.d.ts","sourceRoot":"","sources":["../../../../../../src/notifications/webhooks/outbound/m2m-webhook-signed-request.backend.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAoEH,8EAA8E;AAC9E,eAAO,MAAM,+BAA+B,EAAG,QAAiB,CAAC;AAEjE;;;;;;;;GAQG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAuCF;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAsEtC"}
1
+ {"version":3,"file":"m2m-webhook-signed-request.backend.utils.d.ts","sourceRoot":"","sources":["../../../../../../src/notifications/webhooks/outbound/m2m-webhook-signed-request.backend.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,6BAA6B,EAAmB,MAAM,uBAAuB,CAAC;AAkEvF,8EAA8E;AAC9E,eAAO,MAAM,+BAA+B,EAAE,OAAO,6BAA6B,CAAC,iBAAmE,CAAC;AAEvJ;;;;;;;;GAQG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAuCF;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAsEtC"}
@@ -23,7 +23,7 @@
23
23
  * @see batches/m2m-webhook-delivery.batch.backend.service.ts — real deliveries.
24
24
  * @see services/webhook-config-test-endpoint.custom-impl.backend.service.ts — synthetic probe.
25
25
  */
26
- import { WildoHeaderKeys } from '@wildo-ai/saas-models';
26
+ import { M2M_WEBHOOK_DELIVERY_CONTRACT, WildoHeaderKeys } from '@wildo-ai/saas-models';
27
27
  import { OutboundTargetRejection, classifyOutboundTargetUrl, } from '../../../security/ssrf/outbound-target-guard.backend.utils.js';
28
28
  import { REQUEST_TIMEOUT_MS, WEBHOOK_USER_AGENT, truncateBody, } from './batches/m2m-webhook-delivery.batch.utils.js';
29
29
  /**
@@ -80,7 +80,7 @@ function refuseUnsafeWebhookTarget(url) {
80
80
  }
81
81
  }
82
82
  /** Consumer-facing digest algorithm carried in every outbound webhook JWT. */
83
- export const M2M_WEBHOOK_BODY_HASH_ALGORITHM = 'sha256';
83
+ export const M2M_WEBHOOK_BODY_HASH_ALGORITHM = M2M_WEBHOOK_DELIVERY_CONTRACT.bodyHashAlgorithm;
84
84
  /**
85
85
  * Render a `fetch` rejection into something an operator can act on.
86
86
  *
@@ -1 +1 @@
1
- {"version":3,"file":"m2m-webhook-signed-request.backend.utils.js","sourceRoot":"","sources":["../../../../../../src/notifications/webhooks/outbound/m2m-webhook-signed-request.backend.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The single outbound-transport primitive for M2M webhooks.\n *\n * Every signed request Wildo emits to a customer endpoint goes through\n * {@link sendSignedWebhookRequest} — the retry worker's real deliveries AND the\n * `webhookConfig.testEndpoint` synthetic probe. Two call sites, one implementation, on\n * purpose: a probe that reached the endpoint through a *different* code path would\n * validate that path rather than the delivery path, which is precisely the reassurance an\n * admin is asking for when they press \"test\". Timeout, User-Agent, signature header name,\n * and body-truncation behaviour are therefore not re-decided per caller.\n *\n * Scope boundary — this module owns the WIRE, nothing else. It does not sign (callers hold\n * their own claims), does not classify (see `classifyResponseStatus`), does not persist,\n * and never throws on a transport failure: an unreachable endpoint is data the caller must\n * report, not an exception. The only way to get an exception out of here is to pass\n * something that is not a valid request.\n *\n * The tunables come from `./batches/m2m-webhook-delivery.batch.utils`, which is\n * deliberately import-free so its pure-decision tests load without the DI graph. They are\n * left there rather than moved up here precisely to preserve that property — this module\n * performs I/O and would drag `@wildo-ai/saas-models` into those tests.\n *\n * @see batches/m2m-webhook-delivery.batch.backend.service.ts — real deliveries.\n * @see services/webhook-config-test-endpoint.custom-impl.backend.service.ts — synthetic probe.\n */\n\nimport { WildoHeaderKeys } from '@wildo-ai/saas-models';\nimport {\n OutboundTargetRejection,\n classifyOutboundTargetUrl,\n} from '~wildo-backend-lib/security/ssrf/outbound-target-guard.backend.utils';\nimport {\n REQUEST_TIMEOUT_MS,\n WEBHOOK_USER_AGENT,\n truncateBody,\n} from './batches/m2m-webhook-delivery.batch.utils';\n\n/**\n * Schemes an outbound webhook delivery may use.\n *\n * `http:` is admitted, and that is a DEFERRED decision rather than an endorsement. The A2A sibling\n * requires HTTPS unconditionally, and the same argument applies here — the signature JWT in transit\n * is a credential. But `WebhookEndpointSchema.url` is a bare `z.url()`, so plaintext endpoints are\n * authorable today and may already be authored; refusing them here would silently stop deliveries for\n * tenants whose configuration was accepted by the framework. Tightening this is a product decision\n * with a migration, not a hardening pass.\n */\nconst M2M_WEBHOOK_ADMITTED_PROTOCOLS = ['https:', 'http:'] as const;\n\n/**\n * Refuse a destination that would turn this server into a request-forgery tool.\n *\n * ## Why the guard is HERE and not on the schema\n *\n * `WebhookEndpointSchema.url` validates shape (`z.url()`) and nothing else, so an admin could author\n * `http://169.254.169.254/latest/meta-data/` and the engine would POST signed payloads to the cloud\n * instance-metadata service — then capture the answer into the delivery log as\n * `responseBodyTruncated`. That makes it a READ primitive, not a blind SSRF, and\n * `webhookConfig.testEndpoint` is a low-friction oracle for it.\n *\n * A schema refine would leave two holes this placement does not have: it would not cover endpoints\n * ALREADY STORED before the refine existed, and it would not cover the A2A push-delivery worker,\n * which reaches this same primitive with a URL its own (separate) guard validated at configuration\n * time. Putting the check at the wire makes it unbypassable for every caller — present and future —\n * which is what `.claude/rules/invariant-call-site-enumeration.md` means by preferring one\n * chokepoint over N hand-applied predicates.\n *\n * ## What it cannot do\n *\n * An ordinary hostname is not resolved, so `hook.attacker.test` with an A record of `169.254.169.254`\n * still passes. That residual is inherited from the shared classifier and is stated there; closing it\n * needs either a resolving check (a check-then-connect, still open to rebinding) or egress-path\n * enforcement. It is a deliberate open item, not an oversight.\n */\nfunction refuseUnsafeWebhookTarget(url: string): string | undefined {\n const rejection = classifyOutboundTargetUrl(url, { admittedProtocols: M2M_WEBHOOK_ADMITTED_PROTOCOLS });\n switch (rejection) {\n case undefined:\n return undefined;\n case OutboundTargetRejection.NOT_ABSOLUTE_ADMITTED_SCHEME:\n return 'refused: endpoint is not an absolute http(s) URL';\n case OutboundTargetRejection.CREDENTIALS_IN_URL:\n // Credentials in the URL would be replayed on every delivery and would land in any log or\n // delivery row that records the target.\n return 'refused: endpoint URL carries embedded credentials';\n case OutboundTargetRejection.PRIVATE_OR_RESERVED_ADDRESS:\n return 'refused: endpoint resolves to a private, loopback, link-local or otherwise reserved address';\n case OutboundTargetRejection.RESERVED_SPECIAL_USE_HOSTNAME:\n return 'refused: endpoint uses a reserved special-use hostname';\n }\n}\n\n/** Consumer-facing digest algorithm carried in every outbound webhook JWT. */\nexport const M2M_WEBHOOK_BODY_HASH_ALGORITHM = 'sha256' as const;\n\n/**\n * Outcome of one attempt at the wire level.\n *\n * `responseStatus` and `networkError` are mutually exclusive and exhaustive: either the\n * endpoint produced an HTTP response or it did not. Callers classify from there — this\n * type deliberately carries no verdict of its own, because \"429 is retryable\" is a\n * delivery-policy question and \"429 means the endpoint is reachable\" is a probe question,\n * and the same status answers both differently.\n */\nexport type SignedWebhookRequestResult = {\n /** Set when the endpoint answered, whatever the status. */\n responseStatus?: number;\n /** Best-effort capture of the consumer's response body, truncated on a UTF-8 boundary. */\n responseBodyTruncated?: string;\n /** Set when no HTTP response was obtained (DNS/TLS/connection failure, or timeout). */\n networkError?: string;\n /** Wall-clock duration of this attempt alone, excluding any caller-side work. */\n durationMs: number;\n};\n\n/**\n * Render a `fetch` rejection into something an operator can act on.\n *\n * Node's `fetch` (undici) rejects with a flat `TypeError: fetch failed` and buries the actual\n * cause — `ECONNREFUSED`, `ENOTFOUND`, `EHOSTUNREACH`, a TLS failure — one or more levels down in\n * `error.cause`. Reporting only `error.message` tells an admin their endpoint failed without\n * telling them whether the hostname is wrong, the port is closed, or the certificate is bad; for a\n * diagnostic whose entire job is to explain a failure, that is barely better than the silent\n * success this operation used to return. So walk the cause chain and append what it names.\n *\n * `AggregateError` (emitted when every resolved address fails, e.g. happy-eyeballs against a dead\n * host) carries its detail in `errors[]` rather than `cause`, so that shape is unwrapped too.\n * Depth-capped and de-duplicated: the chain is attacker-adjacent input in the sense that a hostile\n * endpoint controls how deep it goes, and a repeated message adds nothing.\n */\nfunction describeFetchFailure(error: unknown): string {\n if (!(error instanceof Error)) return String(error);\n\n const parts: string[] = [];\n const seen = new Set<string>();\n let current: unknown = error;\n\n for (let depth = 0; depth < 4 && current instanceof Error; depth += 1) {\n // `code` is where Node puts the actionable token (ECONNREFUSED, …); the message often repeats it.\n const code = (current as NodeJS.ErrnoException).code;\n const label = code && !current.message.includes(code) ? `${current.message} (${code})` : current.message;\n if (label && !seen.has(label)) {\n seen.add(label);\n parts.push(label);\n }\n const aggregated = current instanceof AggregateError ? current.errors?.[0] : undefined;\n current = aggregated ?? (current as { cause?: unknown }).cause;\n }\n\n return parts.join(': ') || 'fetch failed';\n}\n\n/**\n * POST a pre-signed body to a webhook endpoint under a hard timeout.\n *\n * Never throws for a transport failure — a refused connection, a DNS miss and a timeout\n * all come back as `networkError`. A timeout is reported with its budget spelled out\n * because \"timed out\" alone sends an operator hunting for the wrong thing.\n *\n * Body capture is best-effort: a consumer that returns an unreadable body still produced a\n * valid status, and losing the body must not turn a successful attempt into a failed one.\n */\nexport async function sendSignedWebhookRequest(params: {\n /** Absolute destination URL. */\n url: string;\n /** Exact bytes to send — already the string whose hash the caller signed. */\n body: string;\n /**\n * Value for the `X-Webhook-Signature` header. Omit when the caller authenticates by another\n * mechanism and supplies its own {@link headers} — see below.\n */\n signature?: string;\n /**\n * Caller-supplied headers, MERGED OVER the defaults.\n *\n * Added for the A2A push-notification worker, which authenticates with the CLIENT's declared\n * scheme (`Authorization: {scheme} {credentials}`) and sends `application/a2a+json` rather than\n * Wildo's own ES256 signature and `application/json`.\n *\n * Widening this primitive instead of writing a second POST is deliberate, and follows the module\n * header's own claim to own THE WIRE: timeout, abort handling, failure description and body\n * truncation are the parts that are easy to get subtly wrong and expensive to have two of. What\n * varies between callers is authentication and media type — which are exactly the two things a\n * caller is in a position to decide. Purely additive: `signature` stays optional-by-absence for\n * the two existing M2M call sites, whose behaviour is byte-for-byte unchanged.\n */\n headers?: Record<string, string>;\n}): Promise<SignedWebhookRequestResult> {\n const startedAt = Date.now();\n\n // Refused BEFORE any timer or connection: a target the guard rejects must produce no DNS lookup,\n // no socket, and no signed body on the wire. Reported as `networkError` rather than thrown, because\n // this module's contract is that a destination problem is DATA the caller reports — the probe\n // surfaces it to the admin as UNREACHABLE and the delivery worker records it on the attempt row.\n const refusal = refuseUnsafeWebhookTarget(params.url);\n if (refusal) {\n return { networkError: refusal, durationMs: Date.now() - startedAt };\n }\n\n const abort = new AbortController();\n const timer = setTimeout(() => abort.abort(), REQUEST_TIMEOUT_MS);\n\n let responseStatus: number | undefined;\n let responseBodyTruncated: string | undefined;\n let networkError: string | undefined;\n\n try {\n const response = await fetch(params.url, {\n method: 'POST',\n signal: abort.signal,\n /**\n * A redirect is REPORTED, never followed.\n *\n * Following one re-points the request at a target the guard above never saw, which is the\n * guard's most obvious bypass — an admin authors a public URL that 302s to\n * `http://169.254.169.254/`. It also silently defeats a decision this subsystem had ALREADY\n * taken and could not enforce: `classifyResponseStatus` classifies 3xx as `fatal` and\n * `resolveProbeOutcome` classifies it as REJECTED (\"webhooks must not redirect, and a\n * CDN/proxy redirect on a wrong URL is one of the most common real misconfigurations an admin\n * uses this probe to find\") — but with `fetch`'s default follow behaviour neither of those\n * branches could ever execute, because a 3xx never reached them.\n *\n * `'manual'` rather than `'error'` on purpose: `'error'` would surface as an opaque network\n * failure, whereas `'manual'` hands back the 3xx status so both classifiers see the redirect\n * and tell the admin exactly what their endpoint did.\n */\n redirect: 'manual',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': WEBHOOK_USER_AGENT,\n ...(params.signature ? { [WildoHeaderKeys.X_WEBHOOK_SIGNATURE]: params.signature } : {}),\n ...params.headers,\n },\n body: params.body,\n });\n responseStatus = response.status;\n try {\n const text = await response.text();\n responseBodyTruncated = text ? truncateBody(text) : undefined;\n } catch {\n // Best-effort body capture — never fail the attempt on a non-readable body.\n responseBodyTruncated = undefined;\n }\n } catch (error) {\n networkError = abort.signal.aborted\n ? `request timed out after ${REQUEST_TIMEOUT_MS}ms`\n : describeFetchFailure(error);\n } finally {\n clearTimeout(timer);\n }\n\n return {\n responseStatus,\n responseBodyTruncated,\n networkError,\n durationMs: Date.now() - startedAt,\n };\n}\n"]}
1
+ {"version":3,"file":"m2m-webhook-signed-request.backend.utils.js","sourceRoot":"","sources":["../../../../../../src/notifications/webhooks/outbound/m2m-webhook-signed-request.backend.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The single outbound-transport primitive for M2M webhooks.\n *\n * Every signed request Wildo emits to a customer endpoint goes through\n * {@link sendSignedWebhookRequest} — the retry worker's real deliveries AND the\n * `webhookConfig.testEndpoint` synthetic probe. Two call sites, one implementation, on\n * purpose: a probe that reached the endpoint through a *different* code path would\n * validate that path rather than the delivery path, which is precisely the reassurance an\n * admin is asking for when they press \"test\". Timeout, User-Agent, signature header name,\n * and body-truncation behaviour are therefore not re-decided per caller.\n *\n * Scope boundary — this module owns the WIRE, nothing else. It does not sign (callers hold\n * their own claims), does not classify (see `classifyResponseStatus`), does not persist,\n * and never throws on a transport failure: an unreachable endpoint is data the caller must\n * report, not an exception. The only way to get an exception out of here is to pass\n * something that is not a valid request.\n *\n * The tunables come from `./batches/m2m-webhook-delivery.batch.utils`, which is\n * deliberately import-free so its pure-decision tests load without the DI graph. They are\n * left there rather than moved up here precisely to preserve that property — this module\n * performs I/O and would drag `@wildo-ai/saas-models` into those tests.\n *\n * @see batches/m2m-webhook-delivery.batch.backend.service.ts — real deliveries.\n * @see services/webhook-config-test-endpoint.custom-impl.backend.service.ts — synthetic probe.\n */\n\nimport { M2M_WEBHOOK_DELIVERY_CONTRACT, WildoHeaderKeys } from '@wildo-ai/saas-models';\nimport {\n OutboundTargetRejection,\n classifyOutboundTargetUrl,\n} from '~wildo-backend-lib/security/ssrf/outbound-target-guard.backend.utils';\nimport {\n REQUEST_TIMEOUT_MS,\n WEBHOOK_USER_AGENT,\n truncateBody,\n} from './batches/m2m-webhook-delivery.batch.utils';\n\n/**\n * Schemes an outbound webhook delivery may use.\n *\n * `http:` is admitted, and that is a DEFERRED decision rather than an endorsement. The A2A sibling\n * requires HTTPS unconditionally, and the same argument applies here — the signature JWT in transit\n * is a credential. But `WebhookEndpointSchema.url` is a bare `z.url()`, so plaintext endpoints are\n * authorable today and may already be authored; refusing them here would silently stop deliveries for\n * tenants whose configuration was accepted by the framework. Tightening this is a product decision\n * with a migration, not a hardening pass.\n */\nconst M2M_WEBHOOK_ADMITTED_PROTOCOLS = ['https:', 'http:'] as const;\n\n/**\n * Refuse a destination that would turn this server into a request-forgery tool.\n *\n * ## Why the guard is HERE and not on the schema\n *\n * `WebhookEndpointSchema.url` validates shape (`z.url()`) and nothing else, so an admin could author\n * `http://169.254.169.254/latest/meta-data/` and the engine would POST signed payloads to the cloud\n * instance-metadata service — then capture the answer into the delivery log as\n * `responseBodyTruncated`. That makes it a READ primitive, not a blind SSRF, and\n * `webhookConfig.testEndpoint` is a low-friction oracle for it.\n *\n * A schema refine would leave two holes this placement does not have: it would not cover endpoints\n * ALREADY STORED before the refine existed, and it would not cover the A2A push-delivery worker,\n * which reaches this same primitive with a URL its own (separate) guard validated at configuration\n * time. Putting the check at the wire makes it unbypassable for every caller — present and future —\n * which is what `.claude/rules/invariant-call-site-enumeration.md` means by preferring one\n * chokepoint over N hand-applied predicates.\n *\n * ## What it cannot do\n *\n * An ordinary hostname is not resolved, so `hook.attacker.test` with an A record of `169.254.169.254`\n * still passes. That residual is inherited from the shared classifier and is stated there; closing it\n * needs either a resolving check (a check-then-connect, still open to rebinding) or egress-path\n * enforcement. It is a deliberate open item, not an oversight.\n */\nfunction refuseUnsafeWebhookTarget(url: string): string | undefined {\n const rejection = classifyOutboundTargetUrl(url, { admittedProtocols: M2M_WEBHOOK_ADMITTED_PROTOCOLS });\n switch (rejection) {\n case undefined:\n return undefined;\n case OutboundTargetRejection.NOT_ABSOLUTE_ADMITTED_SCHEME:\n return 'refused: endpoint is not an absolute http(s) URL';\n case OutboundTargetRejection.CREDENTIALS_IN_URL:\n // Credentials in the URL would be replayed on every delivery and would land in any log or\n // delivery row that records the target.\n return 'refused: endpoint URL carries embedded credentials';\n case OutboundTargetRejection.PRIVATE_OR_RESERVED_ADDRESS:\n return 'refused: endpoint resolves to a private, loopback, link-local or otherwise reserved address';\n case OutboundTargetRejection.RESERVED_SPECIAL_USE_HOSTNAME:\n return 'refused: endpoint uses a reserved special-use hostname';\n }\n}\n\n/** Consumer-facing digest algorithm carried in every outbound webhook JWT. */\nexport const M2M_WEBHOOK_BODY_HASH_ALGORITHM: typeof M2M_WEBHOOK_DELIVERY_CONTRACT.bodyHashAlgorithm = M2M_WEBHOOK_DELIVERY_CONTRACT.bodyHashAlgorithm;\n\n/**\n * Outcome of one attempt at the wire level.\n *\n * `responseStatus` and `networkError` are mutually exclusive and exhaustive: either the\n * endpoint produced an HTTP response or it did not. Callers classify from there — this\n * type deliberately carries no verdict of its own, because \"429 is retryable\" is a\n * delivery-policy question and \"429 means the endpoint is reachable\" is a probe question,\n * and the same status answers both differently.\n */\nexport type SignedWebhookRequestResult = {\n /** Set when the endpoint answered, whatever the status. */\n responseStatus?: number;\n /** Best-effort capture of the consumer's response body, truncated on a UTF-8 boundary. */\n responseBodyTruncated?: string;\n /** Set when no HTTP response was obtained (DNS/TLS/connection failure, or timeout). */\n networkError?: string;\n /** Wall-clock duration of this attempt alone, excluding any caller-side work. */\n durationMs: number;\n};\n\n/**\n * Render a `fetch` rejection into something an operator can act on.\n *\n * Node's `fetch` (undici) rejects with a flat `TypeError: fetch failed` and buries the actual\n * cause — `ECONNREFUSED`, `ENOTFOUND`, `EHOSTUNREACH`, a TLS failure — one or more levels down in\n * `error.cause`. Reporting only `error.message` tells an admin their endpoint failed without\n * telling them whether the hostname is wrong, the port is closed, or the certificate is bad; for a\n * diagnostic whose entire job is to explain a failure, that is barely better than the silent\n * success this operation used to return. So walk the cause chain and append what it names.\n *\n * `AggregateError` (emitted when every resolved address fails, e.g. happy-eyeballs against a dead\n * host) carries its detail in `errors[]` rather than `cause`, so that shape is unwrapped too.\n * Depth-capped and de-duplicated: the chain is attacker-adjacent input in the sense that a hostile\n * endpoint controls how deep it goes, and a repeated message adds nothing.\n */\nfunction describeFetchFailure(error: unknown): string {\n if (!(error instanceof Error)) return String(error);\n\n const parts: string[] = [];\n const seen = new Set<string>();\n let current: unknown = error;\n\n for (let depth = 0; depth < 4 && current instanceof Error; depth += 1) {\n // `code` is where Node puts the actionable token (ECONNREFUSED, …); the message often repeats it.\n const code = (current as NodeJS.ErrnoException).code;\n const label = code && !current.message.includes(code) ? `${current.message} (${code})` : current.message;\n if (label && !seen.has(label)) {\n seen.add(label);\n parts.push(label);\n }\n const aggregated = current instanceof AggregateError ? current.errors?.[0] : undefined;\n current = aggregated ?? (current as { cause?: unknown }).cause;\n }\n\n return parts.join(': ') || 'fetch failed';\n}\n\n/**\n * POST a pre-signed body to a webhook endpoint under a hard timeout.\n *\n * Never throws for a transport failure — a refused connection, a DNS miss and a timeout\n * all come back as `networkError`. A timeout is reported with its budget spelled out\n * because \"timed out\" alone sends an operator hunting for the wrong thing.\n *\n * Body capture is best-effort: a consumer that returns an unreadable body still produced a\n * valid status, and losing the body must not turn a successful attempt into a failed one.\n */\nexport async function sendSignedWebhookRequest(params: {\n /** Absolute destination URL. */\n url: string;\n /** Exact bytes to send — already the string whose hash the caller signed. */\n body: string;\n /**\n * Value for the `X-Webhook-Signature` header. Omit when the caller authenticates by another\n * mechanism and supplies its own {@link headers} — see below.\n */\n signature?: string;\n /**\n * Caller-supplied headers, MERGED OVER the defaults.\n *\n * Added for the A2A push-notification worker, which authenticates with the CLIENT's declared\n * scheme (`Authorization: {scheme} {credentials}`) and sends `application/a2a+json` rather than\n * Wildo's own ES256 signature and `application/json`.\n *\n * Widening this primitive instead of writing a second POST is deliberate, and follows the module\n * header's own claim to own THE WIRE: timeout, abort handling, failure description and body\n * truncation are the parts that are easy to get subtly wrong and expensive to have two of. What\n * varies between callers is authentication and media type — which are exactly the two things a\n * caller is in a position to decide. Purely additive: `signature` stays optional-by-absence for\n * the two existing M2M call sites, whose behaviour is byte-for-byte unchanged.\n */\n headers?: Record<string, string>;\n}): Promise<SignedWebhookRequestResult> {\n const startedAt = Date.now();\n\n // Refused BEFORE any timer or connection: a target the guard rejects must produce no DNS lookup,\n // no socket, and no signed body on the wire. Reported as `networkError` rather than thrown, because\n // this module's contract is that a destination problem is DATA the caller reports — the probe\n // surfaces it to the admin as UNREACHABLE and the delivery worker records it on the attempt row.\n const refusal = refuseUnsafeWebhookTarget(params.url);\n if (refusal) {\n return { networkError: refusal, durationMs: Date.now() - startedAt };\n }\n\n const abort = new AbortController();\n const timer = setTimeout(() => abort.abort(), REQUEST_TIMEOUT_MS);\n\n let responseStatus: number | undefined;\n let responseBodyTruncated: string | undefined;\n let networkError: string | undefined;\n\n try {\n const response = await fetch(params.url, {\n method: 'POST',\n signal: abort.signal,\n /**\n * A redirect is REPORTED, never followed.\n *\n * Following one re-points the request at a target the guard above never saw, which is the\n * guard's most obvious bypass — an admin authors a public URL that 302s to\n * `http://169.254.169.254/`. It also silently defeats a decision this subsystem had ALREADY\n * taken and could not enforce: `classifyResponseStatus` classifies 3xx as `fatal` and\n * `resolveProbeOutcome` classifies it as REJECTED (\"webhooks must not redirect, and a\n * CDN/proxy redirect on a wrong URL is one of the most common real misconfigurations an admin\n * uses this probe to find\") — but with `fetch`'s default follow behaviour neither of those\n * branches could ever execute, because a 3xx never reached them.\n *\n * `'manual'` rather than `'error'` on purpose: `'error'` would surface as an opaque network\n * failure, whereas `'manual'` hands back the 3xx status so both classifiers see the redirect\n * and tell the admin exactly what their endpoint did.\n */\n redirect: 'manual',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': WEBHOOK_USER_AGENT,\n ...(params.signature ? { [WildoHeaderKeys.X_WEBHOOK_SIGNATURE]: params.signature } : {}),\n ...params.headers,\n },\n body: params.body,\n });\n responseStatus = response.status;\n try {\n const text = await response.text();\n responseBodyTruncated = text ? truncateBody(text) : undefined;\n } catch {\n // Best-effort body capture — never fail the attempt on a non-readable body.\n responseBodyTruncated = undefined;\n }\n } catch (error) {\n networkError = abort.signal.aborted\n ? `request timed out after ${REQUEST_TIMEOUT_MS}ms`\n : describeFetchFailure(error);\n } finally {\n clearTimeout(timer);\n }\n\n return {\n responseStatus,\n responseBodyTruncated,\n networkError,\n durationMs: Date.now() - startedAt,\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-config-test-endpoint.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/services/webhook-config-test-endpoint.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,EAWL,2BAA2B,EAI5B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAEL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAiDpH;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,2BAA2B,CAKnG;AAED,wBAAgB,wCAAwC,IAAI,0CAA0C,EAAE,CA8LvG"}
1
+ {"version":3,"file":"webhook-config-test-endpoint.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../../../../src/notifications/webhooks/outbound/services/webhook-config-test-endpoint.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,EAWL,2BAA2B,EAK5B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAEL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAiDpH;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,2BAA2B,CAKnG;AAED,wBAAgB,wCAAwC,IAAI,0CAA0C,EAAE,CAgMvG"}
@@ -38,7 +38,7 @@
38
38
  * @see m2m-webhook-delivery-log.custom-impl.backend.service.ts — sibling REPLACE impls.
39
39
  */
40
40
  import { createHash, randomUUID } from 'node:crypto';
41
- import { AuthScopeType, CoreAuditableEventType, CoreM2MNotificationChannel, CoreResourceType, ErrorCustomMessageReference, ErrorType, ResourceOperationRiskLevel, ResourceOperationVariantType, WebhookConfig_Operations, WebhookConfig_TestEndpointRequestDto, WebhookEndpointProbeOutcome, } from '@wildo-ai/saas-models';
41
+ import { AuthScopeType, CoreAuditableEventType, CoreM2MNotificationChannel, CoreResourceType, ErrorCustomMessageReference, ErrorType, ResourceOperationRiskLevel, ResourceOperationVariantType, WebhookConfig_Operations, WebhookConfig_TestEndpointRequestDto, WebhookEndpointProbeOutcome, M2M_WEBHOOK_DELIVERY_CONTRACT, } from '@wildo-ai/saas-models';
42
42
  import { createResourceCustomServiceImplementation, } from '../../../../base-classes/services/services/resource-operation-custom-implementation.factory.backend.js';
43
43
  import { describeCaughtError } from '../../../../errors/utils/error-log-level-classification.backend.utils.js';
44
44
  import { relaxAbuseThrottleMaxForEnvironment } from '../../../../errors/utils/rate-limit-environment-relaxation.backend.utils.js';
@@ -194,12 +194,14 @@ export function createWebhookConfigTestEndpointFactories() {
194
194
  let signature;
195
195
  try {
196
196
  signature = await jwtService.sign({
197
- jti: signatureJti,
198
- bodyHash,
199
- bodyHashAlg: M2M_WEBHOOK_BODY_HASH_ALGORITHM,
200
- eventChannel: channel,
201
- resourceIdentifier: String(resourceType),
202
- operationIdentifier: String(WebhookConfig_Operations.TEST_ENDPOINT),
197
+ // Claim names come from the published receiver contract, exactly as the
198
+ // delivery worker signs them; a probe must be verifiable by the same receiver code.
199
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.deliveryId]: signatureJti,
200
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.bodyHash]: bodyHash,
201
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.bodyHashAlgorithm]: M2M_WEBHOOK_BODY_HASH_ALGORITHM,
202
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.eventChannel]: channel,
203
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.resourceIdentifier]: String(resourceType),
204
+ [M2M_WEBHOOK_DELIVERY_CONTRACT.signatureClaims.operationIdentifier]: String(WebhookConfig_Operations.TEST_ENDPOINT),
203
205
  // Present ONLY on probes. Lets a consumer that wants to hard-refuse
204
206
  // synthetic traffic do so on the signed envelope rather than on body text.
205
207
  synthetic: true,