authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 21:37:43-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 21:37:43-07:00
logb17b6d50c55e3eff4317163d31c4032423d3d2c1
treef36aeb549736bfda5e87fb5e29ceeba360f0a702
parentccf8331600f323939a796169dfe2598a4c1b59c4
signature Signed by PGP key B5690EEEBB952194

[codex] Complete relay agent activity Effect cleanup (#3210)

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

12 files changed, 185 insertions(+), 139 deletions(-)

infra/relay/src/agentActivity/AgentActivityPublisher.ts+1-1
...@@ -39,7 +39,7 @@ export class AgentActivityPublisher extends Context.Service<...@@ -39,7 +39,7 @@ export class AgentActivityPublisher extends Context.Service<
39 }39 }
40>()("t3code-relay/agentActivity/AgentActivityPublisher") {}40>()("t3code-relay/agentActivity/AgentActivityPublisher") {}
4141
42const make = Effect.gen(function* () {42export const make = Effect.gen(function* () {
43 const rows = yield* AgentActivityRows.AgentActivityRows;43 const rows = yield* AgentActivityRows.AgentActivityRows;
44 const links = yield* EnvironmentLinks.EnvironmentLinks;44 const links = yield* EnvironmentLinks.EnvironmentLinks;
45 const liveActivities = yield* LiveActivities.LiveActivities;45 const liveActivities = yield* LiveActivities.LiveActivities;
infra/relay/src/agentActivity/AgentActivityRows.ts+1-1
...@@ -71,7 +71,7 @@ const decodeRelayAgentActivityStateJson = Schema.decodeUnknownOption(...@@ -71,7 +71,7 @@ const decodeRelayAgentActivityStateJson = Schema.decodeUnknownOption(
71 Schema.fromJsonString(RelayAgentActivityStateSchema),71 Schema.fromJsonString(RelayAgentActivityStateSchema),
72);72);
7373
74const make = Effect.gen(function* () {74export const make = Effect.gen(function* () {
75 const db = yield* RelayDb.RelayDb;75 const db = yield* RelayDb.RelayDb;
7676
77 return AgentActivityRows.of({77 return AgentActivityRows.of({
infra/relay/src/agentActivity/ApnsClient.ts+26-14
...@@ -40,15 +40,21 @@ export interface ApnsDeliveryResult {...@@ -40,15 +40,21 @@ export interface ApnsDeliveryResult {
40 readonly apnsId: string | null;40 readonly apnsId: string | null;
41}41}
4242
43export class ApnsSigningError extends Schema.TaggedErrorClass<ApnsSigningError>()(43export class ApnsJwtEncodingError extends Schema.TaggedErrorClass<ApnsJwtEncodingError>()(
44 "ApnsSigningError",44 "ApnsJwtEncodingError",
45 {45 { cause: Schema.Defect() },
46 phase: Schema.Literals(["encoding", "signing"]),46) {
47 cause: Schema.Defect(),47 override get message(): string {
48 },48 return "Failed to encode APNs JWT.";
49 }
50}
51
52export class ApnsJwtSigningError extends Schema.TaggedErrorClass<ApnsJwtSigningError>()(
53 "ApnsJwtSigningError",
54 { cause: Schema.Defect() },
49) {55) {
50 override get message(): string {56 override get message(): string {
51 return `Failed during APNs JWT ${this.phase}`;57 return "Failed to sign APNs JWT.";
52 }58 }
53}59}
5460
...@@ -59,7 +65,7 @@ export class ApnsHttpRequestError extends Schema.TaggedErrorClass<ApnsHttpReques...@@ -59,7 +65,7 @@ export class ApnsHttpRequestError extends Schema.TaggedErrorClass<ApnsHttpReques
59 },65 },
60) {66) {
61 override get message(): string {67 override get message(): string {
62 return "APNs HTTP request failed";68 return "APNs HTTP request failed.";
63 }69 }
64}70}
6571
...@@ -70,11 +76,17 @@ export class ApnsInvalidResponseError extends Schema.TaggedErrorClass<ApnsInvali...@@ -70,11 +76,17 @@ export class ApnsInvalidResponseError extends Schema.TaggedErrorClass<ApnsInvali
70 },76 },
71) {77) {
72 override get message(): string {78 override get message(): string {
73 return "APNs returned an invalid response";79 return "APNs returned an invalid response.";
74 }80 }
75}81}
7682
77export type ApnsError = ApnsSigningError | ApnsHttpRequestError | ApnsInvalidResponseError;83export const ApnsError = Schema.Union([
84 ApnsJwtEncodingError,
85 ApnsJwtSigningError,
86 ApnsHttpRequestError,
87 ApnsInvalidResponseError,
88]);
89export type ApnsError = typeof ApnsError.Type;
7890
79const decodeApnsErrorResponseJson = Schema.decodeUnknownOption(91const decodeApnsErrorResponseJson = Schema.decodeUnknownOption(
80 Schema.fromJsonString(92 Schema.fromJsonString(
...@@ -107,12 +119,12 @@ const makeApnsJwt = Effect.fn("relay.apns.make_jwt")(function* (input: {...@@ -107,12 +119,12 @@ const makeApnsJwt = Effect.fn("relay.apns.make_jwt")(function* (input: {
107 readonly issuedAtUnixSeconds: number;119 readonly issuedAtUnixSeconds: number;
108}) {120}) {
109 const headerJson = yield* encodeApnsJwtHeaderJson({ alg: "ES256", kid: input.keyId }).pipe(121 const headerJson = yield* encodeApnsJwtHeaderJson({ alg: "ES256", kid: input.keyId }).pipe(
110 Effect.mapError((cause) => new ApnsSigningError({ cause, phase: "encoding" })),122 Effect.mapError((cause) => new ApnsJwtEncodingError({ cause })),
111 );123 );
112 const payloadJson = yield* encodeApnsJwtPayloadJson({124 const payloadJson = yield* encodeApnsJwtPayloadJson({
113 iss: input.teamId,125 iss: input.teamId,
114 iat: input.issuedAtUnixSeconds,126 iat: input.issuedAtUnixSeconds,
115 }).pipe(Effect.mapError((cause) => new ApnsSigningError({ cause, phase: "encoding" })));127 }).pipe(Effect.mapError((cause) => new ApnsJwtEncodingError({ cause })));
116128
117 const privateKey = Redacted.value(input.privateKey);129 const privateKey = Redacted.value(input.privateKey);
118 const header = Encoding.encodeBase64Url(headerJson);130 const header = Encoding.encodeBase64Url(headerJson);
...@@ -129,7 +141,7 @@ const makeApnsJwt = Effect.fn("relay.apns.make_jwt")(function* (input: {...@@ -129,7 +141,7 @@ const makeApnsJwt = Effect.fn("relay.apns.make_jwt")(function* (input: {
129 });141 });
130 return `${signingInput}.${Encoding.encodeBase64Url(signature)}`;142 return `${signingInput}.${Encoding.encodeBase64Url(signature)}`;
131 },143 },
132 catch: (cause) => new ApnsSigningError({ cause, phase: "signing" }),144 catch: (cause) => new ApnsJwtSigningError({ cause }),
133 });145 });
134});146});
135147
...@@ -251,7 +263,7 @@ export class ApnsClient extends Context.Service<...@@ -251,7 +263,7 @@ export class ApnsClient extends Context.Service<
251 }263 }
252>()("t3code-relay/agentActivity/ApnsClient") {}264>()("t3code-relay/agentActivity/ApnsClient") {}
253265
254const make = Effect.gen(function* () {266export const make = Effect.gen(function* () {
255 const httpClient = yield* HttpClient.HttpClient;267 const httpClient = yield* HttpClient.HttpClient;
256268
257 const sendLiveActivityRequest: ApnsClient["Service"]["sendLiveActivityRequest"] = Effect.fn(269 const sendLiveActivityRequest: ApnsClient["Service"]["sendLiveActivityRequest"] = Effect.fn(
infra/relay/src/agentActivity/ApnsDeliveries.ts+11-41
...@@ -21,9 +21,12 @@ import {...@@ -21,9 +21,12 @@ import {
21} from "./agentActivityPayloads.ts";21} from "./agentActivityPayloads.ts";
22import * as Apns from "./ApnsClient.ts";22import * as Apns from "./ApnsClient.ts";
23import {23import {
24 ApnsDeliveryJobInvalid,24 ApnsDeliveryJobLiveActivityAggregateMissing,
25 ApnsDeliveryJobPushNotificationMissing,
26 ApnsDeliveryJobQueuePayloadInvalid,
25 type ApnsNotificationPayload,27 type ApnsNotificationPayload,
26 SignedApnsDeliveryJob,28 SignedApnsDeliveryJob,
29 isApnsDeliveryJobVerificationError,
27 verifySignedApnsDeliveryJob,30 verifySignedApnsDeliveryJob,
28 type ApnsDeliveryJobVerificationError,31 type ApnsDeliveryJobVerificationError,
29} from "./apnsDeliveryJobs.ts";32} from "./apnsDeliveryJobs.ts";
...@@ -92,17 +95,6 @@ const decodeRelayAgentAwarenessPreferencesJson = Schema.decodeUnknownOption(...@@ -92,17 +95,6 @@ const decodeRelayAgentAwarenessPreferencesJson = Schema.decodeUnknownOption(
92);95);
93const decodeSignedApnsDeliveryJob = Schema.decodeUnknownEffect(SignedApnsDeliveryJob);96const decodeSignedApnsDeliveryJob = Schema.decodeUnknownEffect(SignedApnsDeliveryJob);
9497
95function apnsErrorMessage(error: Apns.ApnsError): string {
96 switch (error._tag) {
97 case "ApnsSigningError":
98 return "Failed to sign APNs request.";
99 case "ApnsHttpRequestError":
100 return "Failed to send APNs request.";
101 case "ApnsInvalidResponseError":
102 return "APNs returned an invalid response.";
103 }
104}
105
106function parseAggregate(value: string | null): RelayAgentActivityAggregateState | null {98function parseAggregate(value: string | null): RelayAgentActivityAggregateState | null {
107 if (!value) {99 if (!value) {
108 return null;100 return null;
...@@ -273,15 +265,6 @@ function isPermanentApnsTokenFailure(result: Apns.ApnsDeliveryResult): boolean {...@@ -273,15 +265,6 @@ function isPermanentApnsTokenFailure(result: Apns.ApnsDeliveryResult): boolean {
273 );265 );
274}266}
275267
276function isDeliveryJobVerificationError(value: unknown): value is ApnsDeliveryJobVerificationError {
277 return (
278 typeof value === "object" &&
279 value !== null &&
280 "_tag" in value &&
281 (value._tag === "ApnsDeliveryJobInvalid" || value._tag === "ApnsDeliveryJobExpired")
282 );
283}
284
285function duplicateJobResult(input: {268function duplicateJobResult(input: {
286 readonly deviceId: string;269 readonly deviceId: string;
287 readonly kind: RelayDeliveryKind;270 readonly kind: RelayDeliveryKind;
...@@ -418,7 +401,7 @@ export class ApnsDeliveries extends Context.Service<...@@ -418,7 +401,7 @@ export class ApnsDeliveries extends Context.Service<
418 }401 }
419>()("t3code-relay/agentActivity/ApnsDeliveries") {}402>()("t3code-relay/agentActivity/ApnsDeliveries") {}
420403
421const make = Effect.gen(function* () {404export const make = Effect.gen(function* () {
422 const attempts = yield* DeliveryAttempts.DeliveryAttempts;405 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
423 const liveActivities = yield* LiveActivities.LiveActivities;406 const liveActivities = yield* LiveActivities.LiveActivities;
424 const deliveryQueue = yield* ApnsDeliveryQueue.ApnsDeliveryQueue;407 const deliveryQueue = yield* ApnsDeliveryQueue.ApnsDeliveryQueue;
...@@ -497,7 +480,7 @@ const make = Effect.gen(function* () {...@@ -497,7 +480,7 @@ const make = Effect.gen(function* () {
497 Effect.succeed({480 Effect.succeed({
498 ok: false,481 ok: false,
499 status: 0,482 status: 0,
500 reason: apnsErrorMessage(error),483 reason: error.message,
501 apnsId: null,484 apnsId: null,
502 }),485 }),
503 ),486 ),
...@@ -614,7 +597,7 @@ const make = Effect.gen(function* () {...@@ -614,7 +597,7 @@ const make = Effect.gen(function* () {
614 Effect.succeed({597 Effect.succeed({
615 ok: false,598 ok: false,
616 status: 0,599 status: 0,
617 reason: apnsErrorMessage(error),600 reason: error.message,
618 apnsId: null,601 apnsId: null,
619 }),602 }),
620 ),603 ),
...@@ -657,12 +640,7 @@ const make = Effect.gen(function* () {...@@ -657,12 +640,7 @@ const make = Effect.gen(function* () {
657 "relay.apns_deliveries.process_signed_job",640 "relay.apns_deliveries.process_signed_job",
658 )(function* (body) {641 )(function* (body) {
659 const signedJob = yield* decodeSignedApnsDeliveryJob(body).pipe(642 const signedJob = yield* decodeSignedApnsDeliveryJob(body).pipe(
660 Effect.mapError(643 Effect.mapError(() => new ApnsDeliveryJobQueuePayloadInvalid()),
661 () =>
662 new ApnsDeliveryJobInvalid({
663 reason: "invalid-queue-payload",
664 }),
665 ),
666 );644 );
667 const now = yield* DateTime.now;645 const now = yield* DateTime.now;
668 const payload = verifySignedApnsDeliveryJob({646 const payload = verifySignedApnsDeliveryJob({
...@@ -670,7 +648,7 @@ const make = Effect.gen(function* () {...@@ -670,7 +648,7 @@ const make = Effect.gen(function* () {
670 job: signedJob,648 job: signedJob,
671 nowMs: now.epochMilliseconds,649 nowMs: now.epochMilliseconds,
672 });650 });
673 if (isDeliveryJobVerificationError(payload)) {651 if (isApnsDeliveryJobVerificationError(payload)) {
674 return yield* payload;652 return yield* payload;
675 }653 }
676 yield* Effect.annotateCurrentSpan({654 yield* Effect.annotateCurrentSpan({
...@@ -683,11 +661,7 @@ const make = Effect.gen(function* () {...@@ -683,11 +661,7 @@ const make = Effect.gen(function* () {
683 case "live_activity_start":661 case "live_activity_start":
684 case "live_activity_update":662 case "live_activity_update":
685 if (payload.aggregate === null) {663 if (payload.aggregate === null) {
686 return Effect.fail(664 return Effect.fail(new ApnsDeliveryJobLiveActivityAggregateMissing());
687 new ApnsDeliveryJobInvalid({
688 reason: "missing-live-activity-aggregate",
689 }),
690 );
691 }665 }
692 return sendLiveActivity({666 return sendLiveActivity({
693 target: {667 target: {
...@@ -712,11 +686,7 @@ const make = Effect.gen(function* () {...@@ -712,11 +686,7 @@ const make = Effect.gen(function* () {
712 });686 });
713 case "push_notification":687 case "push_notification":
714 if (payload.notification === null) {688 if (payload.notification === null) {
715 return Effect.fail(689 return Effect.fail(new ApnsDeliveryJobPushNotificationMissing());
716 new ApnsDeliveryJobInvalid({
717 reason: "missing-push-notification",
718 }),
719 );
720 }690 }
721 return sendPushNotification({691 return sendPushNotification({
722 target: {692 target: {
infra/relay/src/agentActivity/ApnsDeliveryQueue.ts+1-1
...@@ -59,7 +59,7 @@ export class ApnsDeliveryQueue extends Context.Service<...@@ -59,7 +59,7 @@ export class ApnsDeliveryQueue extends Context.Service<
59 }59 }
60>()("t3code-relay/agentActivity/ApnsDeliveryQueue") {}60>()("t3code-relay/agentActivity/ApnsDeliveryQueue") {}
6161
62const make = Effect.gen(function* () {62export const make = Effect.gen(function* () {
63 const sender = yield* ApnsDeliveryQueueSender;63 const sender = yield* ApnsDeliveryQueueSender;
64 const crypto = yield* Crypto.Crypto;64 const crypto = yield* Crypto.Crypto;
65 const config = yield* RelayConfiguration.RelayConfiguration;65 const config = yield* RelayConfiguration.RelayConfiguration;
infra/relay/src/agentActivity/DeliveryAttempts.ts+1-1
...@@ -82,7 +82,7 @@ function insertValues(...@@ -82,7 +82,7 @@ function insertValues(
82 };82 };
83}83}
8484
85const make = Effect.gen(function* () {85export const make = Effect.gen(function* () {
86 const db = yield* RelayDb.RelayDb;86 const db = yield* RelayDb.RelayDb;
87 const crypto = yield* Crypto.Crypto;87 const crypto = yield* Crypto.Crypto;
8888
infra/relay/src/agentActivity/Devices.ts+1-1
...@@ -57,7 +57,7 @@ export class Devices extends Context.Service<...@@ -57,7 +57,7 @@ export class Devices extends Context.Service<
57 }57 }
58>()("t3code-relay/agentActivity/Devices") {}58>()("t3code-relay/agentActivity/Devices") {}
5959
60const make = Effect.gen(function* () {60export const make = Effect.gen(function* () {
61 const db = yield* RelayDb.RelayDb;61 const db = yield* RelayDb.RelayDb;
6262
63 return Devices.of({63 return Devices.of({
infra/relay/src/agentActivity/LiveActivities.ts+1-1
...@@ -106,7 +106,7 @@ const encodeRelayAgentActivityAggregateStateJson = Schema.encodeEffect(...@@ -106,7 +106,7 @@ const encodeRelayAgentActivityAggregateStateJson = Schema.encodeEffect(
106 Schema.fromJsonString(RelayAgentActivityAggregateStateSchema),106 Schema.fromJsonString(RelayAgentActivityAggregateStateSchema),
107);107);
108108
109const make = Effect.gen(function* () {109export const make = Effect.gen(function* () {
110 const db = yield* RelayDb.RelayDb;110 const db = yield* RelayDb.RelayDb;
111111
112 return LiveActivities.of({112 return LiveActivities.of({
infra/relay/src/agentActivity/MobileRegistrations.ts+1-1
...@@ -33,7 +33,7 @@ export class MobileRegistrations extends Context.Service<...@@ -33,7 +33,7 @@ export class MobileRegistrations extends Context.Service<
33 }33 }
34>()("t3code-relay/agentActivity/MobileRegistrations") {}34>()("t3code-relay/agentActivity/MobileRegistrations") {}
3535
36const make = Effect.gen(function* () {36export const make = Effect.gen(function* () {
37 const devices = yield* Devices.Devices;37 const devices = yield* Devices.Devices;
38 const liveActivities = yield* LiveActivities.LiveActivities;38 const liveActivities = yield* LiveActivities.LiveActivities;
39 const publisher = yield* AgentActivityPublisher.AgentActivityPublisher;39 const publisher = yield* AgentActivityPublisher.AgentActivityPublisher;
infra/relay/src/agentActivity/apnsDeliveryJobs.test.ts+6-6
...@@ -69,7 +69,7 @@ describe("apnsDeliveryJobs", () => {...@@ -69,7 +69,7 @@ describe("apnsDeliveryJobs", () => {
69 });69 });
7070
71 expect(result).toMatchObject({71 expect(result).toMatchObject({
72 _tag: "ApnsDeliveryJobInvalid",72 _tag: "ApnsDeliveryJobSignatureInvalid",
73 });73 });
74 });74 });
7575
...@@ -93,7 +93,7 @@ describe("apnsDeliveryJobs", () => {...@@ -93,7 +93,7 @@ describe("apnsDeliveryJobs", () => {
93 });93 });
9494
95 expect(result).toMatchObject({95 expect(result).toMatchObject({
96 _tag: "ApnsDeliveryJobInvalid",96 _tag: "ApnsDeliveryJobLiveActivityAggregateMissing",
97 message: "Live Activity start/update jobs require an aggregate.",97 message: "Live Activity start/update jobs require an aggregate.",
98 });98 });
99 });99 });
...@@ -119,7 +119,7 @@ describe("apnsDeliveryJobs", () => {...@@ -119,7 +119,7 @@ describe("apnsDeliveryJobs", () => {
119 });119 });
120120
121 expect(result).toMatchObject({121 expect(result).toMatchObject({
122 _tag: "ApnsDeliveryJobInvalid",122 _tag: "ApnsDeliveryJobPushNotificationAggregateUnexpected",
123 message: "Push notification jobs must not carry aggregate state.",123 message: "Push notification jobs must not carry aggregate state.",
124 });124 });
125 });125 });
...@@ -194,7 +194,7 @@ describe("apnsDeliveryJobs", () => {...@@ -194,7 +194,7 @@ describe("apnsDeliveryJobs", () => {
194 nowMs: 0,194 nowMs: 0,
195 }),195 }),
196 ).toMatchObject({196 ).toMatchObject({
197 _tag: "ApnsDeliveryJobInvalid",197 _tag: "ApnsDeliveryJobCreatedAtInvalid",
198 message: "Invalid APNs delivery job creation time.",198 message: "Invalid APNs delivery job creation time.",
199 });199 });
200 expect(200 expect(
...@@ -204,7 +204,7 @@ describe("apnsDeliveryJobs", () => {...@@ -204,7 +204,7 @@ describe("apnsDeliveryJobs", () => {
204 nowMs: 0,204 nowMs: 0,
205 }),205 }),
206 ).toMatchObject({206 ).toMatchObject({
207 _tag: "ApnsDeliveryJobInvalid",207 _tag: "ApnsDeliveryJobTimeWindowInvalid",
208 message: "Invalid APNs delivery job time window.",208 message: "Invalid APNs delivery job time window.",
209 });209 });
210 expect(210 expect(
...@@ -214,7 +214,7 @@ describe("apnsDeliveryJobs", () => {...@@ -214,7 +214,7 @@ describe("apnsDeliveryJobs", () => {
214 nowMs: 0,214 nowMs: 0,
215 }),215 }),
216 ).toMatchObject({216 ).toMatchObject({
217 _tag: "ApnsDeliveryJobInvalid",217 _tag: "ApnsDeliveryJobTimeWindowTooLong",
218 message: "APNs delivery job time window is too long.",218 message: "APNs delivery job time window is too long.",
219 });219 });
220 });220 });
infra/relay/src/agentActivity/apnsDeliveryJobs.ts+116-59
...@@ -49,49 +49,110 @@ export const SignedApnsDeliveryJob = Schema.Struct({...@@ -49,49 +49,110 @@ export const SignedApnsDeliveryJob = Schema.Struct({
49});49});
50export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type;50export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type;
5151
52export class ApnsDeliveryJobInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobInvalid>()(52export class ApnsDeliveryJobQueuePayloadInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobQueuePayloadInvalid>()(
53 "ApnsDeliveryJobInvalid",53 "ApnsDeliveryJobQueuePayloadInvalid",
54 {54 {},
55 reason: Schema.Literals([55) {
56 "invalid-queue-payload",56 override get message(): string {
57 "missing-live-activity-aggregate",57 return "Invalid APNs delivery queue job.";
58 "unexpected-live-activity-notification",58 }
59 "missing-push-notification",59}
60 "unexpected-push-notification-aggregate",60
61 "invalid-created-at",61export class ApnsDeliveryJobLiveActivityAggregateMissing extends Schema.TaggedErrorClass<ApnsDeliveryJobLiveActivityAggregateMissing>()(
62 "invalid-expires-at",62 "ApnsDeliveryJobLiveActivityAggregateMissing",
63 "invalid-time-window",63 {},
64 "time-window-too-long",64) {
65 "invalid-signature",65 override get message(): string {
66 ]),66 return "Live Activity start/update jobs require an aggregate.";
67 },67 }
68}
69
70export class ApnsDeliveryJobLiveActivityNotificationUnexpected extends Schema.TaggedErrorClass<ApnsDeliveryJobLiveActivityNotificationUnexpected>()(
71 "ApnsDeliveryJobLiveActivityNotificationUnexpected",
72 {},
73) {
74 override get message(): string {
75 return "Live Activity jobs must not carry push notification payloads.";
76 }
77}
78
79export class ApnsDeliveryJobPushNotificationMissing extends Schema.TaggedErrorClass<ApnsDeliveryJobPushNotificationMissing>()(
80 "ApnsDeliveryJobPushNotificationMissing",
81 {},
68) {82) {
69 override get message(): string {83 override get message(): string {
70 switch (this.reason) {84 return "Push notification jobs require a notification payload.";
71 case "invalid-queue-payload":
72 return "Invalid APNs delivery queue job.";
73 case "missing-live-activity-aggregate":
74 return "Live Activity start/update jobs require an aggregate.";
75 case "unexpected-live-activity-notification":
76 return "Live Activity jobs must not carry push notification payloads.";
77 case "missing-push-notification":
78 return "Push notification jobs require a notification payload.";
79 case "unexpected-push-notification-aggregate":
80 return "Push notification jobs must not carry aggregate state.";
81 case "invalid-created-at":
82 return "Invalid APNs delivery job creation time.";
83 case "invalid-expires-at":
84 return "Invalid APNs delivery job expiry.";
85 case "invalid-time-window":
86 return "Invalid APNs delivery job time window.";
87 case "time-window-too-long":
88 return "APNs delivery job time window is too long.";
89 case "invalid-signature":
90 return "Invalid APNs delivery job signature.";
91 }
92 }85 }
93}86}
9487
88export class ApnsDeliveryJobPushNotificationAggregateUnexpected extends Schema.TaggedErrorClass<ApnsDeliveryJobPushNotificationAggregateUnexpected>()(
89 "ApnsDeliveryJobPushNotificationAggregateUnexpected",
90 {},
91) {
92 override get message(): string {
93 return "Push notification jobs must not carry aggregate state.";
94 }
95}
96
97export class ApnsDeliveryJobCreatedAtInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobCreatedAtInvalid>()(
98 "ApnsDeliveryJobCreatedAtInvalid",
99 {},
100) {
101 override get message(): string {
102 return "Invalid APNs delivery job creation time.";
103 }
104}
105
106export class ApnsDeliveryJobExpiresAtInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobExpiresAtInvalid>()(
107 "ApnsDeliveryJobExpiresAtInvalid",
108 {},
109) {
110 override get message(): string {
111 return "Invalid APNs delivery job expiry.";
112 }
113}
114
115export class ApnsDeliveryJobTimeWindowInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobTimeWindowInvalid>()(
116 "ApnsDeliveryJobTimeWindowInvalid",
117 {},
118) {
119 override get message(): string {
120 return "Invalid APNs delivery job time window.";
121 }
122}
123
124export class ApnsDeliveryJobTimeWindowTooLong extends Schema.TaggedErrorClass<ApnsDeliveryJobTimeWindowTooLong>()(
125 "ApnsDeliveryJobTimeWindowTooLong",
126 {},
127) {
128 override get message(): string {
129 return "APNs delivery job time window is too long.";
130 }
131}
132
133export class ApnsDeliveryJobSignatureInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobSignatureInvalid>()(
134 "ApnsDeliveryJobSignatureInvalid",
135 {},
136) {
137 override get message(): string {
138 return "Invalid APNs delivery job signature.";
139 }
140}
141
142export const ApnsDeliveryJobInvalid = Schema.Union([
143 ApnsDeliveryJobQueuePayloadInvalid,
144 ApnsDeliveryJobLiveActivityAggregateMissing,
145 ApnsDeliveryJobLiveActivityNotificationUnexpected,
146 ApnsDeliveryJobPushNotificationMissing,
147 ApnsDeliveryJobPushNotificationAggregateUnexpected,
148 ApnsDeliveryJobCreatedAtInvalid,
149 ApnsDeliveryJobExpiresAtInvalid,
150 ApnsDeliveryJobTimeWindowInvalid,
151 ApnsDeliveryJobTimeWindowTooLong,
152 ApnsDeliveryJobSignatureInvalid,
153]);
154export type ApnsDeliveryJobInvalid = typeof ApnsDeliveryJobInvalid.Type;
155
95export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDeliveryJobExpired>()(156export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDeliveryJobExpired>()(
96 "ApnsDeliveryJobExpired",157 "ApnsDeliveryJobExpired",
97 {158 {
...@@ -103,7 +164,13 @@ export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDelivery...@@ -103,7 +164,13 @@ export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDelivery
103 }164 }
104}165}
105166
106export type ApnsDeliveryJobVerificationError = ApnsDeliveryJobInvalid | ApnsDeliveryJobExpired;167export const ApnsDeliveryJobVerificationError = Schema.Union([
168 ApnsDeliveryJobInvalid,
169 ApnsDeliveryJobExpired,
170]);
171export type ApnsDeliveryJobVerificationError = typeof ApnsDeliveryJobVerificationError.Type;
172
173export const isApnsDeliveryJobVerificationError = Schema.is(ApnsDeliveryJobVerificationError);
107174
108export function makeApnsDeliveryJobPayload(input: {175export function makeApnsDeliveryJobPayload(input: {
109 readonly kind: RelayDeliveryKind;176 readonly kind: RelayDeliveryKind;
...@@ -141,33 +208,23 @@ function validatePayloadShape(payload: ApnsDeliveryJobPayload): ApnsDeliveryJobI...@@ -141,33 +208,23 @@ function validatePayloadShape(payload: ApnsDeliveryJobPayload): ApnsDeliveryJobI
141 case "live_activity_start":208 case "live_activity_start":
142 case "live_activity_update":209 case "live_activity_update":
143 if (payload.aggregate === null) {210 if (payload.aggregate === null) {
144 return new ApnsDeliveryJobInvalid({211 return new ApnsDeliveryJobLiveActivityAggregateMissing();
145 reason: "missing-live-activity-aggregate",
146 });
147 }212 }
148 if (payload.notification !== null) {213 if (payload.notification !== null) {
149 return new ApnsDeliveryJobInvalid({214 return new ApnsDeliveryJobLiveActivityNotificationUnexpected();
150 reason: "unexpected-live-activity-notification",
151 });
152 }215 }
153 return null;216 return null;
154 case "live_activity_end":217 case "live_activity_end":
155 if (payload.notification !== null) {218 if (payload.notification !== null) {
156 return new ApnsDeliveryJobInvalid({219 return new ApnsDeliveryJobLiveActivityNotificationUnexpected();
157 reason: "unexpected-live-activity-notification",
158 });
159 }220 }
160 return null;221 return null;
161 case "push_notification":222 case "push_notification":
162 if (payload.notification === null) {223 if (payload.notification === null) {
163 return new ApnsDeliveryJobInvalid({224 return new ApnsDeliveryJobPushNotificationMissing();
164 reason: "missing-push-notification",
165 });
166 }225 }
167 if (payload.aggregate !== null) {226 if (payload.aggregate !== null) {
168 return new ApnsDeliveryJobInvalid({227 return new ApnsDeliveryJobPushNotificationAggregateUnexpected();
169 reason: "unexpected-push-notification-aggregate",
170 });
171 }228 }
172 return null;229 return null;
173 }230 }
...@@ -213,19 +270,19 @@ export function verifySignedApnsDeliveryJob(input: {...@@ -213,19 +270,19 @@ export function verifySignedApnsDeliveryJob(input: {
213 }270 }
214 const createdAt = DateTime.make(input.job.payload.createdAt);271 const createdAt = DateTime.make(input.job.payload.createdAt);
215 if (Option.isNone(createdAt)) {272 if (Option.isNone(createdAt)) {
216 return new ApnsDeliveryJobInvalid({ reason: "invalid-created-at" });273 return new ApnsDeliveryJobCreatedAtInvalid();
217 }274 }
218 const expiresAt = DateTime.make(input.job.payload.expiresAt);275 const expiresAt = DateTime.make(input.job.payload.expiresAt);
219 if (Option.isNone(expiresAt)) {276 if (Option.isNone(expiresAt)) {
220 return new ApnsDeliveryJobInvalid({ reason: "invalid-expires-at" });277 return new ApnsDeliveryJobExpiresAtInvalid();
221 }278 }
222 const createdAtMs = createdAt.value.epochMilliseconds;279 const createdAtMs = createdAt.value.epochMilliseconds;
223 const expiresAtMs = expiresAt.value.epochMilliseconds;280 const expiresAtMs = expiresAt.value.epochMilliseconds;
224 if (expiresAtMs <= createdAtMs) {281 if (expiresAtMs <= createdAtMs) {
225 return new ApnsDeliveryJobInvalid({ reason: "invalid-time-window" });282 return new ApnsDeliveryJobTimeWindowInvalid();
226 }283 }
227 if (expiresAtMs - createdAtMs > MAX_JOB_AGE_MS) {284 if (expiresAtMs - createdAtMs > MAX_JOB_AGE_MS) {
228 return new ApnsDeliveryJobInvalid({ reason: "time-window-too-long" });285 return new ApnsDeliveryJobTimeWindowTooLong();
229 }286 }
230 if (expiresAtMs <= input.nowMs) {287 if (expiresAtMs <= input.nowMs) {
231 return new ApnsDeliveryJobExpired({ expiresAt: input.job.payload.expiresAt });288 return new ApnsDeliveryJobExpired({ expiresAt: input.job.payload.expiresAt });
...@@ -235,7 +292,7 @@ export function verifySignedApnsDeliveryJob(input: {...@@ -235,7 +292,7 @@ export function verifySignedApnsDeliveryJob(input: {
235 payload: input.job.payload,292 payload: input.job.payload,
236 });293 });
237 if (!timingSafeEqualBase64Url(input.job.signature, expected)) {294 if (!timingSafeEqualBase64Url(input.job.signature, expected)) {
238 return new ApnsDeliveryJobInvalid({ reason: "invalid-signature" });295 return new ApnsDeliveryJobSignatureInvalid();
239 }296 }
240 return input.job.payload;297 return input.job.payload;
241}298}
infra/relay/src/http/Api.ts+19-12
...@@ -777,18 +777,17 @@ export const serverApi = HttpApiBuilder.group(...@@ -777,18 +777,17 @@ export const serverApi = HttpApiBuilder.group(
777 reason: "persistence_failed",777 reason: "persistence_failed",
778 traceId,778 traceId,
779 }),779 }),
780 ApnsDeliveryJobInvalid: (_error, traceId) =>780 ApnsDeliveryJobQueuePayloadInvalid: mapApnsDeliveryJobInternalError,
781 new RelayInternalError({781 ApnsDeliveryJobLiveActivityAggregateMissing: mapApnsDeliveryJobInternalError,
782 code: "internal_error",782 ApnsDeliveryJobLiveActivityNotificationUnexpected: mapApnsDeliveryJobInternalError,
783 reason: "internal_error",783 ApnsDeliveryJobPushNotificationMissing: mapApnsDeliveryJobInternalError,
784 traceId,784 ApnsDeliveryJobPushNotificationAggregateUnexpected: mapApnsDeliveryJobInternalError,
785 }),785 ApnsDeliveryJobCreatedAtInvalid: mapApnsDeliveryJobInternalError,
786 ApnsDeliveryJobExpired: (_error, traceId) =>786 ApnsDeliveryJobExpiresAtInvalid: mapApnsDeliveryJobInternalError,
787 new RelayInternalError({787 ApnsDeliveryJobTimeWindowInvalid: mapApnsDeliveryJobInternalError,
788 code: "internal_error",788 ApnsDeliveryJobTimeWindowTooLong: mapApnsDeliveryJobInternalError,
789 reason: "internal_error",789 ApnsDeliveryJobSignatureInvalid: mapApnsDeliveryJobInternalError,
790 traceId,790 ApnsDeliveryJobExpired: mapApnsDeliveryJobInternalError,
791 }),
792 ApnsDeliveryJobClaimInFlight: (_error, traceId) =>791 ApnsDeliveryJobClaimInFlight: (_error, traceId) =>
793 new RelayInternalError({792 new RelayInternalError({
794 code: "internal_error",793 code: "internal_error",
...@@ -893,6 +892,14 @@ function mapRelayCommonApiErrors(authReason: RelayAuthInvalidReason) {...@@ -893,6 +892,14 @@ function mapRelayCommonApiErrors(authReason: RelayAuthInvalidReason) {
893 ): Effect.Effect<A, MapRelayCommonApiError<E>, R> => effect.pipe(Effect.catch(mapError));892 ): Effect.Effect<A, MapRelayCommonApiError<E>, R> => effect.pipe(Effect.catch(mapError));
894}893}
895894
895function mapApnsDeliveryJobInternalError(_error: unknown, traceId: string) {
896 return new RelayInternalError({
897 code: "internal_error",
898 reason: "internal_error",
899 traceId,
900 });
901}
902
896type TaggedErrorTag<E> = Extract<E, { readonly _tag: string }>["_tag"];903type TaggedErrorTag<E> = Extract<E, { readonly _tag: string }>["_tag"];
897904
898type MapErrorTagCases<E> = {905type MapErrorTagCases<E> = {