@wildo-ai/saas-backend-lib 1.1.8 → 1.1.9

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 (174) hide show
  1. package/dist/esm/data-seeding/models/data-seed.shared.schemas.d.ts +8 -0
  2. package/dist/esm/data-seeding/models/data-seed.shared.schemas.d.ts.map +1 -1
  3. package/dist/esm/data-seeding/models/data-seed.shared.schemas.js +17 -0
  4. package/dist/esm/data-seeding/models/data-seed.shared.schemas.js.map +1 -1
  5. package/dist/esm/data-seeding/services/data-seed-application-bootstrap.backend.d.ts.map +1 -1
  6. package/dist/esm/data-seeding/services/data-seed-application-bootstrap.backend.js +9 -1
  7. package/dist/esm/data-seeding/services/data-seed-application-bootstrap.backend.js.map +1 -1
  8. package/dist/esm/data-seeding/services/data-seed-environment-admission.backend.d.ts +23 -0
  9. package/dist/esm/data-seeding/services/data-seed-environment-admission.backend.d.ts.map +1 -0
  10. package/dist/esm/data-seeding/services/data-seed-environment-admission.backend.js +25 -0
  11. package/dist/esm/data-seeding/services/data-seed-environment-admission.backend.js.map +1 -0
  12. package/dist/esm/databases/services/postgresql-migration-runner.backend.d.ts +6 -0
  13. package/dist/esm/databases/services/postgresql-migration-runner.backend.d.ts.map +1 -1
  14. package/dist/esm/databases/services/postgresql-migration-runner.backend.js +8 -1
  15. package/dist/esm/databases/services/postgresql-migration-runner.backend.js.map +1 -1
  16. package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts +0 -1
  17. package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts.map +1 -1
  18. package/dist/esm/external-providers/services/external-providers-registry.backend.service.js +4 -39
  19. package/dist/esm/external-providers/services/external-providers-registry.backend.service.js.map +1 -1
  20. package/dist/esm/flows-actors/services/agents.backend.service.d.ts +15 -4
  21. package/dist/esm/flows-actors/services/agents.backend.service.d.ts.map +1 -1
  22. package/dist/esm/flows-actors/services/agents.backend.service.js +21 -11
  23. package/dist/esm/flows-actors/services/agents.backend.service.js.map +1 -1
  24. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts +47 -2
  25. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts.map +1 -1
  26. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js +56 -8
  27. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js.map +1 -1
  28. package/dist/esm/queue/job-consumer.backend.service.d.ts.map +1 -1
  29. package/dist/esm/queue/job-consumer.backend.service.js +21 -8
  30. package/dist/esm/queue/job-consumer.backend.service.js.map +1 -1
  31. package/dist/esm/queue/job-status-tracking.backend.service.d.ts.map +1 -1
  32. package/dist/esm/queue/job-status-tracking.backend.service.js +4 -1
  33. package/dist/esm/queue/job-status-tracking.backend.service.js.map +1 -1
  34. package/dist/esm/queue/queue.backend.service.d.ts +14 -0
  35. package/dist/esm/queue/queue.backend.service.d.ts.map +1 -1
  36. package/dist/esm/queue/queue.backend.service.js +68 -12
  37. package/dist/esm/queue/queue.backend.service.js.map +1 -1
  38. package/dist/esm/queue/scheduled-evidence-coverage.backend.d.ts +8 -0
  39. package/dist/esm/queue/scheduled-evidence-coverage.backend.d.ts.map +1 -1
  40. package/dist/esm/queue/scheduled-evidence-coverage.backend.js +20 -2
  41. package/dist/esm/queue/scheduled-evidence-coverage.backend.js.map +1 -1
  42. package/dist/esm/queue/scheduled-execution.backend.service.d.ts +44 -1
  43. package/dist/esm/queue/scheduled-execution.backend.service.d.ts.map +1 -1
  44. package/dist/esm/queue/scheduled-execution.backend.service.js +180 -10
  45. package/dist/esm/queue/scheduled-execution.backend.service.js.map +1 -1
  46. package/dist/esm/queue/scheduled-message-verifier.backend.service.d.ts +73 -21
  47. package/dist/esm/queue/scheduled-message-verifier.backend.service.d.ts.map +1 -1
  48. package/dist/esm/queue/scheduled-message-verifier.backend.service.js +162 -88
  49. package/dist/esm/queue/scheduled-message-verifier.backend.service.js.map +1 -1
  50. package/dist/esm/runtime/services/start-application.backend.service.d.ts.map +1 -1
  51. package/dist/esm/runtime/services/start-application.backend.service.js +16 -1
  52. package/dist/esm/runtime/services/start-application.backend.service.js.map +1 -1
  53. package/package.json +10 -10
  54. package/dist/esm/__tests__/fixture-deep-partial.d.ts +0 -18
  55. package/dist/esm/__tests__/fixture-deep-partial.d.ts.map +0 -1
  56. package/dist/esm/__tests__/fixture-deep-partial.js +0 -1
  57. package/dist/esm/__tests__/fixture-deep-partial.js.map +0 -1
  58. package/dist/esm/__tests__/integration/redis-integration-gate.d.ts +0 -26
  59. package/dist/esm/__tests__/integration/redis-integration-gate.d.ts.map +0 -1
  60. package/dist/esm/__tests__/integration/redis-integration-gate.js +0 -105
  61. package/dist/esm/__tests__/integration/redis-integration-gate.js.map +0 -1
  62. package/dist/esm/__tests__/setup.d.ts +0 -7
  63. package/dist/esm/__tests__/setup.d.ts.map +0 -1
  64. package/dist/esm/__tests__/setup.js +0 -14
  65. package/dist/esm/__tests__/setup.js.map +0 -1
  66. package/dist/esm/__tests__/test-container.d.ts +0 -41
  67. package/dist/esm/__tests__/test-container.d.ts.map +0 -1
  68. package/dist/esm/__tests__/test-container.js +0 -98
  69. package/dist/esm/__tests__/test-container.js.map +0 -1
  70. package/dist/esm/base-classes/services/__tests__/mock-resource-operation-helper.service.d.ts +0 -47
  71. package/dist/esm/base-classes/services/__tests__/mock-resource-operation-helper.service.d.ts.map +0 -1
  72. package/dist/esm/base-classes/services/__tests__/mock-resource-operation-helper.service.js +0 -82
  73. package/dist/esm/base-classes/services/__tests__/mock-resource-operation-helper.service.js.map +0 -1
  74. package/dist/esm/base-classes/services/repositories/__tests__/method-span-scan.guard-helpers.d.ts +0 -57
  75. package/dist/esm/base-classes/services/repositories/__tests__/method-span-scan.guard-helpers.d.ts.map +0 -1
  76. package/dist/esm/base-classes/services/repositories/__tests__/method-span-scan.guard-helpers.js +0 -172
  77. package/dist/esm/base-classes/services/repositories/__tests__/method-span-scan.guard-helpers.js.map +0 -1
  78. package/dist/esm/base-classes/services/repositories/__tests__/repositories-registry-infrastructure-read.double.d.ts +0 -41
  79. package/dist/esm/base-classes/services/repositories/__tests__/repositories-registry-infrastructure-read.double.d.ts.map +0 -1
  80. package/dist/esm/base-classes/services/repositories/__tests__/repositories-registry-infrastructure-read.double.js +0 -54
  81. package/dist/esm/base-classes/services/repositories/__tests__/repositories-registry-infrastructure-read.double.js.map +0 -1
  82. package/dist/esm/billing/services/__tests__/subscription-provider-projection.test-double.d.ts +0 -20
  83. package/dist/esm/billing/services/__tests__/subscription-provider-projection.test-double.d.ts.map +0 -1
  84. package/dist/esm/billing/services/__tests__/subscription-provider-projection.test-double.js +0 -33
  85. package/dist/esm/billing/services/__tests__/subscription-provider-projection.test-double.js.map +0 -1
  86. package/dist/esm/billing/services/custom-impl/__tests__/plan-change-confirmation.fixture.d.ts +0 -22
  87. package/dist/esm/billing/services/custom-impl/__tests__/plan-change-confirmation.fixture.d.ts.map +0 -1
  88. package/dist/esm/billing/services/custom-impl/__tests__/plan-change-confirmation.fixture.js +0 -92
  89. package/dist/esm/billing/services/custom-impl/__tests__/plan-change-confirmation.fixture.js.map +0 -1
  90. package/dist/esm/billing/services/custom-impl/__tests__/subscription-keyed-mutation.fixture.d.ts +0 -15
  91. package/dist/esm/billing/services/custom-impl/__tests__/subscription-keyed-mutation.fixture.d.ts.map +0 -1
  92. package/dist/esm/billing/services/custom-impl/__tests__/subscription-keyed-mutation.fixture.js +0 -61
  93. package/dist/esm/billing/services/custom-impl/__tests__/subscription-keyed-mutation.fixture.js.map +0 -1
  94. package/dist/esm/billing/services/webhooks/__tests__/billing-event-authority.test-double.d.ts +0 -21
  95. package/dist/esm/billing/services/webhooks/__tests__/billing-event-authority.test-double.d.ts.map +0 -1
  96. package/dist/esm/billing/services/webhooks/__tests__/billing-event-authority.test-double.js +0 -32
  97. package/dist/esm/billing/services/webhooks/__tests__/billing-event-authority.test-double.js.map +0 -1
  98. package/dist/esm/billing/services/webhooks/__tests__/settlement-ledger.test-double.d.ts +0 -52
  99. package/dist/esm/billing/services/webhooks/__tests__/settlement-ledger.test-double.d.ts.map +0 -1
  100. package/dist/esm/billing/services/webhooks/__tests__/settlement-ledger.test-double.js +0 -74
  101. package/dist/esm/billing/services/webhooks/__tests__/settlement-ledger.test-double.js.map +0 -1
  102. package/dist/esm/config/services/__tests__/mock-app-configuration.service.d.ts +0 -60
  103. package/dist/esm/config/services/__tests__/mock-app-configuration.service.d.ts.map +0 -1
  104. package/dist/esm/config/services/__tests__/mock-app-configuration.service.js +0 -156
  105. package/dist/esm/config/services/__tests__/mock-app-configuration.service.js.map +0 -1
  106. package/dist/esm/config/services/__tests__/startup-validator-resources-registry.double.d.ts +0 -31
  107. package/dist/esm/config/services/__tests__/startup-validator-resources-registry.double.d.ts.map +0 -1
  108. package/dist/esm/config/services/__tests__/startup-validator-resources-registry.double.js +0 -47
  109. package/dist/esm/config/services/__tests__/startup-validator-resources-registry.double.js.map +0 -1
  110. package/dist/esm/databases/services/__tests__/mock-mongoose-model.service.d.ts +0 -20
  111. package/dist/esm/databases/services/__tests__/mock-mongoose-model.service.d.ts.map +0 -1
  112. package/dist/esm/databases/services/__tests__/mock-mongoose-model.service.js +0 -424
  113. package/dist/esm/databases/services/__tests__/mock-mongoose-model.service.js.map +0 -1
  114. package/dist/esm/databases/services/__tests__/mock-zod-mongoose-schema-convertor.service.d.ts +0 -19
  115. package/dist/esm/databases/services/__tests__/mock-zod-mongoose-schema-convertor.service.d.ts.map +0 -1
  116. package/dist/esm/databases/services/__tests__/mock-zod-mongoose-schema-convertor.service.js +0 -49
  117. package/dist/esm/databases/services/__tests__/mock-zod-mongoose-schema-convertor.service.js.map +0 -1
  118. package/dist/esm/flows-actors/services/__tests__/pending-decision.fixtures.d.ts +0 -43
  119. package/dist/esm/flows-actors/services/__tests__/pending-decision.fixtures.d.ts.map +0 -1
  120. package/dist/esm/flows-actors/services/__tests__/pending-decision.fixtures.js +0 -61
  121. package/dist/esm/flows-actors/services/__tests__/pending-decision.fixtures.js.map +0 -1
  122. package/dist/esm/inbound/services/__tests__/support/in-memory-infrastructure-repositories.d.ts +0 -25
  123. package/dist/esm/inbound/services/__tests__/support/in-memory-infrastructure-repositories.d.ts.map +0 -1
  124. package/dist/esm/inbound/services/__tests__/support/in-memory-infrastructure-repositories.js +0 -92
  125. package/dist/esm/inbound/services/__tests__/support/in-memory-infrastructure-repositories.js.map +0 -1
  126. package/dist/esm/meta-workflows/services/__tests__/fixtures/meta-workflow-evaluation-treatment.test-fixture.d.ts +0 -7
  127. package/dist/esm/meta-workflows/services/__tests__/fixtures/meta-workflow-evaluation-treatment.test-fixture.d.ts.map +0 -1
  128. package/dist/esm/meta-workflows/services/__tests__/fixtures/meta-workflow-evaluation-treatment.test-fixture.js +0 -194
  129. package/dist/esm/meta-workflows/services/__tests__/fixtures/meta-workflow-evaluation-treatment.test-fixture.js.map +0 -1
  130. package/dist/esm/monitoring/logger/services/__tests__/mock-logger.service.d.ts +0 -34
  131. package/dist/esm/monitoring/logger/services/__tests__/mock-logger.service.d.ts.map +0 -1
  132. package/dist/esm/monitoring/logger/services/__tests__/mock-logger.service.js +0 -59
  133. package/dist/esm/monitoring/logger/services/__tests__/mock-logger.service.js.map +0 -1
  134. package/dist/esm/monitoring/observability/services/__tests__/mock-metrics.service.d.ts +0 -48
  135. package/dist/esm/monitoring/observability/services/__tests__/mock-metrics.service.d.ts.map +0 -1
  136. package/dist/esm/monitoring/observability/services/__tests__/mock-metrics.service.js +0 -53
  137. package/dist/esm/monitoring/observability/services/__tests__/mock-metrics.service.js.map +0 -1
  138. package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/bundle.d.ts +0 -3
  139. package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/bundle.d.ts.map +0 -1
  140. package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/bundle.js +0 -19
  141. package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/bundle.js.map +0 -1
  142. package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/host.d.ts +0 -51
  143. package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/host.d.ts.map +0 -1
  144. package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/host.js +0 -24
  145. package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/host.js.map +0 -1
  146. package/dist/esm/notifications/services/__tests__/recipient-language.test-double.d.ts +0 -8
  147. package/dist/esm/notifications/services/__tests__/recipient-language.test-double.d.ts.map +0 -1
  148. package/dist/esm/notifications/services/__tests__/recipient-language.test-double.js +0 -9
  149. package/dist/esm/notifications/services/__tests__/recipient-language.test-double.js.map +0 -1
  150. package/dist/esm/pdf-templates/__tests__/fixtures/tiny-pdf-template.fixture.d.ts +0 -10
  151. package/dist/esm/pdf-templates/__tests__/fixtures/tiny-pdf-template.fixture.d.ts.map +0 -1
  152. package/dist/esm/pdf-templates/__tests__/fixtures/tiny-pdf-template.fixture.js +0 -18
  153. package/dist/esm/pdf-templates/__tests__/fixtures/tiny-pdf-template.fixture.js.map +0 -1
  154. package/dist/esm/security/api-keys/__tests__/api-key-usage-redis.double.d.ts +0 -11
  155. package/dist/esm/security/api-keys/__tests__/api-key-usage-redis.double.d.ts.map +0 -1
  156. package/dist/esm/security/api-keys/__tests__/api-key-usage-redis.double.js +0 -34
  157. package/dist/esm/security/api-keys/__tests__/api-key-usage-redis.double.js.map +0 -1
  158. package/dist/esm/security/authentications/__tests__/registration-onboarding.harness.d.ts +0 -105
  159. package/dist/esm/security/authentications/__tests__/registration-onboarding.harness.d.ts.map +0 -1
  160. package/dist/esm/security/authentications/__tests__/registration-onboarding.harness.js +0 -140
  161. package/dist/esm/security/authentications/__tests__/registration-onboarding.harness.js.map +0 -1
  162. package/dist/esm/security/authentications/services/__tests__/oauth-provider-audit-spies.mock.d.ts +0 -35
  163. package/dist/esm/security/authentications/services/__tests__/oauth-provider-audit-spies.mock.d.ts.map +0 -1
  164. package/dist/esm/security/authentications/services/__tests__/oauth-provider-audit-spies.mock.js +0 -46
  165. package/dist/esm/security/authentications/services/__tests__/oauth-provider-audit-spies.mock.js.map +0 -1
  166. package/dist/esm/security/connected-accounts/__tests__/support/real-connection-client-methods.d.ts +0 -2
  167. package/dist/esm/security/connected-accounts/__tests__/support/real-connection-client-methods.d.ts.map +0 -1
  168. package/dist/esm/security/connected-accounts/__tests__/support/real-connection-client-methods.js +0 -18
  169. package/dist/esm/security/connected-accounts/__tests__/support/real-connection-client-methods.js.map +0 -1
  170. package/dist/esm/systems-of-record/credentials/__tests__/certificate-fixtures.d.ts +0 -20
  171. package/dist/esm/systems-of-record/credentials/__tests__/certificate-fixtures.d.ts.map +0 -1
  172. package/dist/esm/systems-of-record/credentials/__tests__/certificate-fixtures.js +0 -56
  173. package/dist/esm/systems-of-record/credentials/__tests__/certificate-fixtures.js.map +0 -1
  174. package/dist/tsconfig.build.tsbuildinfo +0 -1
