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