authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 12:04:27-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 12:04:27-07:00
log5f860439e9c981c29fc4385ad505ffebfbb63343
treec9db3e68dde21e56c19da4546cd3f790dfe183b5
parentc9159b67b30340e12f75005e34faa7d1aca0f231
signature Signed by PGP key B5690EEEBB952194

[codex] Structure APNs delivery job errors (#3322)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 221 insertions(+), 47 deletions(-)

infra/relay/src/agentActivity/ApnsDeliveries.test.ts+16
...@@ -611,6 +611,22 @@ describe("ApnsDeliveries", () => {...@@ -611,6 +611,22 @@ describe("ApnsDeliveries", () => {
611 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));611 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));
612 });612 });
613613
614 it.effect("preserves the schema cause for invalid queue payloads", () => {
615 const attempts: Array<DeliveryAttempts.DeliveryAttemptInput> = [];
616
617 return Effect.gen(function* () {
618 const deliveries = yield* ApnsDeliveries.ApnsDeliveries;
619 const error = yield* Effect.flip(deliveries.processSignedJob({ invalid: true }));
620
621 expect(error).toMatchObject({
622 _tag: "ApnsDeliveryJobQueuePayloadInvalid",
623 receivedType: "object",
624 message: "Invalid APNs delivery queue job with object payload.",
625 });
626 expect(error.cause).toMatchObject({ _tag: "SchemaError" });
627 }).pipe(Effect.provide(makeLayer({ attempts })));
628 });
629
614 it.effect("processes signed jobs through APNs and records attempts", () => {630 it.effect("processes signed jobs through APNs and records attempts", () => {
615 const attempts: Array<DeliveryAttempts.DeliveryAttemptInput> = [];631 const attempts: Array<DeliveryAttempts.DeliveryAttemptInput> = [];
616 const payload = makeApnsDeliveryJobPayload({632 const payload = makeApnsDeliveryJobPayload({
infra/relay/src/agentActivity/ApnsDeliveries.ts+22-3
...@@ -640,7 +640,13 @@ export const make = Effect.gen(function* () {...@@ -640,7 +640,13 @@ export const make = Effect.gen(function* () {
640 "relay.apns_deliveries.process_signed_job",640 "relay.apns_deliveries.process_signed_job",
641 )(function* (body) {641 )(function* (body) {
642 const signedJob = yield* decodeSignedApnsDeliveryJob(body).pipe(642 const signedJob = yield* decodeSignedApnsDeliveryJob(body).pipe(
643 Effect.mapError(() => new ApnsDeliveryJobQueuePayloadInvalid()),643 Effect.mapError(
644 (cause) =>
645 new ApnsDeliveryJobQueuePayloadInvalid({
646 receivedType: Array.isArray(body) ? "array" : body === null ? "null" : typeof body,
647 cause,
648 }),
649 ),
644 );650 );
645 const now = yield* DateTime.now;651 const now = yield* DateTime.now;
646 const payload = verifySignedApnsDeliveryJob({652 const payload = verifySignedApnsDeliveryJob({
...@@ -661,7 +667,14 @@ export const make = Effect.gen(function* () {...@@ -661,7 +667,14 @@ export const make = Effect.gen(function* () {
661 case "live_activity_start":667 case "live_activity_start":
662 case "live_activity_update":668 case "live_activity_update":
663 if (payload.aggregate === null) {669 if (payload.aggregate === null) {
664 return Effect.fail(new ApnsDeliveryJobLiveActivityAggregateMissing());670 return Effect.fail(
671 new ApnsDeliveryJobLiveActivityAggregateMissing({
672 jobId: payload.jobId,
673 kind: payload.kind,
674 userId: payload.target.userId,
675 deviceId: payload.target.deviceId,
676 }),
677 );
665 }678 }
666 return sendLiveActivity({679 return sendLiveActivity({
667 target: {680 target: {
...@@ -686,7 +699,13 @@ export const make = Effect.gen(function* () {...@@ -686,7 +699,13 @@ export const make = Effect.gen(function* () {
686 });699 });
687 case "push_notification":700 case "push_notification":
688 if (payload.notification === null) {701 if (payload.notification === null) {
689 return Effect.fail(new ApnsDeliveryJobPushNotificationMissing());702 return Effect.fail(
703 new ApnsDeliveryJobPushNotificationMissing({
704 jobId: payload.jobId,
705 userId: payload.target.userId,
706 deviceId: payload.target.deviceId,
707 }),
708 );
690 }709 }
691 return sendPushNotification({710 return sendPushNotification({
692 target: {711 target: {
infra/relay/src/agentActivity/apnsDeliveryJobs.test.ts+36-5
...@@ -70,6 +70,11 @@ describe("apnsDeliveryJobs", () => {...@@ -70,6 +70,11 @@ describe("apnsDeliveryJobs", () => {
7070
71 expect(result).toMatchObject({71 expect(result).toMatchObject({
72 _tag: "ApnsDeliveryJobSignatureInvalid",72 _tag: "ApnsDeliveryJobSignatureInvalid",
73 jobId: "job-1",
74 kind: "live_activity_end",
75 userId: "user-1",
76 deviceId: "device-1",
77 message: "Invalid signature for APNs delivery job job-1.",
73 });78 });
74 });79 });
7580
...@@ -94,7 +99,11 @@ describe("apnsDeliveryJobs", () => {...@@ -94,7 +99,11 @@ describe("apnsDeliveryJobs", () => {
9499
95 expect(result).toMatchObject({100 expect(result).toMatchObject({
96 _tag: "ApnsDeliveryJobLiveActivityAggregateMissing",101 _tag: "ApnsDeliveryJobLiveActivityAggregateMissing",
97 message: "Live Activity start/update jobs require an aggregate.",102 jobId: "job-start-invalid",
103 kind: "live_activity_start",
104 userId: "user-1",
105 deviceId: "device-1",
106 message: "APNs live activity start job job-start-invalid requires an aggregate.",
98 });107 });
99 });108 });
100109
...@@ -120,7 +129,10 @@ describe("apnsDeliveryJobs", () => {...@@ -120,7 +129,10 @@ describe("apnsDeliveryJobs", () => {
120129
121 expect(result).toMatchObject({130 expect(result).toMatchObject({
122 _tag: "ApnsDeliveryJobPushNotificationAggregateUnexpected",131 _tag: "ApnsDeliveryJobPushNotificationAggregateUnexpected",
123 message: "Push notification jobs must not carry aggregate state.",132 jobId: "job-push-invalid",
133 userId: "user-1",
134 deviceId: "device-1",
135 message: "APNs push notification job job-push-invalid must not carry aggregate state.",
124 });136 });
125 });137 });
126138
...@@ -195,7 +207,12 @@ describe("apnsDeliveryJobs", () => {...@@ -195,7 +207,12 @@ describe("apnsDeliveryJobs", () => {
195 }),207 }),
196 ).toMatchObject({208 ).toMatchObject({
197 _tag: "ApnsDeliveryJobCreatedAtInvalid",209 _tag: "ApnsDeliveryJobCreatedAtInvalid",
198 message: "Invalid APNs delivery job creation time.",210 jobId: "job-window",
211 kind: "live_activity_end",
212 userId: "user-1",
213 deviceId: "device-1",
214 createdAt: "not-a-date",
215 message: "APNs delivery job job-window has invalid creation time not-a-date.",
199 });216 });
200 expect(217 expect(
201 verifySignedApnsDeliveryJob({218 verifySignedApnsDeliveryJob({
...@@ -205,7 +222,14 @@ describe("apnsDeliveryJobs", () => {...@@ -205,7 +222,14 @@ describe("apnsDeliveryJobs", () => {
205 }),222 }),
206 ).toMatchObject({223 ).toMatchObject({
207 _tag: "ApnsDeliveryJobTimeWindowInvalid",224 _tag: "ApnsDeliveryJobTimeWindowInvalid",
208 message: "Invalid APNs delivery job time window.",225 jobId: "job-window",
226 kind: "live_activity_end",
227 userId: "user-1",
228 deviceId: "device-1",
229 createdAt: "2026-05-25T00:00:00.000Z",
230 expiresAt: "2026-05-24T23:59:59.000Z",
231 message:
232 "APNs delivery job job-window has invalid time window 2026-05-25T00:00:00.000Z to 2026-05-24T23:59:59.000Z.",
209 });233 });
210 expect(234 expect(
211 verifySignedApnsDeliveryJob({235 verifySignedApnsDeliveryJob({
...@@ -215,7 +239,14 @@ describe("apnsDeliveryJobs", () => {...@@ -215,7 +239,14 @@ describe("apnsDeliveryJobs", () => {
215 }),239 }),
216 ).toMatchObject({240 ).toMatchObject({
217 _tag: "ApnsDeliveryJobTimeWindowTooLong",241 _tag: "ApnsDeliveryJobTimeWindowTooLong",
218 message: "APNs delivery job time window is too long.",242 jobId: "job-window",
243 kind: "live_activity_end",
244 userId: "user-1",
245 deviceId: "device-1",
246 createdAt: "2026-05-25T00:00:00.000Z",
247 expiresAt: "2026-05-25T00:10:01.000Z",
248 message:
249 "APNs delivery job job-window time window 2026-05-25T00:00:00.000Z to 2026-05-25T00:10:01.000Z is too long.",
219 });250 });
220 });251 });
221});252});
infra/relay/src/agentActivity/apnsDeliveryJobs.ts+147-39
...@@ -10,12 +10,27 @@ import * as Schema from "effect/Schema";...@@ -10,12 +10,27 @@ import * as Schema from "effect/Schema";
10const MAX_JOB_AGE_MS = 10 * 60 * 1_000;10const MAX_JOB_AGE_MS = 10 * 60 * 1_000;
11export const APNS_DELIVERY_JOB_SIGNING_ALGORITHM = "hmac-sha256";11export const APNS_DELIVERY_JOB_SIGNING_ALGORITHM = "hmac-sha256";
1212
13const ApnsDeliveryKind = Schema.Literals([13const ApnsDeliveryKindSchema = Schema.Literals([
14 "live_activity_start",14 "live_activity_start",
15 "live_activity_update",15 "live_activity_update",
16 "live_activity_end",16 "live_activity_end",
17 "push_notification",17 "push_notification",
18]);18]);
19const LiveActivityStartOrUpdateKindSchema = Schema.Literals([
20 "live_activity_start",
21 "live_activity_update",
22]);
23const LiveActivityKindSchema = Schema.Literals([
24 "live_activity_start",
25 "live_activity_update",
26 "live_activity_end",
27]);
28
29const ApnsDeliveryJobContext = {
30 jobId: Schema.String,
31 userId: Schema.String,
32 deviceId: Schema.String,
33};
1934
20export const ApnsNotificationPayload = Schema.Struct({35export const ApnsNotificationPayload = Schema.Struct({
21 title: Schema.String,36 title: Schema.String,
...@@ -29,7 +44,7 @@ export type ApnsNotificationPayload = typeof ApnsNotificationPayload.Type;...@@ -29,7 +44,7 @@ export type ApnsNotificationPayload = typeof ApnsNotificationPayload.Type;
29export const ApnsDeliveryJobPayload = Schema.Struct({44export const ApnsDeliveryJobPayload = Schema.Struct({
30 version: Schema.Literal(1),45 version: Schema.Literal(1),
31 jobId: Schema.String,46 jobId: Schema.String,
32 kind: ApnsDeliveryKind,47 kind: ApnsDeliveryKindSchema,
33 target: Schema.Struct({48 target: Schema.Struct({
34 userId: Schema.String,49 userId: Schema.String,
35 deviceId: Schema.String,50 deviceId: Schema.String,
...@@ -51,91 +66,121 @@ export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type;...@@ -51,91 +66,121 @@ export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type;
5166
52export class ApnsDeliveryJobQueuePayloadInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobQueuePayloadInvalid>()(67export class ApnsDeliveryJobQueuePayloadInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobQueuePayloadInvalid>()(
53 "ApnsDeliveryJobQueuePayloadInvalid",68 "ApnsDeliveryJobQueuePayloadInvalid",
54 {},69 {
70 receivedType: Schema.String,
71 cause: Schema.Defect(),
72 },
55) {73) {
56 override get message(): string {74 override get message(): string {
57 return "Invalid APNs delivery queue job.";75 return `Invalid APNs delivery queue job with ${this.receivedType} payload.`;
58 }76 }
59}77}
6078
61export class ApnsDeliveryJobLiveActivityAggregateMissing extends Schema.TaggedErrorClass<ApnsDeliveryJobLiveActivityAggregateMissing>()(79export class ApnsDeliveryJobLiveActivityAggregateMissing extends Schema.TaggedErrorClass<ApnsDeliveryJobLiveActivityAggregateMissing>()(
62 "ApnsDeliveryJobLiveActivityAggregateMissing",80 "ApnsDeliveryJobLiveActivityAggregateMissing",
63 {},81 {
82 ...ApnsDeliveryJobContext,
83 kind: LiveActivityStartOrUpdateKindSchema,
84 },
64) {85) {
65 override get message(): string {86 override get message(): string {
66 return "Live Activity start/update jobs require an aggregate.";87 return `APNs ${this.kind.replaceAll("_", " ")} job ${this.jobId} requires an aggregate.`;
67 }88 }
68}89}
6990
70export class ApnsDeliveryJobLiveActivityNotificationUnexpected extends Schema.TaggedErrorClass<ApnsDeliveryJobLiveActivityNotificationUnexpected>()(91export class ApnsDeliveryJobLiveActivityNotificationUnexpected extends Schema.TaggedErrorClass<ApnsDeliveryJobLiveActivityNotificationUnexpected>()(
71 "ApnsDeliveryJobLiveActivityNotificationUnexpected",92 "ApnsDeliveryJobLiveActivityNotificationUnexpected",
72 {},93 {
94 ...ApnsDeliveryJobContext,
95 kind: LiveActivityKindSchema,
96 },
73) {97) {
74 override get message(): string {98 override get message(): string {
75 return "Live Activity jobs must not carry push notification payloads.";99 return `APNs ${this.kind.replaceAll("_", " ")} job ${this.jobId} must not carry a push notification payload.`;
76 }100 }
77}101}
78102
79export class ApnsDeliveryJobPushNotificationMissing extends Schema.TaggedErrorClass<ApnsDeliveryJobPushNotificationMissing>()(103export class ApnsDeliveryJobPushNotificationMissing extends Schema.TaggedErrorClass<ApnsDeliveryJobPushNotificationMissing>()(
80 "ApnsDeliveryJobPushNotificationMissing",104 "ApnsDeliveryJobPushNotificationMissing",
81 {},105 ApnsDeliveryJobContext,
82) {106) {
83 override get message(): string {107 override get message(): string {
84 return "Push notification jobs require a notification payload.";108 return `APNs push notification job ${this.jobId} requires a notification payload.`;
85 }109 }
86}110}
87111
88export class ApnsDeliveryJobPushNotificationAggregateUnexpected extends Schema.TaggedErrorClass<ApnsDeliveryJobPushNotificationAggregateUnexpected>()(112export class ApnsDeliveryJobPushNotificationAggregateUnexpected extends Schema.TaggedErrorClass<ApnsDeliveryJobPushNotificationAggregateUnexpected>()(
89 "ApnsDeliveryJobPushNotificationAggregateUnexpected",113 "ApnsDeliveryJobPushNotificationAggregateUnexpected",
90 {},114 ApnsDeliveryJobContext,
91) {115) {
92 override get message(): string {116 override get message(): string {
93 return "Push notification jobs must not carry aggregate state.";117 return `APNs push notification job ${this.jobId} must not carry aggregate state.`;
94 }118 }
95}119}
96120
97export class ApnsDeliveryJobCreatedAtInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobCreatedAtInvalid>()(121export class ApnsDeliveryJobCreatedAtInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobCreatedAtInvalid>()(
98 "ApnsDeliveryJobCreatedAtInvalid",122 "ApnsDeliveryJobCreatedAtInvalid",
99 {},123 {
124 ...ApnsDeliveryJobContext,
125 kind: ApnsDeliveryKindSchema,
126 createdAt: Schema.String,
127 },
100) {128) {
101 override get message(): string {129 override get message(): string {
102 return "Invalid APNs delivery job creation time.";130 return `APNs delivery job ${this.jobId} has invalid creation time ${this.createdAt}.`;
103 }131 }
104}132}
105133
106export class ApnsDeliveryJobExpiresAtInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobExpiresAtInvalid>()(134export class ApnsDeliveryJobExpiresAtInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobExpiresAtInvalid>()(
107 "ApnsDeliveryJobExpiresAtInvalid",135 "ApnsDeliveryJobExpiresAtInvalid",
108 {},136 {
137 ...ApnsDeliveryJobContext,
138 kind: ApnsDeliveryKindSchema,
139 expiresAt: Schema.String,
140 },
109) {141) {
110 override get message(): string {142 override get message(): string {
111 return "Invalid APNs delivery job expiry.";143 return `APNs delivery job ${this.jobId} has invalid expiry ${this.expiresAt}.`;
112 }144 }
113}145}
114146
115export class ApnsDeliveryJobTimeWindowInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobTimeWindowInvalid>()(147export class ApnsDeliveryJobTimeWindowInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobTimeWindowInvalid>()(
116 "ApnsDeliveryJobTimeWindowInvalid",148 "ApnsDeliveryJobTimeWindowInvalid",
117 {},149 {
150 ...ApnsDeliveryJobContext,
151 kind: ApnsDeliveryKindSchema,
152 createdAt: Schema.String,
153 expiresAt: Schema.String,
154 },
118) {155) {
119 override get message(): string {156 override get message(): string {
120 return "Invalid APNs delivery job time window.";157 return `APNs delivery job ${this.jobId} has invalid time window ${this.createdAt} to ${this.expiresAt}.`;
121 }158 }
122}159}
123160
124export class ApnsDeliveryJobTimeWindowTooLong extends Schema.TaggedErrorClass<ApnsDeliveryJobTimeWindowTooLong>()(161export class ApnsDeliveryJobTimeWindowTooLong extends Schema.TaggedErrorClass<ApnsDeliveryJobTimeWindowTooLong>()(
125 "ApnsDeliveryJobTimeWindowTooLong",162 "ApnsDeliveryJobTimeWindowTooLong",
126 {},163 {
164 ...ApnsDeliveryJobContext,
165 kind: ApnsDeliveryKindSchema,
166 createdAt: Schema.String,
167 expiresAt: Schema.String,
168 },
127) {169) {
128 override get message(): string {170 override get message(): string {
129 return "APNs delivery job time window is too long.";171 return `APNs delivery job ${this.jobId} time window ${this.createdAt} to ${this.expiresAt} is too long.`;
130 }172 }
131}173}
132174
133export class ApnsDeliveryJobSignatureInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobSignatureInvalid>()(175export class ApnsDeliveryJobSignatureInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobSignatureInvalid>()(
134 "ApnsDeliveryJobSignatureInvalid",176 "ApnsDeliveryJobSignatureInvalid",
135 {},177 {
178 ...ApnsDeliveryJobContext,
179 kind: ApnsDeliveryKindSchema,
180 },
136) {181) {
137 override get message(): string {182 override get message(): string {
138 return "Invalid APNs delivery job signature.";183 return `Invalid signature for APNs delivery job ${this.jobId}.`;
139 }184 }
140}185}
141186
...@@ -156,11 +201,13 @@ export type ApnsDeliveryJobInvalid = typeof ApnsDeliveryJobInvalid.Type;...@@ -156,11 +201,13 @@ export type ApnsDeliveryJobInvalid = typeof ApnsDeliveryJobInvalid.Type;
156export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDeliveryJobExpired>()(201export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDeliveryJobExpired>()(
157 "ApnsDeliveryJobExpired",202 "ApnsDeliveryJobExpired",
158 {203 {
204 ...ApnsDeliveryJobContext,
205 kind: ApnsDeliveryKindSchema,
159 expiresAt: Schema.String,206 expiresAt: Schema.String,
160 },207 },
161) {208) {
162 override get message(): string {209 override get message(): string {
163 return `APNs delivery job expired at ${this.expiresAt}`;210 return `APNs delivery job ${this.jobId} expired at ${this.expiresAt}.`;
164 }211 }
165}212}
166213
...@@ -208,23 +255,46 @@ function validatePayloadShape(payload: ApnsDeliveryJobPayload): ApnsDeliveryJobI...@@ -208,23 +255,46 @@ function validatePayloadShape(payload: ApnsDeliveryJobPayload): ApnsDeliveryJobI
208 case "live_activity_start":255 case "live_activity_start":
209 case "live_activity_update":256 case "live_activity_update":
210 if (payload.aggregate === null) {257 if (payload.aggregate === null) {
211 return new ApnsDeliveryJobLiveActivityAggregateMissing();258 return new ApnsDeliveryJobLiveActivityAggregateMissing({
259 jobId: payload.jobId,
260 kind: payload.kind,
261 userId: payload.target.userId,
262 deviceId: payload.target.deviceId,
263 });
212 }264 }
213 if (payload.notification !== null) {265 if (payload.notification !== null) {
214 return new ApnsDeliveryJobLiveActivityNotificationUnexpected();266 return new ApnsDeliveryJobLiveActivityNotificationUnexpected({
267 jobId: payload.jobId,
268 kind: payload.kind,
269 userId: payload.target.userId,
270 deviceId: payload.target.deviceId,
271 });
215 }272 }
216 return null;273 return null;
217 case "live_activity_end":274 case "live_activity_end":
218 if (payload.notification !== null) {275 if (payload.notification !== null) {
219 return new ApnsDeliveryJobLiveActivityNotificationUnexpected();276 return new ApnsDeliveryJobLiveActivityNotificationUnexpected({
277 jobId: payload.jobId,
278 kind: payload.kind,
279 userId: payload.target.userId,
280 deviceId: payload.target.deviceId,
281 });
220 }282 }
221 return null;283 return null;
222 case "push_notification":284 case "push_notification":
223 if (payload.notification === null) {285 if (payload.notification === null) {
224 return new ApnsDeliveryJobPushNotificationMissing();286 return new ApnsDeliveryJobPushNotificationMissing({
287 jobId: payload.jobId,
288 userId: payload.target.userId,
289 deviceId: payload.target.deviceId,
290 });
225 }291 }
226 if (payload.aggregate !== null) {292 if (payload.aggregate !== null) {
227 return new ApnsDeliveryJobPushNotificationAggregateUnexpected();293 return new ApnsDeliveryJobPushNotificationAggregateUnexpected({
294 jobId: payload.jobId,
295 userId: payload.target.userId,
296 deviceId: payload.target.deviceId,
297 });
228 }298 }
229 return null;299 return null;
230 }300 }
...@@ -264,35 +334,73 @@ export function verifySignedApnsDeliveryJob(input: {...@@ -264,35 +334,73 @@ export function verifySignedApnsDeliveryJob(input: {
264 readonly job: SignedApnsDeliveryJob;334 readonly job: SignedApnsDeliveryJob;
265 readonly nowMs: number;335 readonly nowMs: number;
266}): ApnsDeliveryJobPayload | ApnsDeliveryJobVerificationError {336}): ApnsDeliveryJobPayload | ApnsDeliveryJobVerificationError {
267 const invalidPayload = validatePayloadShape(input.job.payload);337 const payload = input.job.payload;
338 const invalidPayload = validatePayloadShape(payload);
268 if (invalidPayload !== null) {339 if (invalidPayload !== null) {
269 return invalidPayload;340 return invalidPayload;
270 }341 }
271 const createdAt = DateTime.make(input.job.payload.createdAt);342 const createdAt = DateTime.make(payload.createdAt);
272 if (Option.isNone(createdAt)) {343 if (Option.isNone(createdAt)) {
273 return new ApnsDeliveryJobCreatedAtInvalid();344 return new ApnsDeliveryJobCreatedAtInvalid({
345 jobId: payload.jobId,
346 kind: payload.kind,
347 userId: payload.target.userId,
348 deviceId: payload.target.deviceId,
349 createdAt: payload.createdAt,
350 });
274 }351 }
275 const expiresAt = DateTime.make(input.job.payload.expiresAt);352 const expiresAt = DateTime.make(payload.expiresAt);
276 if (Option.isNone(expiresAt)) {353 if (Option.isNone(expiresAt)) {
277 return new ApnsDeliveryJobExpiresAtInvalid();354 return new ApnsDeliveryJobExpiresAtInvalid({
355 jobId: payload.jobId,
356 kind: payload.kind,
357 userId: payload.target.userId,
358 deviceId: payload.target.deviceId,
359 expiresAt: payload.expiresAt,
360 });
278 }361 }
279 const createdAtMs = createdAt.value.epochMilliseconds;362 const createdAtMs = createdAt.value.epochMilliseconds;
280 const expiresAtMs = expiresAt.value.epochMilliseconds;363 const expiresAtMs = expiresAt.value.epochMilliseconds;
281 if (expiresAtMs <= createdAtMs) {364 if (expiresAtMs <= createdAtMs) {
282 return new ApnsDeliveryJobTimeWindowInvalid();365 return new ApnsDeliveryJobTimeWindowInvalid({
366 jobId: payload.jobId,
367 kind: payload.kind,
368 userId: payload.target.userId,
369 deviceId: payload.target.deviceId,
370 createdAt: payload.createdAt,
371 expiresAt: payload.expiresAt,
372 });
283 }373 }
284 if (expiresAtMs - createdAtMs > MAX_JOB_AGE_MS) {374 if (expiresAtMs - createdAtMs > MAX_JOB_AGE_MS) {
285 return new ApnsDeliveryJobTimeWindowTooLong();375 return new ApnsDeliveryJobTimeWindowTooLong({
376 jobId: payload.jobId,
377 kind: payload.kind,
378 userId: payload.target.userId,
379 deviceId: payload.target.deviceId,
380 createdAt: payload.createdAt,
381 expiresAt: payload.expiresAt,
382 });
286 }383 }
287 if (expiresAtMs <= input.nowMs) {384 if (expiresAtMs <= input.nowMs) {
288 return new ApnsDeliveryJobExpired({ expiresAt: input.job.payload.expiresAt });385 return new ApnsDeliveryJobExpired({
386 jobId: payload.jobId,
387 kind: payload.kind,
388 userId: payload.target.userId,
389 deviceId: payload.target.deviceId,
390 expiresAt: payload.expiresAt,
391 });
289 }392 }
290 const expected = signatureForPayload({393 const expected = signatureForPayload({
291 secret: input.secret,394 secret: input.secret,
292 payload: input.job.payload,395 payload,
293 });396 });
294 if (!timingSafeEqualBase64Url(input.job.signature, expected)) {397 if (!timingSafeEqualBase64Url(input.job.signature, expected)) {
295 return new ApnsDeliveryJobSignatureInvalid();398 return new ApnsDeliveryJobSignatureInvalid({
399 jobId: payload.jobId,
400 kind: payload.kind,
401 userId: payload.target.userId,
402 deviceId: payload.target.deviceId,
403 });
296 }404 }
297 return input.job.payload;405 return payload;
298}406}