@@ -9,7 +9,7 @@
9
9
  import { LoggerService } from '../monitoring/logger/services/logger.service';
10
10
  import { JwtBackendService } from '../security/jwt/services/jwt.backend.service';
11
11
  import { ErrorBuilderBackendService } from '../errors/services/error-builder.backend.service';
12
- import { JobPayload, JobType } from '@wildo-ai/saas-models';
12
+ import { JobPayload, JobType, ScheduledDeliveryLateness } from '@wildo-ai/saas-models';
13
13
  import { z } from 'zod';
14
14
  /**
15
15
  * Schema for the SCHEDULED_CRON arm of the scheduled-job token (the
@@ -78,16 +78,46 @@ export type ScheduledJobToken = z.infer<typeof ScheduledJobTokenSchema>;
78
78
  export type ScheduledCronJobToken = z.infer<typeof ScheduledCronJobTokenSchema>;
79
79
  export type MinionTickJobToken = z.infer<typeof MinionTickJobTokenSchema>;
80
80
  /**
81
- * Result of scheduled message verification.
81
+ * What a scheduled delivery IS, decided before anything durable happens (#2212).
82
+ *
83
+ * Three answers, not two, because "not admissible" hid two opposite situations behind one
84
+ * `AUTHENTICATION` error: a genuine token that waited out its window in a queue nobody consumed
85
+ * (operational — the application was down) and a token that cannot be proven genuine (a trust
86
+ * signal). Logged identically, 139 of the first per restart read as a credential outage and sent the
87
+ * diagnosis to the wrong service.
82
88
  */
