@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.
- package/dist/esm/data-seeding/models/data-seed.shared.schemas.d.ts +8 -0
- package/dist/esm/data-seeding/models/data-seed.shared.schemas.d.ts.map +1 -1
- package/dist/esm/data-seeding/models/data-seed.shared.schemas.js +17 -0
- package/dist/esm/data-seeding/models/data-seed.shared.schemas.js.map +1 -1
- package/dist/esm/data-seeding/services/data-seed-application-bootstrap.backend.d.ts.map +1 -1
- package/dist/esm/data-seeding/services/data-seed-application-bootstrap.backend.js +9 -1
- package/dist/esm/data-seeding/services/data-seed-application-bootstrap.backend.js.map +1 -1
- package/dist/esm/data-seeding/services/data-seed-environment-admission.backend.d.ts +23 -0
- package/dist/esm/data-seeding/services/data-seed-environment-admission.backend.d.ts.map +1 -0
- package/dist/esm/data-seeding/services/data-seed-environment-admission.backend.js +25 -0
- package/dist/esm/data-seeding/services/data-seed-environment-admission.backend.js.map +1 -0
- package/dist/esm/databases/services/postgresql-migration-runner.backend.d.ts +6 -0
- package/dist/esm/databases/services/postgresql-migration-runner.backend.d.ts.map +1 -1
- package/dist/esm/databases/services/postgresql-migration-runner.backend.js +8 -1
- package/dist/esm/databases/services/postgresql-migration-runner.backend.js.map +1 -1
- package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts +0 -1
- package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts.map +1 -1
- package/dist/esm/external-providers/services/external-providers-registry.backend.service.js +4 -39
- package/dist/esm/external-providers/services/external-providers-registry.backend.service.js.map +1 -1
- package/dist/esm/flows-actors/services/agents.backend.service.d.ts +15 -4
- package/dist/esm/flows-actors/services/agents.backend.service.d.ts.map +1 -1
- package/dist/esm/flows-actors/services/agents.backend.service.js +21 -11
- package/dist/esm/flows-actors/services/agents.backend.service.js.map +1 -1
- package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts +47 -2
- package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts.map +1 -1
- package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js +56 -8
- package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js.map +1 -1
- package/dist/esm/queue/job-consumer.backend.service.d.ts.map +1 -1
- package/dist/esm/queue/job-consumer.backend.service.js +21 -8
- package/dist/esm/queue/job-consumer.backend.service.js.map +1 -1
- package/dist/esm/queue/job-status-tracking.backend.service.d.ts.map +1 -1
- package/dist/esm/queue/job-status-tracking.backend.service.js +4 -1
- package/dist/esm/queue/job-status-tracking.backend.service.js.map +1 -1
- package/dist/esm/queue/queue.backend.service.d.ts +14 -0
- package/dist/esm/queue/queue.backend.service.d.ts.map +1 -1
- package/dist/esm/queue/queue.backend.service.js +68 -12
- package/dist/esm/queue/queue.backend.service.js.map +1 -1
- package/dist/esm/queue/scheduled-evidence-coverage.backend.d.ts +8 -0
- package/dist/esm/queue/scheduled-evidence-coverage.backend.d.ts.map +1 -1
- package/dist/esm/queue/scheduled-evidence-coverage.backend.js +20 -2
- package/dist/esm/queue/scheduled-evidence-coverage.backend.js.map +1 -1
- package/dist/esm/queue/scheduled-execution.backend.service.d.ts +44 -1
- package/dist/esm/queue/scheduled-execution.backend.service.d.ts.map +1 -1
- package/dist/esm/queue/scheduled-execution.backend.service.js +180 -10
- package/dist/esm/queue/scheduled-execution.backend.service.js.map +1 -1
- package/dist/esm/queue/scheduled-message-verifier.backend.service.d.ts +73 -21
- package/dist/esm/queue/scheduled-message-verifier.backend.service.d.ts.map +1 -1
- package/dist/esm/queue/scheduled-message-verifier.backend.service.js +162 -88
- package/dist/esm/queue/scheduled-message-verifier.backend.service.js.map +1 -1
- package/dist/esm/runtime/services/start-application.backend.service.d.ts.map +1 -1
- package/dist/esm/runtime/services/start-application.backend.service.js +16 -1
- package/dist/esm/runtime/services/start-application.backend.service.js.map +1 -1
- package/package.json +10 -10
- package/dist/esm/__tests__/fixture-deep-partial.d.ts +0 -18
- package/dist/esm/__tests__/fixture-deep-partial.d.ts.map +0 -1
- package/dist/esm/__tests__/fixture-deep-partial.js +0 -1
- package/dist/esm/__tests__/fixture-deep-partial.js.map +0 -1
- package/dist/esm/__tests__/integration/redis-integration-gate.d.ts +0 -26
- package/dist/esm/__tests__/integration/redis-integration-gate.d.ts.map +0 -1
- package/dist/esm/__tests__/integration/redis-integration-gate.js +0 -105
- package/dist/esm/__tests__/integration/redis-integration-gate.js.map +0 -1
- package/dist/esm/__tests__/setup.d.ts +0 -7
- package/dist/esm/__tests__/setup.d.ts.map +0 -1
- package/dist/esm/__tests__/setup.js +0 -14
- package/dist/esm/__tests__/setup.js.map +0 -1
- package/dist/esm/__tests__/test-container.d.ts +0 -41
- package/dist/esm/__tests__/test-container.d.ts.map +0 -1
- package/dist/esm/__tests__/test-container.js +0 -98
- package/dist/esm/__tests__/test-container.js.map +0 -1
- package/dist/esm/base-classes/services/__tests__/mock-resource-operation-helper.service.d.ts +0 -47
- package/dist/esm/base-classes/services/__tests__/mock-resource-operation-helper.service.d.ts.map +0 -1
- package/dist/esm/base-classes/services/__tests__/mock-resource-operation-helper.service.js +0 -82
- package/dist/esm/base-classes/services/__tests__/mock-resource-operation-helper.service.js.map +0 -1
- package/dist/esm/base-classes/services/repositories/__tests__/method-span-scan.guard-helpers.d.ts +0 -57
- package/dist/esm/base-classes/services/repositories/__tests__/method-span-scan.guard-helpers.d.ts.map +0 -1
- package/dist/esm/base-classes/services/repositories/__tests__/method-span-scan.guard-helpers.js +0 -172
- package/dist/esm/base-classes/services/repositories/__tests__/method-span-scan.guard-helpers.js.map +0 -1
- package/dist/esm/base-classes/services/repositories/__tests__/repositories-registry-infrastructure-read.double.d.ts +0 -41
- package/dist/esm/base-classes/services/repositories/__tests__/repositories-registry-infrastructure-read.double.d.ts.map +0 -1
- package/dist/esm/base-classes/services/repositories/__tests__/repositories-registry-infrastructure-read.double.js +0 -54
- package/dist/esm/base-classes/services/repositories/__tests__/repositories-registry-infrastructure-read.double.js.map +0 -1
- package/dist/esm/billing/services/__tests__/subscription-provider-projection.test-double.d.ts +0 -20
- package/dist/esm/billing/services/__tests__/subscription-provider-projection.test-double.d.ts.map +0 -1
- package/dist/esm/billing/services/__tests__/subscription-provider-projection.test-double.js +0 -33
- package/dist/esm/billing/services/__tests__/subscription-provider-projection.test-double.js.map +0 -1
- package/dist/esm/billing/services/custom-impl/__tests__/plan-change-confirmation.fixture.d.ts +0 -22
- package/dist/esm/billing/services/custom-impl/__tests__/plan-change-confirmation.fixture.d.ts.map +0 -1
- package/dist/esm/billing/services/custom-impl/__tests__/plan-change-confirmation.fixture.js +0 -92
- package/dist/esm/billing/services/custom-impl/__tests__/plan-change-confirmation.fixture.js.map +0 -1
- package/dist/esm/billing/services/custom-impl/__tests__/subscription-keyed-mutation.fixture.d.ts +0 -15
- package/dist/esm/billing/services/custom-impl/__tests__/subscription-keyed-mutation.fixture.d.ts.map +0 -1
- package/dist/esm/billing/services/custom-impl/__tests__/subscription-keyed-mutation.fixture.js +0 -61
- package/dist/esm/billing/services/custom-impl/__tests__/subscription-keyed-mutation.fixture.js.map +0 -1
- package/dist/esm/billing/services/webhooks/__tests__/billing-event-authority.test-double.d.ts +0 -21
- package/dist/esm/billing/services/webhooks/__tests__/billing-event-authority.test-double.d.ts.map +0 -1
- package/dist/esm/billing/services/webhooks/__tests__/billing-event-authority.test-double.js +0 -32
- package/dist/esm/billing/services/webhooks/__tests__/billing-event-authority.test-double.js.map +0 -1
- package/dist/esm/billing/services/webhooks/__tests__/settlement-ledger.test-double.d.ts +0 -52
- package/dist/esm/billing/services/webhooks/__tests__/settlement-ledger.test-double.d.ts.map +0 -1
- package/dist/esm/billing/services/webhooks/__tests__/settlement-ledger.test-double.js +0 -74
- package/dist/esm/billing/services/webhooks/__tests__/settlement-ledger.test-double.js.map +0 -1
- package/dist/esm/config/services/__tests__/mock-app-configuration.service.d.ts +0 -60
- package/dist/esm/config/services/__tests__/mock-app-configuration.service.d.ts.map +0 -1
- package/dist/esm/config/services/__tests__/mock-app-configuration.service.js +0 -156
- package/dist/esm/config/services/__tests__/mock-app-configuration.service.js.map +0 -1
- package/dist/esm/config/services/__tests__/startup-validator-resources-registry.double.d.ts +0 -31
- package/dist/esm/config/services/__tests__/startup-validator-resources-registry.double.d.ts.map +0 -1
- package/dist/esm/config/services/__tests__/startup-validator-resources-registry.double.js +0 -47
- package/dist/esm/config/services/__tests__/startup-validator-resources-registry.double.js.map +0 -1
- package/dist/esm/databases/services/__tests__/mock-mongoose-model.service.d.ts +0 -20
- package/dist/esm/databases/services/__tests__/mock-mongoose-model.service.d.ts.map +0 -1
- package/dist/esm/databases/services/__tests__/mock-mongoose-model.service.js +0 -424
- package/dist/esm/databases/services/__tests__/mock-mongoose-model.service.js.map +0 -1
- package/dist/esm/databases/services/__tests__/mock-zod-mongoose-schema-convertor.service.d.ts +0 -19
- package/dist/esm/databases/services/__tests__/mock-zod-mongoose-schema-convertor.service.d.ts.map +0 -1
- package/dist/esm/databases/services/__tests__/mock-zod-mongoose-schema-convertor.service.js +0 -49
- package/dist/esm/databases/services/__tests__/mock-zod-mongoose-schema-convertor.service.js.map +0 -1
- package/dist/esm/flows-actors/services/__tests__/pending-decision.fixtures.d.ts +0 -43
- package/dist/esm/flows-actors/services/__tests__/pending-decision.fixtures.d.ts.map +0 -1
- package/dist/esm/flows-actors/services/__tests__/pending-decision.fixtures.js +0 -61
- package/dist/esm/flows-actors/services/__tests__/pending-decision.fixtures.js.map +0 -1
- package/dist/esm/inbound/services/__tests__/support/in-memory-infrastructure-repositories.d.ts +0 -25
- package/dist/esm/inbound/services/__tests__/support/in-memory-infrastructure-repositories.d.ts.map +0 -1
- package/dist/esm/inbound/services/__tests__/support/in-memory-infrastructure-repositories.js +0 -92
- package/dist/esm/inbound/services/__tests__/support/in-memory-infrastructure-repositories.js.map +0 -1
- package/dist/esm/meta-workflows/services/__tests__/fixtures/meta-workflow-evaluation-treatment.test-fixture.d.ts +0 -7
- package/dist/esm/meta-workflows/services/__tests__/fixtures/meta-workflow-evaluation-treatment.test-fixture.d.ts.map +0 -1
- package/dist/esm/meta-workflows/services/__tests__/fixtures/meta-workflow-evaluation-treatment.test-fixture.js +0 -194
- package/dist/esm/meta-workflows/services/__tests__/fixtures/meta-workflow-evaluation-treatment.test-fixture.js.map +0 -1
- package/dist/esm/monitoring/logger/services/__tests__/mock-logger.service.d.ts +0 -34
- package/dist/esm/monitoring/logger/services/__tests__/mock-logger.service.d.ts.map +0 -1
- package/dist/esm/monitoring/logger/services/__tests__/mock-logger.service.js +0 -59
- package/dist/esm/monitoring/logger/services/__tests__/mock-logger.service.js.map +0 -1
- package/dist/esm/monitoring/observability/services/__tests__/mock-metrics.service.d.ts +0 -48
- package/dist/esm/monitoring/observability/services/__tests__/mock-metrics.service.d.ts.map +0 -1
- package/dist/esm/monitoring/observability/services/__tests__/mock-metrics.service.js +0 -53
- package/dist/esm/monitoring/observability/services/__tests__/mock-metrics.service.js.map +0 -1
- package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/bundle.d.ts +0 -3
- package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/bundle.d.ts.map +0 -1
- package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/bundle.js +0 -19
- package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/bundle.js.map +0 -1
- package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/host.d.ts +0 -51
- package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/host.d.ts.map +0 -1
- package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/host.js +0 -24
- package/dist/esm/notifications/services/__tests__/fixtures/personal-notification-browser/host.js.map +0 -1
- package/dist/esm/notifications/services/__tests__/recipient-language.test-double.d.ts +0 -8
- package/dist/esm/notifications/services/__tests__/recipient-language.test-double.d.ts.map +0 -1
- package/dist/esm/notifications/services/__tests__/recipient-language.test-double.js +0 -9
- package/dist/esm/notifications/services/__tests__/recipient-language.test-double.js.map +0 -1
- package/dist/esm/pdf-templates/__tests__/fixtures/tiny-pdf-template.fixture.d.ts +0 -10
- package/dist/esm/pdf-templates/__tests__/fixtures/tiny-pdf-template.fixture.d.ts.map +0 -1
- package/dist/esm/pdf-templates/__tests__/fixtures/tiny-pdf-template.fixture.js +0 -18
- package/dist/esm/pdf-templates/__tests__/fixtures/tiny-pdf-template.fixture.js.map +0 -1
- package/dist/esm/security/api-keys/__tests__/api-key-usage-redis.double.d.ts +0 -11
- package/dist/esm/security/api-keys/__tests__/api-key-usage-redis.double.d.ts.map +0 -1
- package/dist/esm/security/api-keys/__tests__/api-key-usage-redis.double.js +0 -34
- package/dist/esm/security/api-keys/__tests__/api-key-usage-redis.double.js.map +0 -1
- package/dist/esm/security/authentications/__tests__/registration-onboarding.harness.d.ts +0 -105
- package/dist/esm/security/authentications/__tests__/registration-onboarding.harness.d.ts.map +0 -1
- package/dist/esm/security/authentications/__tests__/registration-onboarding.harness.js +0 -140
- package/dist/esm/security/authentications/__tests__/registration-onboarding.harness.js.map +0 -1
- package/dist/esm/security/authentications/services/__tests__/oauth-provider-audit-spies.mock.d.ts +0 -35
- package/dist/esm/security/authentications/services/__tests__/oauth-provider-audit-spies.mock.d.ts.map +0 -1
- package/dist/esm/security/authentications/services/__tests__/oauth-provider-audit-spies.mock.js +0 -46
- package/dist/esm/security/authentications/services/__tests__/oauth-provider-audit-spies.mock.js.map +0 -1
- package/dist/esm/security/connected-accounts/__tests__/support/real-connection-client-methods.d.ts +0 -2
- package/dist/esm/security/connected-accounts/__tests__/support/real-connection-client-methods.d.ts.map +0 -1
- package/dist/esm/security/connected-accounts/__tests__/support/real-connection-client-methods.js +0 -18
- package/dist/esm/security/connected-accounts/__tests__/support/real-connection-client-methods.js.map +0 -1
- package/dist/esm/systems-of-record/credentials/__tests__/certificate-fixtures.d.ts +0 -20
- package/dist/esm/systems-of-record/credentials/__tests__/certificate-fixtures.d.ts.map +0 -1
- package/dist/esm/systems-of-record/credentials/__tests__/certificate-fixtures.js +0 -56
- package/dist/esm/systems-of-record/credentials/__tests__/certificate-fixtures.js.map +0 -1
- 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
|
-
*
|
|
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
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
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
|
-
*
|
|
137
|
-
*
|
|
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,
|
|
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,EAMP,yBAAyB,EAC1B,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;;;;;;;;GAQG;AACH,oBAAY,wBAAwB;IAClC,wDAAwD;IACxD,UAAU,eAAe;IACzB,gIAAgI;IAChI,IAAI,SAAS;IACb,wFAAwF;IACxF,QAAQ,aAAa;CACtB;AAED,gDAAgD;AAChD,MAAM,MAAM,kCAAkC,GAC1C;IAAE,OAAO,EAAE,wBAAwB,CAAC,UAAU,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GAC1E;IAAE,OAAO,EAAE,wBAAwB,CAAC,IAAI,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAC;IAAC,QAAQ,EAAE,yBAAyB,CAAA;CAAE,GACzG;IAAE,OAAO,EAAE,wBAAwB,CAAC,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE;;;GAGG;AACH,MAAM,MAAM,0BAA0B,CAAC,CAAC,IACpC;IAAE,OAAO,EAAE,wBAAwB,CAAC,UAAU,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAC1D;IAAE,OAAO,EAAE,wBAAwB,CAAC,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,yBAAyB,CAAA;CAAE,CAAC;AAE9F,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;;;;;;;OAOG;IACG,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kCAAkC,CAAC;IAqExF;;;;;;;;;;;;;;;OAeG;YACW,2BAA2B;IAqBzC,OAAO,CAAC,QAAQ;IA+BhB;;;;;;;;OAQG;IACH,mBAAmB,CAAC,KAAK,EAAE,qBAAqB,GAAG,UAAU;IAiC7D;;;;;;;;;;;OAWG;IACG,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,CAAC,UAAU,CAAC,CAAC;IAwCtF;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,0BAA0B,CAC9B,KAAK,EAAE,MAAM,EACb,kBAAkB,EAAE,MAAM,GACzB,OAAO,CAAC,0BAA0B,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAAC;CAsClE"}
|
|
@@ -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
|
-
*
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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
|
-
*
|
|
266
|
-
*
|
|
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 (
|
|
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.
|
|
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"]}
|