| 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,7 +39,7 @@ export class AgentActivityPublisher extends Context.Service< |
| 39 | } | 39 | } |
| 40 | >()("t3code-relay/agentActivity/AgentActivityPublisher") {} | 40 | >()("t3code-relay/agentActivity/AgentActivityPublisher") {} |
| 41 | 41 | ||
| 42 | const make = Effect.gen(function* () { | 42 | export 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 | ); |
| 73 | 73 | ||
| 74 | const make = Effect.gen(function* () { | 74 | export const make = Effect.gen(function* () { |
| 75 | const db = yield* RelayDb.RelayDb; | 75 | const db = yield* RelayDb.RelayDb; |
| 76 | 76 | ||
| 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 | } |
| 42 | 42 | ||
| 43 | export class ApnsSigningError extends Schema.TaggedErrorClass<ApnsSigningError>()( | 43 | export 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 | |||
| 52 | export 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 | } |
| 54 | 60 | ||
| ... | @@ -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 | } |
| 65 | 71 | ||
| ... | @@ -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 | } |
| 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 | const decodeApnsErrorResponseJson = Schema.decodeUnknownOption( | 91 | const 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 }))); |
| 116 | 128 | ||
| 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 | }); |
| 135 | 147 | ||
| ... | @@ -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") {} |
| 253 | 265 | ||
| 254 | const make = Effect.gen(function* () { | 266 | export const make = Effect.gen(function* () { |
| 255 | const httpClient = yield* HttpClient.HttpClient; | 267 | const httpClient = yield* HttpClient.HttpClient; |
| 256 | 268 | ||
| 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"; |
| 22 | import * as Apns from "./ApnsClient.ts"; | 22 | import * as Apns from "./ApnsClient.ts"; |
| 23 | import { | 23 | import { |
| 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 | ); |
| 93 | const decodeSignedApnsDeliveryJob = Schema.decodeUnknownEffect(SignedApnsDeliveryJob); | 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 | function parseAggregate(value: string | null): RelayAgentActivityAggregateState | null { | 98 | function 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 | } |
| 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 | function duplicateJobResult(input: { | 268 | function 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") {} |
| 420 | 403 | ||
| 421 | const make = Effect.gen(function* () { | 404 | export 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") {} |
| 61 | 61 | ||
| 62 | const make = Effect.gen(function* () { | 62 | export 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 | } |
| 84 | 84 | ||
| 85 | const make = Effect.gen(function* () { | 85 | export 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; |
| 88 | 88 |
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") {} |
| 59 | 59 | ||
| 60 | const make = Effect.gen(function* () { | 60 | export const make = Effect.gen(function* () { |
| 61 | const db = yield* RelayDb.RelayDb; | 61 | const db = yield* RelayDb.RelayDb; |
| 62 | 62 | ||
| 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 | ); |
| 108 | 108 | ||
| 109 | const make = Effect.gen(function* () { | 109 | export const make = Effect.gen(function* () { |
| 110 | const db = yield* RelayDb.RelayDb; | 110 | const db = yield* RelayDb.RelayDb; |
| 111 | 111 | ||
| 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") {} |
| 35 | 35 | ||
| 36 | const make = Effect.gen(function* () { | 36 | export 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 | }); |
| 70 | 70 | ||
| 71 | expect(result).toMatchObject({ | 71 | expect(result).toMatchObject({ |
| 72 | _tag: "ApnsDeliveryJobInvalid", | 72 | _tag: "ApnsDeliveryJobSignatureInvalid", |
| 73 | }); | 73 | }); |
| 74 | }); | 74 | }); |
| 75 | 75 | ||
| ... | @@ -93,7 +93,7 @@ describe("apnsDeliveryJobs", () => { | ... | @@ -93,7 +93,7 @@ describe("apnsDeliveryJobs", () => { |
| 93 | }); | 93 | }); |
| 94 | 94 | ||
| 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 | }); |
| 120 | 120 | ||
| 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 | }); |
| 50 | export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type; | 50 | export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type; |
| 51 | 51 | ||
| 52 | export class ApnsDeliveryJobInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobInvalid>()( | 52 | export 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", | 61 | export 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 | |||
| 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 | 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 | } |
| 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 | export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDeliveryJobExpired>()( | 156 | export 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 | } |
| 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 | export function makeApnsDeliveryJobPayload(input: { | 175 | export 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 | } |
| 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 | type TaggedErrorTag<E> = Extract<E, { readonly _tag: string }>["_tag"]; | 903 | type TaggedErrorTag<E> = Extract<E, { readonly _tag: string }>["_tag"]; |
| 897 | 904 | ||
| 898 | type MapErrorTagCases<E> = { | 905 | type MapErrorTagCases<E> = { |