83
- export interface ScheduledMessageVerificationResult {
84
- /** Whether the message is valid */
85
- isValid: boolean;
86
- /** The verified token payload (if valid) */
87
- token?: ScheduledJobToken;
88
- /** Error message (if invalid) */
89
- error?: string;
89
+ export declare enum ScheduledDeliveryVerdict {
90
+ /** Authentic, inside its admission window: admit it. */
91
+ ADMISSIBLE = "admissible",
92
+ /** Authentic (signature, issuer, audience, structure all proven) but past its admission deadline: the occurrence was MISSED. */
93
+ LATE = "late",
94
+ /** Not provably authentic, or it names an occurrence in the future: a trust failure. */
95
+ REJECTED = "rejected"
90
96
  }
97
+ /** Result of scheduled message verification. */
98
+ export type ScheduledMessageVerificationResult = {
99
+ verdict: ScheduledDeliveryVerdict.ADMISSIBLE;
100
+ token: ScheduledJobToken;
101
+ } | {
102
+ verdict: ScheduledDeliveryVerdict.LATE;
103
+ token: ScheduledJobToken;
104
+ lateness: ScheduledDeliveryLateness;
105
+ } | {
106
+ verdict: ScheduledDeliveryVerdict.REJECTED;
107
+ error: string;
108
+ };
109
+ /**
110
+ * An authentic delivery, converted for its consumer. Only the two authentic verdicts are
111
+ * representable here; a rejection is thrown by the `verifyAnd…` entry points.
112
+ */
113
+ export type AuthenticScheduledDelivery<T> = {
114
+ verdict: ScheduledDeliveryVerdict.ADMISSIBLE;
115
+ value: T;
116
+ } | {
117
+ verdict: ScheduledDeliveryVerdict.LATE;
118
+ value: T;
119
+ lateness: ScheduledDeliveryLateness;
120
+ };
91
121
  export declare class ScheduledMessageVerifierBackendService {
92
122
  private readonly jwtService;
93
123
  private readonly logger;
@@ -95,12 +125,32 @@ export declare class ScheduledMessageVerifierBackendService {
95
125
  protected logDebug: (message: string, context?: Record<string, unknown>) => void;
96
126
  constructor(jwtService: JwtBackendService, logger: LoggerService, errorBuilder: ErrorBuilderBackendService);
97
127
  /**
98
- * Verify a scheduled job message token.
128
+ * Classify a scheduled job message token: admissible, late, or rejected.
129
+ *
130
+ * Only a REJECTED verdict logs here (warn), because only it is a trust signal. A LATE delivery is
131
+ * reported by the consumer's runner as one digest per drain, not once per message.
99
132
  *
100
133
  * @param token - The JWT token from the message
101
- * @returns Verification result with token payload if valid
102
134
  */
103
135
  verifyScheduledMessage(token: string): Promise<ScheduledMessageVerificationResult>;
136
+ /**
137
+ * The payload of a token whose ONLY defect is expiry — or, when it is not provably that, why.
138
+ *
139
+ * `jsonwebtoken` checks expiry before audience and issuer, so a `TokenExpiredError` alone proves
140
+ * the signature and nothing more. Re-verifying with expiry ignored — quietly, through the same key
141
+ * set, rotation included — proves issuer and audience too, and the `exp` check below confirms the
142
+ * first failure really was the clock. Any other first failure (bad signature, wrong audience, an
143
+ * unknown key) fails this re-verification and stays a rejection. The late payload is never ADMITTED,
144
+ * so this cannot turn an expired token into a run; it only decides how the miss is reported.
145
+ *
146
+ * A token that expired longer ago than `SCHEDULED_LATE_DELIVERY_HORIZON_MS` is refused too: no
147
+ * genuine backlog is that old, and an authentic-but-ancient token is what a replay looks like.
148
+ *
149
+ * `refused` carries the re-verification's own error when it threw, and is `undefined` when the
150
+ * token re-verified but is not a recordable late delivery (not expired, or past the horizon).
151
+ */
152
+ private authenticatedExpiredPayload;
153
+ private rejected;
104
154
  /**
105
155
  * Convert a verified `SCHEDULED_CRON` token to a JobPayload for
106
156
  * execution by the resource-operation / custom-batch executor pipeline.
@@ -113,19 +163,20 @@ export declare class ScheduledMessageVerifierBackendService {
113
163
  convertToJobPayload(token: ScheduledCronJobToken): JobPayload;
114
164
  /**
115
165
  * Verify and convert a `SCHEDULED_CRON` scheduled message in one step.
116
- * Throws if verification fails OR if the token is a `MINION_TICK`
117
- * (which the resource-operation / custom-batch executor cannot
118
- * dispatch; the minion runtime owns those — use
119
- * `verifyAndExtractMinionTick`).
166
+ *
167
+ * Returns the converted payload with its verdict — ADMISSIBLE, or LATE with why — and throws only
168
+ * when the delivery is not authentic, or is a `MINION_TICK` (which the resource-operation /
169
+ * custom-batch executor cannot dispatch; the minion runtime owns those — use
170
+ * `verifyAndExtractMinionTick`). A LATE delivery is returned, never thrown: it is genuine, and the
171
+ * consumer records it as a missed occurrence instead of failing it (#2212).
120
172
  *
121
173
  * @param token - The JWT token from the message
122
- * @returns JobPayload ready for execution
123
174
  * @throws WildoError if verification fails or the token targets a minion
124
175
  */
125
- verifyAndConvert(token: string): Promise<JobPayload>;
176
+ verifyAndConvert(token: string): Promise<AuthenticScheduledDelivery<JobPayload>>;
126
177
  /**
127
178
  * Verify a scheduled message and extract a `MinionTickJobToken` if and
128
- * only if the token targets the given minion (KD-12).
179
+ * only if the token targets the given minion (KD-12), with its verdict.
129
180
  *
130
181
  * Returns `null` (not throws) for the two non-actionable cases that the
131
182
  * minion runtime should silently skip:
@@ -133,8 +184,9 @@ export declare class ScheduledMessageVerifierBackendService {
133
184
  * - the token is a `MINION_TICK` whose `minionName` does not match
134
185
  * `expectedMinionName`.
135
186
  *
136
- * Throws (typed authentication error) only when verification itself
137
- * fails — invalid signature, malformed payload, wrong issuer/audience.
187
+ * A LATE tick is returned with its lateness, exactly like `verifyAndConvert` (#2212). Throws (typed
188
+ * authentication error) only when the delivery is not authentic — invalid signature, malformed
189
+ * payload, wrong issuer/audience, a future occurrence.
138
190
  *
139
191
  * The minion-side consumer passes its compiled-in `minionName` (from
140
192
  * `defineMinionInit({ minionName })`, cross-validated against the fetched
@@ -143,7 +195,7 @@ export declare class ScheduledMessageVerifierBackendService {
143
195
  * should be NACK'd back to the queue (or skipped, depending on the
144
196
  * queue-routing strategy chosen for the minion fan-out).
145
197
  */
146
- verifyAndExtractMinionTick(token: string, expectedMinionName: string): Promise<MinionTickJobToken | null>;
198
+ verifyAndExtractMinionTick(token: string, expectedMinionName: string): Promise<AuthenticScheduledDelivery<MinionTickJobToken> | null>;
147
199
  }
148
200
  export {};
149
201
  //# sourceMappingURL=scheduled-message-verifier.backend.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scheduled-message-verifier.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/queue/scheduled-message-verifier.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAAE,aAAa,EAAE,MAAM,8DAA8D,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,8DAA8D,CAAC;AAEjG,OAAO,EAAE,0BAA0B,EAAE,MAAM,kEAAkE,CAAC;AAC9G,OAAO,EACL,UAAU,EACV,OAAO,EAMR,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA6BxB;;;GAGG;AACH,QAAA,MAAM,2BAA2B;;;;;;;;;;;;iBAQ/B,CAAC;AAEH;;;;GAIG;AACH,QAAA,MAAM,wBAAwB;;;;;;;;;;;iBAM5B,CAAC;AAEH;;;;GAIG;AACH,QAAA,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;8BAG3B,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAChF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E;;GAEG;AACH,MAAM,WAAW,kCAAkC;IACjD,mCAAmC;IACnC,OAAO,EAAE,OAAO,CAAC;IAEjB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAE1B,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBACa,sCAAsC;IAK/C,OAAO,CAAC,QAAQ,CAAC,UAAU;IAE3B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAR/B,SAAS,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;gBAI9D,UAAU,EAAE,iBAAiB,EAE7B,MAAM,EAAE,aAAa,EAErB,YAAY,EAAE,0BAA0B;IAM3D;;;;;OAKG;IACG,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kCAAkC,CAAC;IA6FxF;;;;;;;;OAQG;IACH,mBAAmB,CAAC,KAAK,EAAE,qBAAqB,GAAG,UAAU;IAiC7D;;;;;;;;;;OAUG;IACG,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAqC1D;;;;;;;;;;;;;;;;;;;OAmBG;IACG,0BAA0B,CAC9B,KAAK,EAAE,MAAM,EACb,kBAAkB,EAAE,MAAM,GACzB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;CAoCtC"}
1
+ {"version":3,"file":"scheduled-message-verifier.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/queue/scheduled-message-verifier.backend.service.ts"],"names":[],"mappings":"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"}
@@ -18,14 +18,14 @@ var __metadata = (this && this.__metadata) || function (k, v) {
18
18
  var __param = (this && this.__param) || function (paramIndex, decorator) {
19
19
  return function (target, key) { decorator(target, key, paramIndex); }
20
20
  };
21
- import { SCHEDULED_INITIAL_ADMISSION_MAX_AGE_MS, SCHEDULED_INITIAL_ADMISSION_FUTURE_SKEW_MS } from '@wildo-ai/saas-models';
21
+ import { SCHEDULED_INITIAL_ADMISSION_MAX_AGE_MS, SCHEDULED_INITIAL_ADMISSION_FUTURE_SKEW_MS, SCHEDULED_LATE_DELIVERY_HORIZON_MS } from '@wildo-ai/saas-models';
22
22
  import { injectable, inject } from 'inversify';
23
23
  import { SAAS_SERVICE_TYPES } from '../inject/models/service-types.js';
24
24
  import { LoggerService } from '../monitoring/logger/services/logger.service.js';
25
25
  import { JwtBackendService } from '../security/jwt/services/jwt.backend.service.js';
26
26
  import { JwtPublicKeys } from '../security/authentications/models/jwt.backend.schemas.js';
27
27
  import { ErrorBuilderBackendService } from '../errors/services/error-builder.backend.service.js';
28
- import { JobType, JobInitiator, JobPriority, ErrorType, ErrorCustomMessageReference, isWildoBackendError, } from '@wildo-ai/saas-models';
28
+ import { JobType, JobInitiator, JobPriority, ErrorType, ErrorCustomMessageReference, isWildoBackendError, ScheduledDeliveryLateness, } from '@wildo-ai/saas-models';
29
29
  import { z } from 'zod';
30
30
  import { describeCaughtError } from '../errors/utils/error-log-level-classification.backend.utils.js';
31
31
  /**
@@ -81,6 +81,24 @@ const ScheduledJobTokenSchema = z.discriminatedUnion('jobType', [
81
81
  ScheduledCronJobTokenSchema,
82
82
  MinionTickJobTokenSchema,
83
83
  ]);
84
+ /**
85
+ * What a scheduled delivery IS, decided before anything durable happens (#2212).
86
+ *
87
+ * Three answers, not two, because "not admissible" hid two opposite situations behind one
88
+ * `AUTHENTICATION` error: a genuine token that waited out its window in a queue nobody consumed
89
+ * (operational — the application was down) and a token that cannot be proven genuine (a trust
90
+ * signal). Logged identically, 139 of the first per restart read as a credential outage and sent the
91
+ * diagnosis to the wrong service.
92
+ */
93
+ export var ScheduledDeliveryVerdict;
94
+ (function (ScheduledDeliveryVerdict) {
95
+ /** Authentic, inside its admission window: admit it. */
96
+ ScheduledDeliveryVerdict["ADMISSIBLE"] = "admissible";
97
+ /** Authentic (signature, issuer, audience, structure all proven) but past its admission deadline: the occurrence was MISSED. */
98
+ ScheduledDeliveryVerdict["LATE"] = "late";
99
+ /** Not provably authentic, or it names an occurrence in the future: a trust failure. */
100
+ ScheduledDeliveryVerdict["REJECTED"] = "rejected";
101
+ })(ScheduledDeliveryVerdict || (ScheduledDeliveryVerdict = {}));
84
102
  let ScheduledMessageVerifierBackendService = class ScheduledMessageVerifierBackendService {
85
103
  jwtService;
86
104
  logger;
@@ -94,91 +112,140 @@ let ScheduledMessageVerifierBackendService = class ScheduledMessageVerifierBacke
94
112
  this.logger.setDebugTypeLogging('ScheduledMessageVerifierBackendService', false);
95
113
  }
96
114
  /**
97
- * Verify a scheduled job message token.
115
+ * Classify a scheduled job message token: admissible, late, or rejected.
116
+ *
117
+ * Only a REJECTED verdict logs here (warn), because only it is a trust signal. A LATE delivery is
118
+ * reported by the consumer's runner as one digest per drain, not once per message.
98
119
  *
99
120
  * @param token - The JWT token from the message
100
- * @returns Verification result with token payload if valid
101
121
  */
102
122
  async verifyScheduledMessage(token) {
123
+ let decoded;
124
+ let lateness;
103
125
  try {
104
126
  // Verify the JWT using the crontabs-batches-manager public key
105
- const decoded = await this.jwtService.verify(token, JwtPublicKeys.PLATFORM_CRONTABS_BATCHES_MANAGER);
106
- // Parse and validate the token payload
107
- const parseResult = ScheduledJobTokenSchema.safeParse(decoded);
108
- if (!parseResult.success) {
109
- this.logger.warn('Scheduled message token has invalid structure', {
110
- issues: parseResult.error.issues,
111
- });
112
- return {
113
- isValid: false,
114
- error: 'Invalid token structure',
115
- };
127
+ decoded = await this.jwtService.verify(token, JwtPublicKeys.PLATFORM_CRONTABS_BATCHES_MANAGER);
128
+ }
129
+ catch (error) {
130
+ const expired = await this.authenticatedExpiredPayload(token);
131
+ // When the first failure was the clock and the re-verification refused, the REAL reason is the
132
+ // re-verification's (a wrong audience behind an expiry, say): logging "jwt expired" there would
133
+ // be the exact misdirection #2212 exists to remove, aimed the other way.
134
+ if ('refused' in expired)
135
+ return this.rejected(expired.refused ?? error);
136
+ decoded = expired.payload;
137
+ lateness = ScheduledDeliveryLateness.TOKEN_EXPIRED;
138
+ }
139
+ // Parse and validate the token payload
140
+ const parseResult = ScheduledJobTokenSchema.safeParse(decoded);
141
+ if (!parseResult.success) {
142
+ this.logger.warn('Scheduled message token has invalid structure', {
143
+ issues: parseResult.error.issues,
144
+ });
145
+ return { verdict: ScheduledDeliveryVerdict.REJECTED, error: 'Invalid token structure' };
146
+ }
147
+ const tokenPayload = parseResult.data;
148
+ if (tokenPayload.scheduledAt) {
149
+ const age = Date.now() - Date.parse(tokenPayload.scheduledAt);
150
+ // A FUTURE occurrence is not lateness: a signer whose clock runs ahead, or a token minted for
151
+ // an instant that has not happened, is a trust question and stays a rejection.
152
+ if (age < -SCHEDULED_INITIAL_ADMISSION_FUTURE_SKEW_MS) {
153
+ return this.rejected(undefined, 'Scheduled occurrence is outside the initial admission window (in the future)');
116
154
  }
117
- const tokenPayload = parseResult.data;
118
- if (tokenPayload.scheduledAt) {
119
- const age = Date.now() - Date.parse(tokenPayload.scheduledAt);
120
- if (age > SCHEDULED_INITIAL_ADMISSION_MAX_AGE_MS || age < -SCHEDULED_INITIAL_ADMISSION_FUTURE_SKEW_MS) {
121
- return { isValid: false, error: 'Scheduled occurrence is outside the initial admission window' };
155
+ // An occurrence older than the window is LATE, not a trust failure: the token is authentic and
156
+ // current, and the delay is shared between the scheduler's signing and this application's
157
+ // consumption (`ScheduledDeliveryLateness.OUTSIDE_ADMISSION_WINDOW`). Decided with #2212, whose
158
+ // first wording named only the expired-token case: reporting a genuine, merely late token as an
159
+ // authentication failure is the misdirection that issue removes, whichever gate fires.
160
+ if (age > SCHEDULED_INITIAL_ADMISSION_MAX_AGE_MS)
161
+ lateness ??= ScheduledDeliveryLateness.OUTSIDE_ADMISSION_WINDOW;
162
+ }
163
+ // Note: issuer + audience are already enforced by `JwtBackendService.verify`
164
+ // via `JwtVerifyOptions[PLATFORM_CRONTABS_BATCHES_MANAGER]`
165
+ // (`issuer: 'platform_crontabs_batches_manager'` + `audience: 'SCHEDULED_JOB'`), and by the
166
+ // expiry-ignoring re-verification in `authenticatedExpiredPayload` for a late token.
167
+ if (lateness !== undefined) {
168
+ return { verdict: ScheduledDeliveryVerdict.LATE, token: tokenPayload, lateness };
169
+ }
170
+ this.logDebug('Scheduled message verified successfully', {
171
+ jobId: tokenPayload.jobId,
172
+ jobType: tokenPayload.jobType,
173
+ applicationId: tokenPayload.applicationId,
174
+ ...(tokenPayload.jobType === JobType.SCHEDULED_CRON
175
+ ? {
176
+ hasBatchRef: !!tokenPayload.batchRef,
177
+ hasOperationPathKey: !!tokenPayload.operationPathKey,
122
178
  }
123
- }
124
- // Note: issuer + audience are already enforced by `JwtBackendService.verify`
125
- // via `JwtVerifyOptions[PLATFORM_CRONTABS_BATCHES_MANAGER]`
126
- // (`issuer: 'platform_crontabs_batches_manager'` + `audience: 'SCHEDULED_JOB'`).
127
- // Any token that reaches this point passed those checks; the
128
- // post-verify uppercase-issuer / audience checks that previously
129
- // lived here were dead code and have been removed (L5 fix per the
130
- // implementation review).
131
- this.logDebug('Scheduled message verified successfully', {
132
- jobId: tokenPayload.jobId,
133
- jobType: tokenPayload.jobType,
134
- applicationId: tokenPayload.applicationId,
135
- ...(tokenPayload.jobType === JobType.SCHEDULED_CRON
136
- ? {
137
- hasBatchRef: !!tokenPayload.batchRef,
138
- hasOperationPathKey: !!tokenPayload.operationPathKey,
139
- }
140
- : { minionName: tokenPayload.minionName }),
141
- });
142
- return {
143
- isValid: true,
144
- token: tokenPayload,
145
- };
179
+ : { minionName: tokenPayload.minionName }),
180
+ });
181
+ return { verdict: ScheduledDeliveryVerdict.ADMISSIBLE, token: tokenPayload };
182
+ }
183
+ /**
184
+ * The payload of a token whose ONLY defect is expiry — or, when it is not provably that, why.
185
+ *
186
+ * `jsonwebtoken` checks expiry before audience and issuer, so a `TokenExpiredError` alone proves
187
+ * the signature and nothing more. Re-verifying with expiry ignored — quietly, through the same key
188
+ * set, rotation included — proves issuer and audience too, and the `exp` check below confirms the
189
+ * first failure really was the clock. Any other first failure (bad signature, wrong audience, an
190
+ * unknown key) fails this re-verification and stays a rejection. The late payload is never ADMITTED,
191
+ * so this cannot turn an expired token into a run; it only decides how the miss is reported.
192
+ *
193
+ * A token that expired longer ago than `SCHEDULED_LATE_DELIVERY_HORIZON_MS` is refused too: no
194
+ * genuine backlog is that old, and an authentic-but-ancient token is what a replay looks like.
195
+ *
196
+ * `refused` carries the re-verification's own error when it threw, and is `undefined` when the
197
+ * token re-verified but is not a recordable late delivery (not expired, or past the horizon).
198
+ */
199
+ async authenticatedExpiredPayload(token) {
200
+ let decoded;
201
+ try {
202
+ decoded = await this.jwtService.verifyWithoutBackendErrorActions(token, JwtPublicKeys.PLATFORM_CRONTABS_BATCHES_MANAGER, { ignoreExpiration: true });
146
203
  }
147
204
  catch (error) {
148
- // Surface the REAL reason. A `WildoBackendError` is NOT `instanceof Error`, so the previous
149
- // `... : 'Unknown error'` guard MASKED the actual cause (e.g. an empty/absent
150
- // PLATFORM_CRONTABS_BATCHES_MANAGER verify key vs a genuine signature mismatch) — which hid a
151
- // whole-platform "every scheduled tick dead-letters" bug for weeks. Never swallow it again.
152
- // The unmask must reach the JWT diagnostic, not stop at the error TYPE.
153
- //
154
- // `verifyRaw` builds its AUTHENTICATION error with `context.jwtErrorName` +
155
- // `context.jwtErrorMessage` and NO `context.message`, so a `context.message ?? type`
156
- // fallback printed a bare "AUTHENTICATION" and discarded the only fields that say
157
- // WHY — expired vs bad signature vs wrong audience. That is the same swallowing this
158
- // block was written to end, one level deeper: a minion tick that sat in its durable
159
- // queue past the 5-minute token TTL and a genuinely forged token were indistinguishable
160
- // in the log, which is why the real cause went unread for days.
161
- const detail = isWildoBackendError(error)
162
- ? (error.context?.message
163
- ?? (error.context?.jwtErrorName
164
- ? `${String(error.context.jwtErrorName)}: ${String(error.context.jwtErrorMessage ?? '')}`.trim()
165
- : undefined)
166
- ?? error.type)
167
- : describeCaughtError(error);
168
- this.logger.warn('Scheduled message verification failed', {
169
- error: detail,
170
- // Kept as discrete fields too so a log query can facet on the JWT failure kind
171
- // (`TokenExpiredError` is an operational signal — the consumer was down longer
172
- // than the token TTL — while `JsonWebTokenError` is a trust signal).
173
- ...(isWildoBackendError(error) && error.context?.jwtErrorName
174
- ? { jwtErrorName: error.context.jwtErrorName, jwtErrorMessage: error.context.jwtErrorMessage }
175
- : {}),
176
- });
177
- return {
178
- isValid: false,
179
- error: detail,
180
- };
205
+ return { refused: error };
181
206
  }
207
+ const exp = decoded?.exp;
208
+ if (typeof exp !== 'number')
209
+ return { refused: undefined };
210
+ const expiredForMs = Date.now() - exp * 1000;
211
+ if (expiredForMs < 0)
212
+ return { refused: undefined };
213
+ if (expiredForMs > SCHEDULED_LATE_DELIVERY_HORIZON_MS) {
214
+ return { refused: this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {
215
+ customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,
216
+ context: { message: `Scheduled delivery token expired ${Math.round(expiredForMs / 86400000)} day(s) ago, beyond the late-delivery horizon` },
217
+ }) };
218
+ }
219
+ return { payload: decoded };
220
+ }
221
+ rejected(error, reason) {
222
+ // Surface the REAL reason. A `WildoBackendError` is NOT `instanceof Error`, so the previous
223
+ // `... : 'Unknown error'` guard MASKED the actual cause (e.g. an empty/absent
224
+ // PLATFORM_CRONTABS_BATCHES_MANAGER verify key vs a genuine signature mismatch) — which hid a
225
+ // whole-platform "every scheduled tick dead-letters" bug for weeks. Never swallow it again.
226
+ // The unmask must reach the JWT diagnostic, not stop at the error TYPE.
227
+ //
228
+ // `verifyRaw` builds its AUTHENTICATION error with `context.jwtErrorName` +
229
+ // `context.jwtErrorMessage` and NO `context.message`, so a `context.message ?? type`
230
+ // fallback printed a bare "AUTHENTICATION" and discarded the only fields that say
231
+ // WHY — bad signature vs wrong audience vs unknown key.
232
+ const detail = reason ?? (isWildoBackendError(error)
233
+ ? (error.context?.message
234
+ ?? (error.context?.jwtErrorName
235
+ ? `${String(error.context.jwtErrorName)}: ${String(error.context.jwtErrorMessage ?? '')}`.trim()
236
+ : undefined)
237
+ ?? error.type)
238
+ : describeCaughtError(error));
239
+ this.logger.warn('Scheduled message verification failed', {
240
+ error: detail,
241
+ // Kept as discrete fields too so a log query can facet on the JWT failure kind. An expired
242
+ // but authentic token within the horizon never reaches here — it is a LATE verdict (#2212) —
243
+ // so what lands here is a trust signal, named by the check that actually refused it.
244
+ ...(isWildoBackendError(error) && error.context?.jwtErrorName
245
+ ? { jwtErrorName: error.context.jwtErrorName, jwtErrorMessage: error.context.jwtErrorMessage }
246
+ : {}),
247
+ });
248
+ return { verdict: ScheduledDeliveryVerdict.REJECTED, error: detail };
182
249
  }
183
250
  /**
184
251
  * Convert a verified `SCHEDULED_CRON` token to a JobPayload for
@@ -219,18 +286,19 @@ let ScheduledMessageVerifierBackendService = class ScheduledMessageVerifierBacke
219
286
  }
220
287
  /**
221
288
  * Verify and convert a `SCHEDULED_CRON` scheduled message in one step.
222
- * Throws if verification fails OR if the token is a `MINION_TICK`
223
- * (which the resource-operation / custom-batch executor cannot
224
- * dispatch; the minion runtime owns those — use
225
- * `verifyAndExtractMinionTick`).
289
+ *
290
+ * Returns the converted payload with its verdict — ADMISSIBLE, or LATE with why — and throws only
291
+ * when the delivery is not authentic, or is a `MINION_TICK` (which the resource-operation /
292
+ * custom-batch executor cannot dispatch; the minion runtime owns those — use
293
+ * `verifyAndExtractMinionTick`). A LATE delivery is returned, never thrown: it is genuine, and the
294
+ * consumer records it as a missed occurrence instead of failing it (#2212).
226
295
  *
227
296
  * @param token - The JWT token from the message
228
- * @returns JobPayload ready for execution
229
297
  * @throws WildoError if verification fails or the token targets a minion
230
298
  */
231
299
  async verifyAndConvert(token) {
232
300
  const result = await this.verifyScheduledMessage(token);
233
- if (!result.isValid || !result.token) {
301
+ if (result.verdict === ScheduledDeliveryVerdict.REJECTED) {
234
302
  throw this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {
235
303
  customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,
236
304
  context: {
@@ -250,11 +318,14 @@ let ScheduledMessageVerifierBackendService = class ScheduledMessageVerifierBacke
250
318
  },
251
319
  });
252
320
  }
253
- return this.convertToJobPayload(result.token);
321
+ const value = this.convertToJobPayload(result.token);
322
+ return result.verdict === ScheduledDeliveryVerdict.LATE
323
+ ? { verdict: ScheduledDeliveryVerdict.LATE, value, lateness: result.lateness }
324
+ : { verdict: ScheduledDeliveryVerdict.ADMISSIBLE, value };
254
325
  }
255
326
  /**
256
327
  * Verify a scheduled message and extract a `MinionTickJobToken` if and
257
- * only if the token targets the given minion (KD-12).
328
+ * only if the token targets the given minion (KD-12), with its verdict.
258
329
  *
259
330
  * Returns `null` (not throws) for the two non-actionable cases that the
260
331
  * minion runtime should silently skip:
@@ -262,8 +333,9 @@ let ScheduledMessageVerifierBackendService = class ScheduledMessageVerifierBacke
262
333
  * - the token is a `MINION_TICK` whose `minionName` does not match
263
334
  * `expectedMinionName`.
264
335
  *
265
- * Throws (typed authentication error) only when verification itself
266
- * fails — invalid signature, malformed payload, wrong issuer/audience.
336
+ * A LATE tick is returned with its lateness, exactly like `verifyAndConvert` (#2212). Throws (typed
337
+ * authentication error) only when the delivery is not authentic — invalid signature, malformed
338
+ * payload, wrong issuer/audience, a future occurrence.
267
339
  *
268
340
  * The minion-side consumer passes its compiled-in `minionName` (from
269
341
  * `defineMinionInit({ minionName })`, cross-validated against the fetched
@@ -274,7 +346,7 @@ let ScheduledMessageVerifierBackendService = class ScheduledMessageVerifierBacke
274
346
  */
275
347
  async verifyAndExtractMinionTick(token, expectedMinionName) {
276
348
  const result = await this.verifyScheduledMessage(token);
277
- if (!result.isValid || !result.token) {
349
+ if (result.verdict === ScheduledDeliveryVerdict.REJECTED) {
278
350
  throw this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {
279
351
  customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,
280
352
  context: {
@@ -298,7 +370,9 @@ let ScheduledMessageVerifierBackendService = class ScheduledMessageVerifierBacke
298
370
  });
299
371
  return null;
300
372
  }
301
- return result.token;
373
+ return result.verdict === ScheduledDeliveryVerdict.LATE
374
+ ? { verdict: ScheduledDeliveryVerdict.LATE, value: result.token, lateness: result.lateness }
375
+ : { verdict: ScheduledDeliveryVerdict.ADMISSIBLE, value: result.token };
302
376
  }
303
377
  };
304
378
  ScheduledMessageVerifierBackendService = __decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"scheduled-message-verifier.backend.service.js","sourceRoot":"","sources":["../../../../src/queue/scheduled-message-verifier.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;;;;;;;;;;;;;AAEH,OAAO,EAAE,sCAAsC,EAAE,0CAA0C,EAAE,MAAM,uBAAuB,CAAC;AAC3H,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gDAAgD,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,8DAA8D,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,8DAA8D,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,wEAAwE,CAAC;AACvG,OAAO,EAAE,0BAA0B,EAAE,MAAM,kEAAkE,CAAC;AAC9G,OAAO,EAEL,OAAO,EACP,YAAY,EACZ,WAAW,EACX,SAAS,EACT,2BAA2B,EAC3B,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,mBAAmB,EAAE,MAAM,8EAA8E,CAAC;AACnH;;GAEG;AACH,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,oCAAoC;IACpC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IAExB,wCAAwC;IACxC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE;IAEzB,mFAAmF;IACnF,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAExC,2CAA2C;IAC3C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IAEf,wCAAwC;IACxC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IAEf,2DAA2D;IAC3D,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IAEf,yCAAyC;IACzC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;CAChB,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,2BAA2B,GAAG,+BAA+B,CAAC,MAAM,CAAC;IACzE,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;IAC1C,yHAAyH;IACzH,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACvC,6BAA6B;IAC7B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,gDAAgD;IAChD,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,wBAAwB,GAAG,+BAA+B,CAAC,MAAM,CAAC;IACtE,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC;IACvC,yNAAyN;IACzN,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,0DAA0D;IAC1D,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,uBAAuB,GAAG,CAAC,CAAC,kBAAkB,CAAC,SAAS,EAAE;IAC9D,2BAA2B;IAC3B,wBAAwB;CACzB,CAAC,CAAC;AAqBI,IAAM,sCAAsC,GAA5C,MAAM,sCAAsC;IAK9B;IAEA;IAEA;IART,QAAQ,CAA+D;IAEjF,YAEmB,UAA6B,EAE7B,MAAqB,EAErB,YAAwC;QAJxC,eAAU,GAAV,UAAU,CAAmB;QAE7B,WAAM,GAAN,MAAM,CAAe;QAErB,iBAAY,GAAZ,YAAY,CAA4B;QAEzD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,wCAAwC,CAAC,CAAC;QAClF,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,wCAAwC,EAAE,KAAK,CAAC,CAAC;IACnF,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,sBAAsB,CAAC,KAAa;QACxC,IAAI,CAAC;YACH,+DAA+D;YAC/D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAC1C,KAAK,EACL,aAAa,CAAC,iCAAiC,CAChD,CAAC;YAEF,uCAAuC;YACvC,MAAM,WAAW,GAAG,uBAAuB,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;YAE/D,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;gBACzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,+CAA+C,EAAE;oBAChE,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC,MAAM;iBACjC,CAAC,CAAC;gBACH,OAAO;oBACL,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE,yBAAyB;iBACjC,CAAC;YACJ,CAAC;YAED,MAAM,YAAY,GAAG,WAAW,CAAC,IAAI,CAAC;YACtC,IAAI,YAAY,CAAC,WAAW,EAAE,CAAC;gBAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;gBAC9D,IAAI,GAAG,GAAG,sCAAsC,IAAI,GAAG,GAAG,CAAC,0CAA0C,EAAE,CAAC;oBACtG,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,8DAA8D,EAAE,CAAC;gBACnG,CAAC;YACH,CAAC;YAGD,6EAA6E;YAC7E,4DAA4D;YAC5D,iFAAiF;YACjF,6DAA6D;YAC7D,iEAAiE;YACjE,kEAAkE;YAClE,0BAA0B;YAE1B,IAAI,CAAC,QAAQ,CAAC,yCAAyC,EAAE;gBACvD,KAAK,EAAE,YAAY,CAAC,KAAK;gBACzB,OAAO,EAAE,YAAY,CAAC,OAAO;gBAC7B,aAAa,EAAE,YAAY,CAAC,aAAa;gBACzC,GAAG,CAAC,YAAY,CAAC,OAAO,KAAK,OAAO,CAAC,cAAc;oBACjD,CAAC,CAAC;wBACE,WAAW,EAAE,CAAC,CAAC,YAAY,CAAC,QAAQ;wBACpC,mBAAmB,EAAE,CAAC,CAAC,YAAY,CAAC,gBAAgB;qBACrD;oBACH,CAAC,CAAC,EAAE,UAAU,EAAE,YAAY,CAAC,UAAU,EAAE,CAAC;aAC7C,CAAC,CAAC;YAEH,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,KAAK,EAAE,YAAY;aACpB,CAAC;QAEJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,4FAA4F;YAC5F,8EAA8E;YAC9E,8FAA8F;YAC9F,4FAA4F;YAC5F,wEAAwE;YACxE,EAAE;YACF,4EAA4E;YAC5E,qFAAqF;YACrF,kFAAkF;YAClF,qFAAqF;YACrF,oFAAoF;YACpF,wFAAwF;YACxF,gEAAgE;YAChE,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC;gBACvC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO;uBAClB,CAAC,KAAK,CAAC,OAAO,EAAE,YAAY;wBAC7B,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,eAAe,IAAI,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE;wBAChG,CAAC,CAAC,SAAS,CAAC;uBACX,KAAK,CAAC,IAAI,CAAC;gBAClB,CAAC,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC;YAC/B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,uCAAuC,EAAE;gBACxD,KAAK,EAAE,MAAM;gBACb,+EAA+E;gBAC/E,+EAA+E;gBAC/E,qEAAqE;gBACrE,GAAG,CAAC,mBAAmB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,YAAY;oBAC3D,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,eAAe,EAAE;oBAC9F,CAAC,CAAC,EAAE,CAAC;aACR,CAAC,CAAC;YAEH,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,MAAM;aACd,CAAC;QACJ,CAAC;IACH,CAAC;IAED;;;;;;;;OAQG;IACH,mBAAmB,CAAC,KAA4B;QAC9C,kGAAkG;QAClG,gGAAgG;QAChG,iGAAiG;QACjG,gGAAgG;QAChG,kEAAkE;QAClE,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,OAAO,EAAE,OAAO,CAAC,cAAc;YAC/B,aAAa,EAAE,KAAK,CAAC,aAAa;YAElC,8DAA8D;YAC9D,8FAA8F;YAC9F,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;YACxC,QAAQ,EAAE,KAAK,CAAC,QAAQ;YAExB,aAAa,EAAE,KAAK,CAAC,aAAa;YAElC,WAAW,EAAE,YAAY,CAAC,OAAO;YACjC,QAAQ,EAAE,WAAW,CAAC,MAAM;YAC5B,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,CAAC;YACb,2EAA2E;YAC3E,8EAA8E;YAC9E,SAAS,EAAE,MAAM;YAEjB,SAAS,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC;YACrC,sEAAsE;YACtE,sEAAsE;YACtE,WAAW,EAAE,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;SACvF,CAAC;IACJ,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,gBAAgB,CAAC,KAAa;QAClC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC;QAExD,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACrC,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,CAChC,SAAS,CAAC,cAAc,EACxB,SAAS,EACT;gBACE,sBAAsB,EAAE,2BAA2B,CAAC,4BAA4B;gBAChF,OAAO,EAAE;oBACP,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,uCAAuC;oBAC/D,MAAM,EAAE,0BAA0B;iBACnC;aACF,CACF,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC;YACpD,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,CAChC,SAAS,CAAC,cAAc,EACxB,SAAS,EACT;gBACE,sBAAsB,EAAE,2BAA2B,CAAC,4BAA4B;gBAChF,OAAO,EAAE;oBACP,MAAM,EACJ,oDAAoD,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI;wBAC5E,4EAA4E;wBAC5E,6FAA6F;oBAC/F,MAAM,EAAE,0BAA0B;iBACnC;aACF,CACF,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,CAAC,0BAA0B,CAC9B,KAAa,EACb,kBAA0B;QAE1B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC;QAExD,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACrC,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,CAChC,SAAS,CAAC,cAAc,EACxB,SAAS,EACT;gBACE,sBAAsB,EAAE,2BAA2B,CAAC,4BAA4B;gBAChF,OAAO,EAAE;oBACP,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,uCAAuC;oBAC/D,MAAM,EAAE,0BAA0B;iBACnC;aACF,CACF,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC;YACjD,IAAI,CAAC,QAAQ,CAAC,4CAA4C,EAAE;gBAC1D,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK;gBACzB,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO;aAC9B,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,MAAM,CAAC,KAAK,CAAC,UAAU,KAAK,kBAAkB,EAAE,CAAC;YACnD,IAAI,CAAC,QAAQ,CAAC,qDAAqD,EAAE;gBACnE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK;gBACzB,gBAAgB,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU;gBACzC,kBAAkB;aACnB,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC;CACF,CAAA;AAvQY,sCAAsC;IADlD,UAAU,EAAE;IAKR,WAAA,MAAM,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,CAAA;IAE5C,WAAA,MAAM,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAA;IAEjC,WAAA,MAAM,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,CAAA;qCAHlB,iBAAiB;QAErB,aAAa;QAEP,0BAA0B;GAThD,sCAAsC,CAuQlD","sourcesContent":["/**\n * Scheduled Message Verifier Backend Service\n *\n * Verifies JWT signatures on scheduled job messages from crontabs-batches-manager.\n * Only messages signed by the platform's crontabs-batches-manager are accepted.\n *\n * This prevents unauthorized job execution in horizontally scaled K8s environments.\n */\n\nimport { SCHEDULED_INITIAL_ADMISSION_MAX_AGE_MS, SCHEDULED_INITIAL_ADMISSION_FUTURE_SKEW_MS } from '@wildo-ai/saas-models';\nimport { injectable, inject } from 'inversify';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport { LoggerService } from '~wildo-backend-lib/monitoring/logger/services/logger.service';\nimport { JwtBackendService } from '~wildo-backend-lib/security/jwt/services/jwt.backend.service';\nimport { JwtPublicKeys } from '~wildo-backend-lib/security/authentications/models/jwt.backend.schemas';\nimport { ErrorBuilderBackendService } from '~wildo-backend-lib/errors/services/error-builder.backend.service';\nimport {\n JobPayload,\n JobType,\n JobInitiator,\n JobPriority,\n ErrorType,\n ErrorCustomMessageReference,\n isWildoBackendError,\n} from '@wildo-ai/saas-models';\nimport { z } from 'zod';\n\nimport { describeCaughtError } from '~wildo-backend-lib/errors/utils/error-log-level-classification.backend.utils';\n/**\n * Common JWT envelope fields shared by every scheduled-job token shape.\n */\nconst ScheduledJobTokenEnvelopeSchema = z.object({\n /** Job ID (unique per execution) */\n jobId: z.string().uuid(),\n\n /** Application ID the job belongs to */\n applicationId: z.string(),\n\n /** Scheduled occurrence time; manual invocations intentionally omit this claim. */\n scheduledAt: z.iso.datetime().optional(),\n\n /** Token issued at (standard JWT claim) */\n iat: z.number(),\n\n /** Token expiry (standard JWT claim) */\n exp: z.number(),\n\n /** Issuer (should be PLATFORM_CRONTABS_BATCHES_MANAGER) */\n iss: z.string(),\n\n /** Audience (should be SCHEDULED_JOB) */\n aud: z.string(),\n});\n\n/**\n * Schema for the SCHEDULED_CRON arm of the scheduled-job token (the\n * pre-existing resource-operation / custom-batch tick).\n */\nconst ScheduledCronJobTokenSchema = ScheduledJobTokenEnvelopeSchema.extend({\n jobType: z.literal(JobType.SCHEDULED_CRON),\n /** Operation path key for resource operations (format: \"resource | operation | variantType | variant | isBulk:false\") */\n operationPathKey: z.string().optional(),\n /** Custom batch reference */\n batchRef: z.string().optional(),\n /** Input data for the job (JSON stringified) */\n inputDataJson: z.string().optional(),\n});\n\n/**\n * Schema for the MINION_TICK arm of the scheduled-job token (KD-12).\n * Carries the minion identity discriminator that the minion runtime uses\n * to filter inbound ticks.\n */\nconst MinionTickJobTokenSchema = ScheduledJobTokenEnvelopeSchema.extend({\n jobType: z.literal(JobType.MINION_TICK),\n /** Minion name — matches `wildo.saas.config.ts > minions.<key>`, which the minion runtime knows via its compiled-in `defineMinionInit({ minionName })` identity (NOT read from the `APP_OWNED_SERVICE_NAME` env var). */\n minionName: z.string().min(1),\n /** Optional input data for the tick (JSON stringified) */\n inputDataJson: z.string().optional(),\n});\n\n/**\n * Discriminated union over the two scheduled-job token shapes. Per KD-12,\n * dispatch is a pure switch on `jobType` rather than \"which optional\n * field is set\".\n */\nconst ScheduledJobTokenSchema = z.discriminatedUnion('jobType', [\n ScheduledCronJobTokenSchema,\n MinionTickJobTokenSchema,\n]);\n\nexport type ScheduledJobToken = z.infer<typeof ScheduledJobTokenSchema>;\nexport type ScheduledCronJobToken = z.infer<typeof ScheduledCronJobTokenSchema>;\nexport type MinionTickJobToken = z.infer<typeof MinionTickJobTokenSchema>;\n\n/**\n * Result of scheduled message verification.\n */\nexport interface ScheduledMessageVerificationResult {\n /** Whether the message is valid */\n isValid: boolean;\n\n /** The verified token payload (if valid) */\n token?: ScheduledJobToken;\n\n /** Error message (if invalid) */\n error?: string;\n}\n\n@injectable()\nexport class ScheduledMessageVerifierBackendService {\n protected logDebug: (message: string, context?: Record<string, unknown>) => void;\n\n constructor(\n @inject(SAAS_SERVICE_TYPES.JwtBackendService)\n private readonly jwtService: JwtBackendService,\n @inject(SAAS_SERVICE_TYPES.Logger)\n private readonly logger: LoggerService,\n @inject(SAAS_SERVICE_TYPES.ErrorBuilderService)\n private readonly errorBuilder: ErrorBuilderBackendService,\n ) {\n this.logDebug = this.logger.debugByType('ScheduledMessageVerifierBackendService');\n this.logger.setDebugTypeLogging('ScheduledMessageVerifierBackendService', false);\n }\n\n /**\n * Verify a scheduled job message token.\n *\n * @param token - The JWT token from the message\n * @returns Verification result with token payload if valid\n */\n async verifyScheduledMessage(token: string): Promise<ScheduledMessageVerificationResult> {\n try {\n // Verify the JWT using the crontabs-batches-manager public key\n const decoded = await this.jwtService.verify(\n token,\n JwtPublicKeys.PLATFORM_CRONTABS_BATCHES_MANAGER\n );\n\n // Parse and validate the token payload\n const parseResult = ScheduledJobTokenSchema.safeParse(decoded);\n\n if (!parseResult.success) {\n this.logger.warn('Scheduled message token has invalid structure', {\n issues: parseResult.error.issues,\n });\n return {\n isValid: false,\n error: 'Invalid token structure',\n };\n }\n\n const tokenPayload = parseResult.data;\n if (tokenPayload.scheduledAt) {\n const age = Date.now() - Date.parse(tokenPayload.scheduledAt);\n if (age > SCHEDULED_INITIAL_ADMISSION_MAX_AGE_MS || age < -SCHEDULED_INITIAL_ADMISSION_FUTURE_SKEW_MS) {\n return { isValid: false, error: 'Scheduled occurrence is outside the initial admission window' };\n }\n }\n\n\n // Note: issuer + audience are already enforced by `JwtBackendService.verify`\n // via `JwtVerifyOptions[PLATFORM_CRONTABS_BATCHES_MANAGER]`\n // (`issuer: 'platform_crontabs_batches_manager'` + `audience: 'SCHEDULED_JOB'`).\n // Any token that reaches this point passed those checks; the\n // post-verify uppercase-issuer / audience checks that previously\n // lived here were dead code and have been removed (L5 fix per the\n // implementation review).\n\n this.logDebug('Scheduled message verified successfully', {\n jobId: tokenPayload.jobId,\n jobType: tokenPayload.jobType,\n applicationId: tokenPayload.applicationId,\n ...(tokenPayload.jobType === JobType.SCHEDULED_CRON\n ? {\n hasBatchRef: !!tokenPayload.batchRef,\n hasOperationPathKey: !!tokenPayload.operationPathKey,\n }\n : { minionName: tokenPayload.minionName }),\n });\n\n return {\n isValid: true,\n token: tokenPayload,\n };\n\n } catch (error) {\n // Surface the REAL reason. A `WildoBackendError` is NOT `instanceof Error`, so the previous\n // `... : 'Unknown error'` guard MASKED the actual cause (e.g. an empty/absent\n // PLATFORM_CRONTABS_BATCHES_MANAGER verify key vs a genuine signature mismatch) — which hid a\n // whole-platform \"every scheduled tick dead-letters\" bug for weeks. Never swallow it again.\n // The unmask must reach the JWT diagnostic, not stop at the error TYPE.\n //\n // `verifyRaw` builds its AUTHENTICATION error with `context.jwtErrorName` +\n // `context.jwtErrorMessage` and NO `context.message`, so a `context.message ?? type`\n // fallback printed a bare \"AUTHENTICATION\" and discarded the only fields that say\n // WHY — expired vs bad signature vs wrong audience. That is the same swallowing this\n // block was written to end, one level deeper: a minion tick that sat in its durable\n // queue past the 5-minute token TTL and a genuinely forged token were indistinguishable\n // in the log, which is why the real cause went unread for days.\n const detail = isWildoBackendError(error)\n ? (error.context?.message\n ?? (error.context?.jwtErrorName\n ? `${String(error.context.jwtErrorName)}: ${String(error.context.jwtErrorMessage ?? '')}`.trim()\n : undefined)\n ?? error.type)\n : describeCaughtError(error);\n this.logger.warn('Scheduled message verification failed', {\n error: detail,\n // Kept as discrete fields too so a log query can facet on the JWT failure kind\n // (`TokenExpiredError` is an operational signal — the consumer was down longer\n // than the token TTL — while `JsonWebTokenError` is a trust signal).\n ...(isWildoBackendError(error) && error.context?.jwtErrorName\n ? { jwtErrorName: error.context.jwtErrorName, jwtErrorMessage: error.context.jwtErrorMessage }\n : {}),\n });\n\n return {\n isValid: false,\n error: detail,\n };\n }\n }\n\n /**\n * Convert a verified `SCHEDULED_CRON` token to a JobPayload for\n * execution by the resource-operation / custom-batch executor pipeline.\n *\n * MINION_TICK conversion belongs to the minion consumer, which uses the\n * same durable coordinator but dispatches a different local handler.\n * The verification entry point refuses those tokens here —\n * see `verifyAndConvert` and `verifyAndExtractMinionTick` below.\n */\n convertToJobPayload(token: ScheduledCronJobToken): JobPayload {\n // No `originCorrelationId` — deliberate, and the one JobPayload writer for which that is correct.\n // A scheduled cron tick is SCHEDULE-originated: no request enqueued it, so there is no upstream\n // trace to join. The consumer mints a fresh id per tick, which is the accurate attribution. (The\n // other two writers — `buildJobPayload` and `enqueueCustomBatch` — both seed it; see the writer\n // enumeration in `.claude/plans/traceability-correlation-id.md`.)\n return {\n jobId: token.jobId,\n jobType: JobType.SCHEDULED_CRON,\n applicationId: token.applicationId,\n\n // operationPathKey is the human-readable key format, NOT JSON\n // The executor will parse it using ResourcesConfig_OperationPathUtils.parseOperationPathKey()\n operationPathKey: token.operationPathKey,\n batchRef: token.batchRef,\n\n inputDataJson: token.inputDataJson,\n\n initiatedBy: JobInitiator.CRONTAB,\n priority: JobPriority.NORMAL,\n retryCount: 0,\n maxRetries: 0,\n // Ordinary queue retry metadata is deliberately absent. Scheduled recovery\n // resolves the local handler contract and persists its own attempts/deadline.\n timeoutMs: 300000,\n\n createdAt: new Date(token.iat * 1000),\n // Occurrence time is not publication time. Preserve the authenticated\n // instant in durable recovery input; manual triggers leave it absent.\n scheduledAt: token.scheduledAt === undefined ? undefined : new Date(token.scheduledAt),\n };\n }\n\n /**\n * Verify and convert a `SCHEDULED_CRON` scheduled message in one step.\n * Throws if verification fails OR if the token is a `MINION_TICK`\n * (which the resource-operation / custom-batch executor cannot\n * dispatch; the minion runtime owns those — use\n * `verifyAndExtractMinionTick`).\n *\n * @param token - The JWT token from the message\n * @returns JobPayload ready for execution\n * @throws WildoError if verification fails or the token targets a minion\n */\n async verifyAndConvert(token: string): Promise<JobPayload> {\n const result = await this.verifyScheduledMessage(token);\n\n if (!result.isValid || !result.token) {\n throw this.errorBuilder.buildError(\n ErrorType.AUTHENTICATION,\n undefined,\n {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n context: {\n reason: result.error || 'Scheduled message verification failed',\n source: 'crontabs-batches-manager',\n },\n }\n );\n }\n\n if (result.token.jobType !== JobType.SCHEDULED_CRON) {\n throw this.errorBuilder.buildError(\n ErrorType.AUTHENTICATION,\n undefined,\n {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n context: {\n reason:\n `verifyAndConvert() received a token with jobType=${result.token.jobType}; ` +\n `the resource-operation / custom-batch executor cannot dispatch this kind. ` +\n `MINION_TICK tokens belong to the minion runtime — use verifyAndExtractMinionTick() instead.`,\n source: 'crontabs-batches-manager',\n },\n }\n );\n }\n\n return this.convertToJobPayload(result.token);\n }\n\n /**\n * Verify a scheduled message and extract a `MinionTickJobToken` if and\n * only if the token targets the given minion (KD-12).\n *\n * Returns `null` (not throws) for the two non-actionable cases that the\n * minion runtime should silently skip:\n * - the token is a `SCHEDULED_CRON` (belongs to the backend-api),\n * - the token is a `MINION_TICK` whose `minionName` does not match\n * `expectedMinionName`.\n *\n * Throws (typed authentication error) only when verification itself\n * fails — invalid signature, malformed payload, wrong issuer/audience.\n *\n * The minion-side consumer passes its compiled-in `minionName` (from\n * `defineMinionInit({ minionName })`, cross-validated against the fetched\n * `appConfig.minions[minionName]`) as `expectedMinionName` — NOT an\n * env-projected `APP_OWNED_SERVICE_NAME`; tokens that fail the filter\n * should be NACK'd back to the queue (or skipped, depending on the\n * queue-routing strategy chosen for the minion fan-out).\n */\n async verifyAndExtractMinionTick(\n token: string,\n expectedMinionName: string,\n ): Promise<MinionTickJobToken | null> {\n const result = await this.verifyScheduledMessage(token);\n\n if (!result.isValid || !result.token) {\n throw this.errorBuilder.buildError(\n ErrorType.AUTHENTICATION,\n undefined,\n {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n context: {\n reason: result.error || 'Scheduled message verification failed',\n source: 'crontabs-batches-manager',\n },\n }\n );\n }\n\n if (result.token.jobType !== JobType.MINION_TICK) {\n this.logDebug('Skipping non-minion-tick scheduled message', {\n jobId: result.token.jobId,\n jobType: result.token.jobType,\n });\n return null;\n }\n\n if (result.token.minionName !== expectedMinionName) {\n this.logDebug('Skipping minion tick targeted at a different minion', {\n jobId: result.token.jobId,\n targetMinionName: result.token.minionName,\n expectedMinionName,\n });\n return null;\n }\n\n return result.token;\n }\n}\n"]}
1
+ {"version":3,"file":"scheduled-message-verifier.backend.service.js","sourceRoot":"","sources":["../../../../src/queue/scheduled-message-verifier.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scheduled Message Verifier Backend Service\n *\n * Verifies JWT signatures on scheduled job messages from crontabs-batches-manager.\n * Only messages signed by the platform's crontabs-batches-manager are accepted.\n *\n * This prevents unauthorized job execution in horizontally scaled K8s environments.\n */\n\nimport { SCHEDULED_INITIAL_ADMISSION_MAX_AGE_MS, SCHEDULED_INITIAL_ADMISSION_FUTURE_SKEW_MS, SCHEDULED_LATE_DELIVERY_HORIZON_MS } from '@wildo-ai/saas-models';\nimport { injectable, inject } from 'inversify';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport { LoggerService } from '~wildo-backend-lib/monitoring/logger/services/logger.service';\nimport { JwtBackendService } from '~wildo-backend-lib/security/jwt/services/jwt.backend.service';\nimport { JwtPublicKeys } from '~wildo-backend-lib/security/authentications/models/jwt.backend.schemas';\nimport { ErrorBuilderBackendService } from '~wildo-backend-lib/errors/services/error-builder.backend.service';\nimport {\n JobPayload,\n JobType,\n JobInitiator,\n JobPriority,\n ErrorType,\n ErrorCustomMessageReference,\n isWildoBackendError,\n ScheduledDeliveryLateness,\n} from '@wildo-ai/saas-models';\nimport { z } from 'zod';\n\nimport { describeCaughtError } from '~wildo-backend-lib/errors/utils/error-log-level-classification.backend.utils';\n/**\n * Common JWT envelope fields shared by every scheduled-job token shape.\n */\nconst ScheduledJobTokenEnvelopeSchema = z.object({\n /** Job ID (unique per execution) */\n jobId: z.string().uuid(),\n\n /** Application ID the job belongs to */\n applicationId: z.string(),\n\n /** Scheduled occurrence time; manual invocations intentionally omit this claim. */\n scheduledAt: z.iso.datetime().optional(),\n\n /** Token issued at (standard JWT claim) */\n iat: z.number(),\n\n /** Token expiry (standard JWT claim) */\n exp: z.number(),\n\n /** Issuer (should be PLATFORM_CRONTABS_BATCHES_MANAGER) */\n iss: z.string(),\n\n /** Audience (should be SCHEDULED_JOB) */\n aud: z.string(),\n});\n\n/**\n * Schema for the SCHEDULED_CRON arm of the scheduled-job token (the\n * pre-existing resource-operation / custom-batch tick).\n */\nconst ScheduledCronJobTokenSchema = ScheduledJobTokenEnvelopeSchema.extend({\n jobType: z.literal(JobType.SCHEDULED_CRON),\n /** Operation path key for resource operations (format: \"resource | operation | variantType | variant | isBulk:false\") */\n operationPathKey: z.string().optional(),\n /** Custom batch reference */\n batchRef: z.string().optional(),\n /** Input data for the job (JSON stringified) */\n inputDataJson: z.string().optional(),\n});\n\n/**\n * Schema for the MINION_TICK arm of the scheduled-job token (KD-12).\n * Carries the minion identity discriminator that the minion runtime uses\n * to filter inbound ticks.\n */\nconst MinionTickJobTokenSchema = ScheduledJobTokenEnvelopeSchema.extend({\n jobType: z.literal(JobType.MINION_TICK),\n /** Minion name — matches `wildo.saas.config.ts > minions.<key>`, which the minion runtime knows via its compiled-in `defineMinionInit({ minionName })` identity (NOT read from the `APP_OWNED_SERVICE_NAME` env var). */\n minionName: z.string().min(1),\n /** Optional input data for the tick (JSON stringified) */\n inputDataJson: z.string().optional(),\n});\n\n/**\n * Discriminated union over the two scheduled-job token shapes. Per KD-12,\n * dispatch is a pure switch on `jobType` rather than \"which optional\n * field is set\".\n */\nconst ScheduledJobTokenSchema = z.discriminatedUnion('jobType', [\n ScheduledCronJobTokenSchema,\n MinionTickJobTokenSchema,\n]);\n\nexport type ScheduledJobToken = z.infer<typeof ScheduledJobTokenSchema>;\nexport type ScheduledCronJobToken = z.infer<typeof ScheduledCronJobTokenSchema>;\nexport type MinionTickJobToken = z.infer<typeof MinionTickJobTokenSchema>;\n\n/**\n * What a scheduled delivery IS, decided before anything durable happens (#2212).\n *\n * Three answers, not two, because \"not admissible\" hid two opposite situations behind one\n * `AUTHENTICATION` error: a genuine token that waited out its window in a queue nobody consumed\n * (operational — the application was down) and a token that cannot be proven genuine (a trust\n * signal). Logged identically, 139 of the first per restart read as a credential outage and sent the\n * diagnosis to the wrong service.\n */\nexport enum ScheduledDeliveryVerdict {\n /** Authentic, inside its admission window: admit it. */\n ADMISSIBLE = 'admissible',\n /** Authentic (signature, issuer, audience, structure all proven) but past its admission deadline: the occurrence was MISSED. */\n LATE = 'late',\n /** Not provably authentic, or it names an occurrence in the future: a trust failure. */\n REJECTED = 'rejected',\n}\n\n/** Result of scheduled message verification. */\nexport type ScheduledMessageVerificationResult =\n | { verdict: ScheduledDeliveryVerdict.ADMISSIBLE; token: ScheduledJobToken }\n | { verdict: ScheduledDeliveryVerdict.LATE; token: ScheduledJobToken; lateness: ScheduledDeliveryLateness }\n | { verdict: ScheduledDeliveryVerdict.REJECTED; error: string };\n\n/**\n * An authentic delivery, converted for its consumer. Only the two authentic verdicts are\n * representable here; a rejection is thrown by the `verifyAnd…` entry points.\n */\nexport type AuthenticScheduledDelivery<T> =\n | { verdict: ScheduledDeliveryVerdict.ADMISSIBLE; value: T }\n | { verdict: ScheduledDeliveryVerdict.LATE; value: T; lateness: ScheduledDeliveryLateness };\n\n@injectable()\nexport class ScheduledMessageVerifierBackendService {\n protected logDebug: (message: string, context?: Record<string, unknown>) => void;\n\n constructor(\n @inject(SAAS_SERVICE_TYPES.JwtBackendService)\n private readonly jwtService: JwtBackendService,\n @inject(SAAS_SERVICE_TYPES.Logger)\n private readonly logger: LoggerService,\n @inject(SAAS_SERVICE_TYPES.ErrorBuilderService)\n private readonly errorBuilder: ErrorBuilderBackendService,\n ) {\n this.logDebug = this.logger.debugByType('ScheduledMessageVerifierBackendService');\n this.logger.setDebugTypeLogging('ScheduledMessageVerifierBackendService', false);\n }\n\n /**\n * Classify a scheduled job message token: admissible, late, or rejected.\n *\n * Only a REJECTED verdict logs here (warn), because only it is a trust signal. A LATE delivery is\n * reported by the consumer's runner as one digest per drain, not once per message.\n *\n * @param token - The JWT token from the message\n */\n async verifyScheduledMessage(token: string): Promise<ScheduledMessageVerificationResult> {\n let decoded: unknown;\n let lateness: ScheduledDeliveryLateness | undefined;\n try {\n // Verify the JWT using the crontabs-batches-manager public key\n decoded = await this.jwtService.verify(\n token,\n JwtPublicKeys.PLATFORM_CRONTABS_BATCHES_MANAGER\n );\n } catch (error) {\n const expired = await this.authenticatedExpiredPayload(token);\n // When the first failure was the clock and the re-verification refused, the REAL reason is the\n // re-verification's (a wrong audience behind an expiry, say): logging \"jwt expired\" there would\n // be the exact misdirection #2212 exists to remove, aimed the other way.\n if ('refused' in expired) return this.rejected(expired.refused ?? error);\n decoded = expired.payload;\n lateness = ScheduledDeliveryLateness.TOKEN_EXPIRED;\n }\n\n // Parse and validate the token payload\n const parseResult = ScheduledJobTokenSchema.safeParse(decoded);\n\n if (!parseResult.success) {\n this.logger.warn('Scheduled message token has invalid structure', {\n issues: parseResult.error.issues,\n });\n return { verdict: ScheduledDeliveryVerdict.REJECTED, error: 'Invalid token structure' };\n }\n\n const tokenPayload = parseResult.data;\n if (tokenPayload.scheduledAt) {\n const age = Date.now() - Date.parse(tokenPayload.scheduledAt);\n // A FUTURE occurrence is not lateness: a signer whose clock runs ahead, or a token minted for\n // an instant that has not happened, is a trust question and stays a rejection.\n if (age < -SCHEDULED_INITIAL_ADMISSION_FUTURE_SKEW_MS) {\n return this.rejected(undefined, 'Scheduled occurrence is outside the initial admission window (in the future)');\n }\n // An occurrence older than the window is LATE, not a trust failure: the token is authentic and\n // current, and the delay is shared between the scheduler's signing and this application's\n // consumption (`ScheduledDeliveryLateness.OUTSIDE_ADMISSION_WINDOW`). Decided with #2212, whose\n // first wording named only the expired-token case: reporting a genuine, merely late token as an\n // authentication failure is the misdirection that issue removes, whichever gate fires.\n if (age > SCHEDULED_INITIAL_ADMISSION_MAX_AGE_MS) lateness ??= ScheduledDeliveryLateness.OUTSIDE_ADMISSION_WINDOW;\n }\n\n // Note: issuer + audience are already enforced by `JwtBackendService.verify`\n // via `JwtVerifyOptions[PLATFORM_CRONTABS_BATCHES_MANAGER]`\n // (`issuer: 'platform_crontabs_batches_manager'` + `audience: 'SCHEDULED_JOB'`), and by the\n // expiry-ignoring re-verification in `authenticatedExpiredPayload` for a late token.\n\n if (lateness !== undefined) {\n return { verdict: ScheduledDeliveryVerdict.LATE, token: tokenPayload, lateness };\n }\n\n this.logDebug('Scheduled message verified successfully', {\n jobId: tokenPayload.jobId,\n jobType: tokenPayload.jobType,\n applicationId: tokenPayload.applicationId,\n ...(tokenPayload.jobType === JobType.SCHEDULED_CRON\n ? {\n hasBatchRef: !!tokenPayload.batchRef,\n hasOperationPathKey: !!tokenPayload.operationPathKey,\n }\n : { minionName: tokenPayload.minionName }),\n });\n\n return { verdict: ScheduledDeliveryVerdict.ADMISSIBLE, token: tokenPayload };\n }\n\n /**\n * The payload of a token whose ONLY defect is expiry — or, when it is not provably that, why.\n *\n * `jsonwebtoken` checks expiry before audience and issuer, so a `TokenExpiredError` alone proves\n * the signature and nothing more. Re-verifying with expiry ignored — quietly, through the same key\n * set, rotation included — proves issuer and audience too, and the `exp` check below confirms the\n * first failure really was the clock. Any other first failure (bad signature, wrong audience, an\n * unknown key) fails this re-verification and stays a rejection. The late payload is never ADMITTED,\n * so this cannot turn an expired token into a run; it only decides how the miss is reported.\n *\n * A token that expired longer ago than `SCHEDULED_LATE_DELIVERY_HORIZON_MS` is refused too: no\n * genuine backlog is that old, and an authentic-but-ancient token is what a replay looks like.\n *\n * `refused` carries the re-verification's own error when it threw, and is `undefined` when the\n * token re-verified but is not a recordable late delivery (not expired, or past the horizon).\n */\n private async authenticatedExpiredPayload(token: string): Promise<{ payload: unknown } | { refused: unknown }> {\n let decoded: unknown;\n try {\n decoded = await this.jwtService.verifyWithoutBackendErrorActions(\n token, JwtPublicKeys.PLATFORM_CRONTABS_BATCHES_MANAGER, { ignoreExpiration: true });\n } catch (error) {\n return { refused: error };\n }\n const exp = (decoded as { exp?: unknown } | null)?.exp;\n if (typeof exp !== 'number') return { refused: undefined };\n const expiredForMs = Date.now() - exp * 1000;\n if (expiredForMs < 0) return { refused: undefined };\n if (expiredForMs > SCHEDULED_LATE_DELIVERY_HORIZON_MS) {\n return { refused: this.errorBuilder.buildError(ErrorType.AUTHENTICATION, undefined, {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n context: { message: `Scheduled delivery token expired ${Math.round(expiredForMs / 86400000)} day(s) ago, beyond the late-delivery horizon` },\n }) };\n }\n return { payload: decoded };\n }\n\n private rejected(error: unknown, reason?: string): ScheduledMessageVerificationResult {\n // Surface the REAL reason. A `WildoBackendError` is NOT `instanceof Error`, so the previous\n // `... : 'Unknown error'` guard MASKED the actual cause (e.g. an empty/absent\n // PLATFORM_CRONTABS_BATCHES_MANAGER verify key vs a genuine signature mismatch) — which hid a\n // whole-platform \"every scheduled tick dead-letters\" bug for weeks. Never swallow it again.\n // The unmask must reach the JWT diagnostic, not stop at the error TYPE.\n //\n // `verifyRaw` builds its AUTHENTICATION error with `context.jwtErrorName` +\n // `context.jwtErrorMessage` and NO `context.message`, so a `context.message ?? type`\n // fallback printed a bare \"AUTHENTICATION\" and discarded the only fields that say\n // WHY — bad signature vs wrong audience vs unknown key.\n const detail = reason ?? (isWildoBackendError(error)\n ? (error.context?.message\n ?? (error.context?.jwtErrorName\n ? `${String(error.context.jwtErrorName)}: ${String(error.context.jwtErrorMessage ?? '')}`.trim()\n : undefined)\n ?? error.type)\n : describeCaughtError(error));\n this.logger.warn('Scheduled message verification failed', {\n error: detail,\n // Kept as discrete fields too so a log query can facet on the JWT failure kind. An expired\n // but authentic token within the horizon never reaches here — it is a LATE verdict (#2212) —\n // so what lands here is a trust signal, named by the check that actually refused it.\n ...(isWildoBackendError(error) && error.context?.jwtErrorName\n ? { jwtErrorName: error.context.jwtErrorName, jwtErrorMessage: error.context.jwtErrorMessage }\n : {}),\n });\n\n return { verdict: ScheduledDeliveryVerdict.REJECTED, error: detail };\n }\n\n /**\n * Convert a verified `SCHEDULED_CRON` token to a JobPayload for\n * execution by the resource-operation / custom-batch executor pipeline.\n *\n * MINION_TICK conversion belongs to the minion consumer, which uses the\n * same durable coordinator but dispatches a different local handler.\n * The verification entry point refuses those tokens here —\n * see `verifyAndConvert` and `verifyAndExtractMinionTick` below.\n */\n convertToJobPayload(token: ScheduledCronJobToken): JobPayload {\n // No `originCorrelationId` — deliberate, and the one JobPayload writer for which that is correct.\n // A scheduled cron tick is SCHEDULE-originated: no request enqueued it, so there is no upstream\n // trace to join. The consumer mints a fresh id per tick, which is the accurate attribution. (The\n // other two writers — `buildJobPayload` and `enqueueCustomBatch` — both seed it; see the writer\n // enumeration in `.claude/plans/traceability-correlation-id.md`.)\n return {\n jobId: token.jobId,\n jobType: JobType.SCHEDULED_CRON,\n applicationId: token.applicationId,\n\n // operationPathKey is the human-readable key format, NOT JSON\n // The executor will parse it using ResourcesConfig_OperationPathUtils.parseOperationPathKey()\n operationPathKey: token.operationPathKey,\n batchRef: token.batchRef,\n\n inputDataJson: token.inputDataJson,\n\n initiatedBy: JobInitiator.CRONTAB,\n priority: JobPriority.NORMAL,\n retryCount: 0,\n maxRetries: 0,\n // Ordinary queue retry metadata is deliberately absent. Scheduled recovery\n // resolves the local handler contract and persists its own attempts/deadline.\n timeoutMs: 300000,\n\n createdAt: new Date(token.iat * 1000),\n // Occurrence time is not publication time. Preserve the authenticated\n // instant in durable recovery input; manual triggers leave it absent.\n scheduledAt: token.scheduledAt === undefined ? undefined : new Date(token.scheduledAt),\n };\n }\n\n /**\n * Verify and convert a `SCHEDULED_CRON` scheduled message in one step.\n *\n * Returns the converted payload with its verdict — ADMISSIBLE, or LATE with why — and throws only\n * when the delivery is not authentic, or is a `MINION_TICK` (which the resource-operation /\n * custom-batch executor cannot dispatch; the minion runtime owns those — use\n * `verifyAndExtractMinionTick`). A LATE delivery is returned, never thrown: it is genuine, and the\n * consumer records it as a missed occurrence instead of failing it (#2212).\n *\n * @param token - The JWT token from the message\n * @throws WildoError if verification fails or the token targets a minion\n */\n async verifyAndConvert(token: string): Promise<AuthenticScheduledDelivery<JobPayload>> {\n const result = await this.verifyScheduledMessage(token);\n\n if (result.verdict === ScheduledDeliveryVerdict.REJECTED) {\n throw this.errorBuilder.buildError(\n ErrorType.AUTHENTICATION,\n undefined,\n {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n context: {\n reason: result.error || 'Scheduled message verification failed',\n source: 'crontabs-batches-manager',\n },\n }\n );\n }\n\n if (result.token.jobType !== JobType.SCHEDULED_CRON) {\n throw this.errorBuilder.buildError(\n ErrorType.AUTHENTICATION,\n undefined,\n {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n context: {\n reason:\n `verifyAndConvert() received a token with jobType=${result.token.jobType}; ` +\n `the resource-operation / custom-batch executor cannot dispatch this kind. ` +\n `MINION_TICK tokens belong to the minion runtime — use verifyAndExtractMinionTick() instead.`,\n source: 'crontabs-batches-manager',\n },\n }\n );\n }\n\n const value = this.convertToJobPayload(result.token);\n return result.verdict === ScheduledDeliveryVerdict.LATE\n ? { verdict: ScheduledDeliveryVerdict.LATE, value, lateness: result.lateness }\n : { verdict: ScheduledDeliveryVerdict.ADMISSIBLE, value };\n }\n\n /**\n * Verify a scheduled message and extract a `MinionTickJobToken` if and\n * only if the token targets the given minion (KD-12), with its verdict.\n *\n * Returns `null` (not throws) for the two non-actionable cases that the\n * minion runtime should silently skip:\n * - the token is a `SCHEDULED_CRON` (belongs to the backend-api),\n * - the token is a `MINION_TICK` whose `minionName` does not match\n * `expectedMinionName`.\n *\n * A LATE tick is returned with its lateness, exactly like `verifyAndConvert` (#2212). Throws (typed\n * authentication error) only when the delivery is not authentic — invalid signature, malformed\n * payload, wrong issuer/audience, a future occurrence.\n *\n * The minion-side consumer passes its compiled-in `minionName` (from\n * `defineMinionInit({ minionName })`, cross-validated against the fetched\n * `appConfig.minions[minionName]`) as `expectedMinionName` — NOT an\n * env-projected `APP_OWNED_SERVICE_NAME`; tokens that fail the filter\n * should be NACK'd back to the queue (or skipped, depending on the\n * queue-routing strategy chosen for the minion fan-out).\n */\n async verifyAndExtractMinionTick(\n token: string,\n expectedMinionName: string,\n ): Promise<AuthenticScheduledDelivery<MinionTickJobToken> | null> {\n const result = await this.verifyScheduledMessage(token);\n\n if (result.verdict === ScheduledDeliveryVerdict.REJECTED) {\n throw this.errorBuilder.buildError(\n ErrorType.AUTHENTICATION,\n undefined,\n {\n customMessageReference: ErrorCustomMessageReference.AUTHENTICATION_INVALID_TOKEN,\n context: {\n reason: result.error || 'Scheduled message verification failed',\n source: 'crontabs-batches-manager',\n },\n }\n );\n }\n\n if (result.token.jobType !== JobType.MINION_TICK) {\n this.logDebug('Skipping non-minion-tick scheduled message', {\n jobId: result.token.jobId,\n jobType: result.token.jobType,\n });\n return null;\n }\n\n if (result.token.minionName !== expectedMinionName) {\n this.logDebug('Skipping minion tick targeted at a different minion', {\n jobId: result.token.jobId,\n targetMinionName: result.token.minionName,\n expectedMinionName,\n });\n return null;\n }\n\n return result.verdict === ScheduledDeliveryVerdict.LATE\n ? { verdict: ScheduledDeliveryVerdict.LATE, value: result.token, lateness: result.lateness }\n : { verdict: ScheduledDeliveryVerdict.ADMISSIBLE, value: result.token };\n }\n}\n"]}