authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 21:13:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 21:13:01-07:00
logd5a72d5be105d16a95df6dc18ad08f0b0cdb7efb
treee7528f84cd0c7ca83bfa613caff790db2707760e
parent742dd7af0dcb54c32774f87fd354105726c7a9db
signature Signed by PGP key B5690EEEBB952194

[codex] align relay agent activity Effect services (#3179)

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

13 files changed, 320 insertions(+), 285 deletions(-)

infra/relay/src/agentActivity/AgentActivityPublisher.test.ts+17-14
......@@ -41,8 +41,8 @@ function target(deviceId: string): LiveActivities.TargetRow {
4141}
4242
4343function makeLiveActivities(
44 overrides: Partial<LiveActivities.LiveActivitiesShape> = {},
45): LiveActivities.LiveActivitiesShape {
44 overrides: Partial<LiveActivities.LiveActivities["Service"]> = {},
45): LiveActivities.LiveActivities["Service"] {
4646 return {
4747 register: () => Effect.void,
4848 listTargets: () => Effect.succeed([]),
......@@ -55,8 +55,8 @@ function makeLiveActivities(
5555}
5656
5757function makeAgentActivityRows(
58 overrides: Partial<AgentActivityRows.AgentActivityRowsShape> = {},
59): AgentActivityRows.AgentActivityRowsShape {
58 overrides: Partial<AgentActivityRows.AgentActivityRows["Service"]> = {},
59): AgentActivityRows.AgentActivityRows["Service"] {
6060 return {
6161 upsert: () => Effect.void,
6262 remove: () => Effect.void,
......@@ -88,8 +88,8 @@ function makeEnvironmentLinks(
8888}
8989
9090function makeApnsDeliveries(
91 overrides: Partial<ApnsDeliveries.ApnsDeliveriesShape> = {},
92): ApnsDeliveries.ApnsDeliveriesShape {
91 overrides: Partial<ApnsDeliveries.ApnsDeliveries["Service"]> = {},
92): ApnsDeliveries.ApnsDeliveries["Service"] {
9393 return {
9494 sendForTarget: () => Effect.succeed(null),
9595 sendPushNotificationForTarget: () => Effect.succeed(null),
......@@ -133,7 +133,8 @@ describe("AgentActivityPublisher", () => {
133133 remote_start_queued_at: null,
134134 remote_started_at: "1970-01-01T00:00:01.000Z",
135135 };
136 const sent: Array<Parameters<ApnsDeliveries.ApnsDeliveriesShape["sendForTarget"]>[0]> = [];
136 const sent: Array<Parameters<ApnsDeliveries.ApnsDeliveries["Service"]["sendForTarget"]>[0]> =
137 [];
137138 const deliveryResult: RelayDeliveryResult = {
138139 deviceId: "device-1",
139140 kind: "live_activity_update",
......@@ -211,7 +212,8 @@ describe("AgentActivityPublisher", () => {
211212 readonly environmentId: string;
212213 readonly environmentPublicKey: string;
213214 }> = [];
214 const upserts: Array<Parameters<AgentActivityRows.AgentActivityRowsShape["upsert"]>[0]> = [];
215 const upserts: Array<Parameters<AgentActivityRows.AgentActivityRows["Service"]["upsert"]>[0]> =
216 [];
215217
216218 return Effect.gen(function* () {
217219 const result = yield* Effect.gen(function* () {
......@@ -302,9 +304,10 @@ describe("AgentActivityPublisher", () => {
302304 updatedAt: "1970-01-01T00:00:10.000Z",
303305 };
304306 const sentAggregates: Array<
305 Parameters<ApnsDeliveries.ApnsDeliveriesShape["sendForTarget"]>[0]
307 Parameters<ApnsDeliveries.ApnsDeliveries["Service"]["sendForTarget"]>[0]
306308 > = [];
307 const removals: Array<Parameters<AgentActivityRows.AgentActivityRowsShape["remove"]>[0]> = [];
309 const removals: Array<Parameters<AgentActivityRows.AgentActivityRows["Service"]["remove"]>[0]> =
310 [];
308311
309312 return Effect.gen(function* () {
310313 const result = yield* Effect.gen(function* () {
......@@ -405,10 +408,10 @@ describe("AgentActivityPublisher", () => {
405408 headline: "Needs input",
406409 };
407410 const liveAggregates: Array<
408 Parameters<ApnsDeliveries.ApnsDeliveriesShape["sendForTarget"]>[0]
411 Parameters<ApnsDeliveries.ApnsDeliveries["Service"]["sendForTarget"]>[0]
409412 > = [];
410413 const pushAggregates: Array<
411 Parameters<ApnsDeliveries.ApnsDeliveriesShape["sendPushNotificationForTarget"]>[0]
414 Parameters<ApnsDeliveries.ApnsDeliveries["Service"]["sendPushNotificationForTarget"]>[0]
412415 > = [];
413416
414417 return Effect.gen(function* () {
......@@ -517,10 +520,10 @@ describe("AgentActivityPublisher", () => {
517520 headline: "Needs approval",
518521 };
519522 const liveAggregates: Array<
520 Parameters<ApnsDeliveries.ApnsDeliveriesShape["sendForTarget"]>[0]
523 Parameters<ApnsDeliveries.ApnsDeliveries["Service"]["sendForTarget"]>[0]
521524 > = [];
522525 const pushAggregates: Array<
523 Parameters<ApnsDeliveries.ApnsDeliveriesShape["sendPushNotificationForTarget"]>[0]
526 Parameters<ApnsDeliveries.ApnsDeliveries["Service"]["sendPushNotificationForTarget"]>[0]
524527 > = [];
525528
526529 return Effect.gen(function* () {
infra/relay/src/agentActivity/AgentActivityPublisher.ts+12-14
......@@ -23,22 +23,20 @@ export type AgentActivityPublishError =
2323 | LiveActivities.LiveActivityTargetListPersistenceError
2424 | ApnsDeliveries.ApnsDeliveryError;
2525
26export interface AgentActivityPublisherShape {
27 readonly publish: (input: {
28 readonly environmentId: string;
29 readonly environmentPublicKey: string;
30 readonly threadId: string;
31 readonly state: RelayAgentActivityState | null;
32 }) => Effect.Effect<RelayPublishResponse, AgentActivityPublishError>;
33 readonly replayForLiveActivityRegistration: (input: {
34 readonly userId: string;
35 readonly deviceId: string;
36 }) => Effect.Effect<RelayDeliveryResult | null, AgentActivityPublishError>;
37}
38
3926export class AgentActivityPublisher extends Context.Service<
4027 AgentActivityPublisher,
41 AgentActivityPublisherShape
28 {
29 readonly publish: (input: {
30 readonly environmentId: string;
31 readonly environmentPublicKey: string;
32 readonly threadId: string;
33 readonly state: RelayAgentActivityState | null;
34 }) => Effect.Effect<RelayPublishResponse, AgentActivityPublishError>;
35 readonly replayForLiveActivityRegistration: (input: {
36 readonly userId: string;
37 readonly deviceId: string;
38 }) => Effect.Effect<RelayDeliveryResult | null, AgentActivityPublishError>;
39 }
4240>()("t3code-relay/agentActivity/AgentActivityPublisher") {}
4341
4442const make = Effect.gen(function* () {
infra/relay/src/agentActivity/AgentActivityRows.ts+22-20
......@@ -3,7 +3,7 @@ import { RelayAgentActivityState as RelayAgentActivityStateSchema } from "@t3too
33import * as Context from "effect/Context";
44import * as DateTime from "effect/DateTime";
55import * as Effect from "effect/Effect";
6import { cast } from "effect/Function";
6import * as Function from "effect/Function";
77import * as Layer from "effect/Layer";
88import * as Option from "effect/Option";
99import * as Schema from "effect/Schema";
......@@ -39,24 +39,26 @@ export class AgentActivityRowListPersistenceError extends Schema.TaggedErrorClas
3939 }
4040}
4141
42export interface AgentActivityRowsShape {
43 readonly upsert: (input: {
44 readonly environmentPublicKey: string;
45 readonly state: RelayAgentActivityState;
46 }) => Effect.Effect<void, AgentActivityRowUpsertPersistenceError>;
47 readonly remove: (input: {
48 readonly environmentId: string;
49 readonly environmentPublicKey: string;
50 readonly threadId: string;
51 }) => Effect.Effect<void, AgentActivityRowDeletePersistenceError>;
52 readonly listForUser: (input: {
53 readonly userId: string;
54 }) => Effect.Effect<ReadonlyArray<RelayAgentActivityState>, AgentActivityRowListPersistenceError>;
55}
56
57export class AgentActivityRows extends Context.Service<AgentActivityRows, AgentActivityRowsShape>()(
58 "t3code-relay/agentActivity/AgentActivityRows",
59) {}
42export class AgentActivityRows extends Context.Service<
43 AgentActivityRows,
44 {
45 readonly upsert: (input: {
46 readonly environmentPublicKey: string;
47 readonly state: RelayAgentActivityState;
48 }) => Effect.Effect<void, AgentActivityRowUpsertPersistenceError>;
49 readonly remove: (input: {
50 readonly environmentId: string;
51 readonly environmentPublicKey: string;
52 readonly threadId: string;
53 }) => Effect.Effect<void, AgentActivityRowDeletePersistenceError>;
54 readonly listForUser: (input: {
55 readonly userId: string;
56 }) => Effect.Effect<
57 ReadonlyArray<RelayAgentActivityState>,
58 AgentActivityRowListPersistenceError
59 >;
60 }
61>()("t3code-relay/agentActivity/AgentActivityRows") {}
6062
6163const decodeJsonString = Schema.decodeEffect(Schema.UnknownFromJsonString);
6264const encodeJsonValue = Schema.encodeEffect(Schema.UnknownFromJsonString);
......@@ -82,7 +84,7 @@ const make = Effect.gen(function* () {
8284 const now = yield* DateTime.now;
8385 const stateJson = yield* encodeRelayAgentActivityStateJson(input.state).pipe(
8486 Effect.flatMap(decodeJsonString),
85 Effect.map(cast<unknown, RelayAgentActivityState>),
87 Effect.map(Function.cast<unknown, RelayAgentActivityState>),
8688 );
8789 yield* db
8890 .insert(relayAgentActivityRows)
infra/relay/src/agentActivity/ApnsClient.ts+56-54
......@@ -8,8 +8,10 @@ import * as Layer from "effect/Layer";
88import * as Option from "effect/Option";
99import * as Redacted from "effect/Redacted";
1010import * as Schema from "effect/Schema";
11import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http";
12import * as RelayConfiguration from "../Config.ts";
11import * as Headers from "effect/unstable/http/Headers";
12import * as HttpClient from "effect/unstable/http/HttpClient";
13import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
14import type * as RelayConfiguration from "../Config.ts";
1315import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts";
1416
1517const LIVE_ACTIVITY_NAME = "AgentActivity";
......@@ -231,29 +233,28 @@ function apnsReasonFromBody(body: string): string | undefined {
231233 });
232234}
233235
234export interface ApnsClientShape {
235 readonly makeLiveActivityRequest: typeof makeLiveActivityRequest;
236 readonly makePushNotificationRequest: typeof makePushNotificationRequest;
237 readonly sendLiveActivityRequest: (input: {
238 readonly credentials: RelayConfiguration.ApnsCredentials;
239 readonly request: ApnsLiveActivityRequest;
240 readonly issuedAtUnixSeconds: number;
241 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;
242 readonly sendPushNotificationRequest: (input: {
243 readonly credentials: RelayConfiguration.ApnsCredentials;
244 readonly request: ApnsPushNotificationRequest;
245 readonly issuedAtUnixSeconds: number;
246 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;
247}
248
249export class ApnsClient extends Context.Service<ApnsClient, ApnsClientShape>()(
250 "t3code-relay/agentActivity/ApnsClient",
251) {}
236export class ApnsClient extends Context.Service<
237 ApnsClient,
238 {
239 readonly makeLiveActivityRequest: typeof makeLiveActivityRequest;
240 readonly makePushNotificationRequest: typeof makePushNotificationRequest;
241 readonly sendLiveActivityRequest: (input: {
242 readonly credentials: RelayConfiguration.ApnsCredentials;
243 readonly request: ApnsLiveActivityRequest;
244 readonly issuedAtUnixSeconds: number;
245 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;
246 readonly sendPushNotificationRequest: (input: {
247 readonly credentials: RelayConfiguration.ApnsCredentials;
248 readonly request: ApnsPushNotificationRequest;
249 readonly issuedAtUnixSeconds: number;
250 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;
251 }
252>()("t3code-relay/agentActivity/ApnsClient") {}
252253
253254const make = Effect.gen(function* () {
254255 const httpClient = yield* HttpClient.HttpClient;
255256
256 const sendLiveActivityRequest: ApnsClientShape["sendLiveActivityRequest"] = Effect.fn(
257 const sendLiveActivityRequest: ApnsClient["Service"]["sendLiveActivityRequest"] = Effect.fn(
257258 "relay.apns.send_live_activity_request",
258259 )(function* (input) {
259260 yield* Effect.annotateCurrentSpan({ "relay.apns.event": input.request.event });
......@@ -288,40 +289,41 @@ const make = Effect.gen(function* () {
288289 };
289290 });
290291
291 const sendPushNotificationRequest: ApnsClientShape["sendPushNotificationRequest"] = Effect.fn(
292 "relay.apns.send_push_notification_request",
293 )(function* (input) {
294 yield* Effect.annotateCurrentSpan({ "relay.apns.event": "push_notification" });
295 const jwt = yield* makeApnsJwt({
296 ...input.credentials,
297 issuedAtUnixSeconds: input.issuedAtUnixSeconds,
292 const sendPushNotificationRequest: ApnsClient["Service"]["sendPushNotificationRequest"] =
293 Effect.fn("relay.apns.send_push_notification_request")(function* (input) {
294 yield* Effect.annotateCurrentSpan({ "relay.apns.event": "push_notification" });
295 const jwt = yield* makeApnsJwt({
296 ...input.credentials,
297 issuedAtUnixSeconds: input.issuedAtUnixSeconds,
298 });
299 const host =
300 input.credentials.environment === "production"
301 ? "https://api.push.apple.com"
302 : "https://api.sandbox.push.apple.com";
303 const response = yield* HttpClientRequest.post(
304 `${host}/3/device/${input.request.token}`,
305 ).pipe(
306 HttpClientRequest.setHeaders({
307 authorization: `bearer ${jwt}`,
308 "apns-priority": input.request.priority,
309 "apns-push-type": "alert",
310 "apns-topic": input.credentials.bundleId,
311 }),
312 HttpClientRequest.bodyJson(input.request.payload),
313 Effect.flatMap(httpClient.execute),
314 Effect.mapError((cause) => new ApnsHttpRequestError({ cause })),
315 );
316 const responseText = yield* response.text.pipe(
317 Effect.mapError((cause) => new ApnsHttpRequestError({ cause })),
318 );
319 const reason = apnsReasonFromBody(responseText);
320 return {
321 ok: response.status >= 200 && response.status < 300,
322 status: response.status,
323 ...(reason === undefined ? {} : { reason }),
324 apnsId: Option.getOrNull(Headers.get(response.headers, "apns-id")),
325 };
298326 });
299 const host =
300 input.credentials.environment === "production"
301 ? "https://api.push.apple.com"
302 : "https://api.sandbox.push.apple.com";
303 const response = yield* HttpClientRequest.post(`${host}/3/device/${input.request.token}`).pipe(
304 HttpClientRequest.setHeaders({
305 authorization: `bearer ${jwt}`,
306 "apns-priority": input.request.priority,
307 "apns-push-type": "alert",
308 "apns-topic": input.credentials.bundleId,
309 }),
310 HttpClientRequest.bodyJson(input.request.payload),
311 Effect.flatMap(httpClient.execute),
312 Effect.mapError((cause) => new ApnsHttpRequestError({ cause })),
313 );
314 const responseText = yield* response.text.pipe(
315 Effect.mapError((cause) => new ApnsHttpRequestError({ cause })),
316 );
317 const reason = apnsReasonFromBody(responseText);
318 return {
319 ok: response.status >= 200 && response.status < 300,
320 status: response.status,
321 ...(reason === undefined ? {} : { reason }),
322 apnsId: Option.getOrNull(Headers.get(response.headers, "apns-id")),
323 };
324 });
325327
326328 return ApnsClient.of({
327329 makeLiveActivityRequest,
infra/relay/src/agentActivity/ApnsDeliveries.test.ts+10-10
......@@ -141,16 +141,16 @@ function makeLayer(input: {
141141 readonly sourceJobClaims?: ReadonlyMap<string, DeliveryAttempts.DeliverySourceJobClaimResult>;
142142 readonly queuedJobs?: Array<SignedApnsDeliveryJob>;
143143 readonly queuedStarts?: Array<
144 Parameters<LiveActivities.LiveActivitiesShape["markStartQueued"]>[0]
144 Parameters<LiveActivities.LiveActivities["Service"]["markStartQueued"]>[0]
145145 >;
146146 readonly clearedStarts?: Array<
147 Parameters<LiveActivities.LiveActivitiesShape["clearStartQueued"]>[0]
147 Parameters<LiveActivities.LiveActivities["Service"]["clearStartQueued"]>[0]
148148 >;
149149 readonly markedDeliveries?: Array<
150 Parameters<LiveActivities.LiveActivitiesShape["markDelivery"]>[0]
150 Parameters<LiveActivities.LiveActivities["Service"]["markDelivery"]>[0]
151151 >;
152152 readonly invalidatedTokens?: Array<
153 Parameters<LiveActivities.LiveActivitiesShape["invalidateDeliveryToken"]>[0]
153 Parameters<LiveActivities.LiveActivities["Service"]["invalidateDeliveryToken"]>[0]
154154 >;
155155 readonly currentTargets?: ReadonlyArray<LiveActivities.TargetRow>;
156156 readonly config?: RelayConfiguration.RelayConfiguration["Service"];
......@@ -227,10 +227,10 @@ describe("ApnsDeliveries", () => {
227227 const attempts: Array<DeliveryAttempts.DeliveryAttemptInput> = [];
228228 const queuedJobs: Array<SignedApnsDeliveryJob> = [];
229229 const queuedStarts: Array<
230 Parameters<LiveActivities.LiveActivitiesShape["markStartQueued"]>[0]
230 Parameters<LiveActivities.LiveActivities["Service"]["markStartQueued"]>[0]
231231 > = [];
232232 const markedDeliveries: Array<
233 Parameters<LiveActivities.LiveActivitiesShape["markDelivery"]>[0]
233 Parameters<LiveActivities.LiveActivities["Service"]["markDelivery"]>[0]
234234 > = [];
235235
236236 return Effect.gen(function* () {
......@@ -933,7 +933,7 @@ describe("ApnsDeliveries", () => {
933933 it.effect("invalidates dead device push tokens after permanent APNs alert failures", () => {
934934 const attempts: Array<DeliveryAttempts.DeliveryAttemptInput> = [];
935935 const invalidatedTokens: Array<
936 Parameters<LiveActivities.LiveActivitiesShape["invalidateDeliveryToken"]>[0]
936 Parameters<LiveActivities.LiveActivities["Service"]["invalidateDeliveryToken"]>[0]
937937 > = [];
938938 const payload = makeApnsDeliveryJobPayload({
939939 kind: "push_notification",
......@@ -1000,7 +1000,7 @@ describe("ApnsDeliveries", () => {
10001000 it.effect("clears queued start state when a start job fails in APNs", () => {
10011001 const attempts: Array<DeliveryAttempts.DeliveryAttemptInput> = [];
10021002 const clearedStarts: Array<
1003 Parameters<LiveActivities.LiveActivitiesShape["clearStartQueued"]>[0]
1003 Parameters<LiveActivities.LiveActivities["Service"]["clearStartQueued"]>[0]
10041004 > = [];
10051005 const payload = makeApnsDeliveryJobPayload({
10061006 kind: "live_activity_start",
......@@ -1035,7 +1035,7 @@ describe("ApnsDeliveries", () => {
10351035 it.effect("invalidates dead push-to-start tokens after permanent APNs start failures", () => {
10361036 const attempts: Array<DeliveryAttempts.DeliveryAttemptInput> = [];
10371037 const invalidatedTokens: Array<
1038 Parameters<LiveActivities.LiveActivitiesShape["invalidateDeliveryToken"]>[0]
1038 Parameters<LiveActivities.LiveActivities["Service"]["invalidateDeliveryToken"]>[0]
10391039 > = [];
10401040 const payload = makeApnsDeliveryJobPayload({
10411041 kind: "live_activity_start",
......@@ -1082,7 +1082,7 @@ describe("ApnsDeliveries", () => {
10821082 it.effect("invalidates dead Live Activity tokens after APNs unregisters them", () => {
10831083 const attempts: Array<DeliveryAttempts.DeliveryAttemptInput> = [];
10841084 const invalidatedTokens: Array<
1085 Parameters<LiveActivities.LiveActivitiesShape["invalidateDeliveryToken"]>[0]
1085 Parameters<LiveActivities.LiveActivities["Service"]["invalidateDeliveryToken"]>[0]
10861086 > = [];
10871087 const payload = makeApnsDeliveryJobPayload({
10881088 kind: "live_activity_update",
infra/relay/src/agentActivity/ApnsDeliveries.ts+33-34
......@@ -356,7 +356,7 @@ export type SendLiveActivityDeliveryInput =
356356 });
357357
358358function makeLiveActivityDeliveryRequest(
359 apns: Apns.ApnsClientShape,
359 apns: Apns.ApnsClient["Service"],
360360 input: SendLiveActivityDeliveryInput,
361361 now: DateTime.DateTime,
362362) {
......@@ -391,33 +391,32 @@ function makeLiveActivityDeliveryRequest(
391391 }
392392}
393393
394export interface ApnsDeliveriesShape {
395 readonly sendForTarget: (input: {
396 readonly target: LiveActivities.TargetRow;
397 readonly aggregate: RelayAgentActivityAggregateState | null;
398 readonly nowMs: number;
399 }) => Effect.Effect<RelayDeliveryResult | null, ApnsDeliveryError>;
400 readonly sendPushNotificationForTarget: (input: {
401 readonly target: LiveActivities.TargetRow;
402 readonly aggregate: RelayAgentActivityAggregateState | null;
403 }) => Effect.Effect<RelayDeliveryResult | null, ApnsDeliveryError>;
404 readonly sendLiveActivity: (
405 input: SendLiveActivityDeliveryInput,
406 ) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryError>;
407 readonly processSignedJob: (
408 body: unknown,
409 ) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryError>;
410 readonly sendPushNotification: (input: {
411 readonly target: LiveActivityDeliveryTarget;
412 readonly token: string;
413 readonly sourceJobId?: string | null;
414 readonly notification: ApnsNotificationPayload;
415 }) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryError>;
416}
417
418export class ApnsDeliveries extends Context.Service<ApnsDeliveries, ApnsDeliveriesShape>()(
419 "t3code-relay/agentActivity/ApnsDeliveries",
420) {}
394export class ApnsDeliveries extends Context.Service<
395 ApnsDeliveries,
396 {
397 readonly sendForTarget: (input: {
398 readonly target: LiveActivities.TargetRow;
399 readonly aggregate: RelayAgentActivityAggregateState | null;
400 readonly nowMs: number;
401 }) => Effect.Effect<RelayDeliveryResult | null, ApnsDeliveryError>;
402 readonly sendPushNotificationForTarget: (input: {
403 readonly target: LiveActivities.TargetRow;
404 readonly aggregate: RelayAgentActivityAggregateState | null;
405 }) => Effect.Effect<RelayDeliveryResult | null, ApnsDeliveryError>;
406 readonly sendLiveActivity: (
407 input: SendLiveActivityDeliveryInput,
408 ) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryError>;
409 readonly processSignedJob: (
410 body: unknown,
411 ) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryError>;
412 readonly sendPushNotification: (input: {
413 readonly target: LiveActivityDeliveryTarget;
414 readonly token: string;
415 readonly sourceJobId?: string | null;
416 readonly notification: ApnsNotificationPayload;
417 }) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryError>;
418 }
419>()("t3code-relay/agentActivity/ApnsDeliveries") {}
421420
422421const make = Effect.gen(function* () {
423422 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
......@@ -442,7 +441,7 @@ const make = Effect.gen(function* () {
442441 );
443442 });
444443
445 const sendLiveActivity: ApnsDeliveriesShape["sendLiveActivity"] = Effect.fn(
444 const sendLiveActivity: ApnsDeliveries["Service"]["sendLiveActivity"] = Effect.fn(
446445 "relay.apns_deliveries.send_live_activity",
447446 )(function* (input) {
448447 yield* Effect.annotateCurrentSpan({
......@@ -550,7 +549,7 @@ const make = Effect.gen(function* () {
550549 };
551550 });
552551
553 const sendPushNotification: ApnsDeliveriesShape["sendPushNotification"] = Effect.fn(
552 const sendPushNotification: ApnsDeliveries["Service"]["sendPushNotification"] = Effect.fn(
554553 "relay.apns_deliveries.send_push_notification",
555554 )(function* (input) {
556555 yield* Effect.annotateCurrentSpan({
......@@ -654,14 +653,14 @@ const make = Effect.gen(function* () {
654653 };
655654 });
656655
657 const processSignedJob: ApnsDeliveriesShape["processSignedJob"] = Effect.fn(
656 const processSignedJob: ApnsDeliveries["Service"]["processSignedJob"] = Effect.fn(
658657 "relay.apns_deliveries.process_signed_job",
659658 )(function* (body) {
660659 const signedJob = yield* decodeSignedApnsDeliveryJob(body).pipe(
661660 Effect.mapError(
662661 () =>
663662 new ApnsDeliveryJobInvalid({
664 message: "Invalid APNs delivery queue job.",
663 reason: "invalid-queue-payload",
665664 }),
666665 ),
667666 );
......@@ -686,7 +685,7 @@ const make = Effect.gen(function* () {
686685 if (payload.aggregate === null) {
687686 return Effect.fail(
688687 new ApnsDeliveryJobInvalid({
689 message: "Live Activity start/update jobs require an aggregate.",
688 reason: "missing-live-activity-aggregate",
690689 }),
691690 );
692691 }
......@@ -715,7 +714,7 @@ const make = Effect.gen(function* () {
715714 if (payload.notification === null) {
716715 return Effect.fail(
717716 new ApnsDeliveryJobInvalid({
718 message: "Push notification jobs require a notification payload.",
717 reason: "missing-push-notification",
719718 }),
720719 );
721720 }
infra/relay/src/agentActivity/ApnsDeliveryQueue.ts+21-24
......@@ -33,34 +33,31 @@ export class ApnsDeliveryQueueSendError extends Schema.TaggedErrorClass<ApnsDeli
3333
3434export type ApnsDeliveryQueueError = ApnsDeliveryQueueSendError;
3535
36export interface ApnsDeliveryQueueSenderShape {
37 readonly send: (body: SignedApnsDeliveryJob) => Effect.Effect<void, ApnsDeliveryQueueSendError>;
38}
39
4036export class ApnsDeliveryQueueSender extends Context.Service<
4137 ApnsDeliveryQueueSender,
42 ApnsDeliveryQueueSenderShape
38 {
39 readonly send: (body: SignedApnsDeliveryJob) => Effect.Effect<void, ApnsDeliveryQueueSendError>;
40 }
4341>()("t3code-relay/agentActivity/ApnsDeliveryQueue/ApnsDeliveryQueueSender") {}
4442
45export interface ApnsDeliveryQueueShape {
46 readonly enqueueLiveActivity: (input: {
47 readonly kind: ApnsDeliveryJobPayload["kind"];
48 readonly userId: string;
49 readonly deviceId: string;
50 readonly token: string;
51 readonly aggregate: ApnsDeliveryJobPayload["aggregate"];
52 }) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryQueueError>;
53 readonly enqueuePushNotification: (input: {
54 readonly userId: string;
55 readonly deviceId: string;
56 readonly token: string;
57 readonly notification: NonNullable<ApnsDeliveryJobPayload["notification"]>;
58 }) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryQueueError>;
59}
60
61export class ApnsDeliveryQueue extends Context.Service<ApnsDeliveryQueue, ApnsDeliveryQueueShape>()(
62 "t3code-relay/agentActivity/ApnsDeliveryQueue",
63) {}
43export class ApnsDeliveryQueue extends Context.Service<
44 ApnsDeliveryQueue,
45 {
46 readonly enqueueLiveActivity: (input: {
47 readonly kind: ApnsDeliveryJobPayload["kind"];
48 readonly userId: string;
49 readonly deviceId: string;
50 readonly token: string;
51 readonly aggregate: ApnsDeliveryJobPayload["aggregate"];
52 }) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryQueueError>;
53 readonly enqueuePushNotification: (input: {
54 readonly userId: string;
55 readonly deviceId: string;
56 readonly token: string;
57 readonly notification: NonNullable<ApnsDeliveryJobPayload["notification"]>;
58 }) => Effect.Effect<RelayDeliveryResult, ApnsDeliveryQueueError>;
59 }
60>()("t3code-relay/agentActivity/ApnsDeliveryQueue") {}
6461
6562const make = Effect.gen(function* () {
6663 const sender = yield* ApnsDeliveryQueueSender;
infra/relay/src/agentActivity/DeliveryAttempts.ts+14-15
......@@ -43,21 +43,20 @@ export interface DeliveryAttemptCompletionInput {
4343
4444export type DeliverySourceJobClaimResult = "claimed" | "completed" | "in_flight";
4545
46export interface DeliveryAttemptsShape {
47 readonly record: (
48 input: DeliveryAttemptInput,
49 ) => Effect.Effect<void, DeliveryAttemptRecordPersistenceError>;
50 readonly claimSourceJob: (
51 input: DeliveryAttemptInput & { readonly sourceJobId: string },
52 ) => Effect.Effect<DeliverySourceJobClaimResult, DeliveryAttemptRecordPersistenceError>;
53 readonly completeSourceJob: (
54 input: DeliveryAttemptCompletionInput,
55 ) => Effect.Effect<void, DeliveryAttemptRecordPersistenceError>;
56}
57
58export class DeliveryAttempts extends Context.Service<DeliveryAttempts, DeliveryAttemptsShape>()(
59 "t3code-relay/agentActivity/DeliveryAttempts",
60) {}
46export class DeliveryAttempts extends Context.Service<
47 DeliveryAttempts,
48 {
49 readonly record: (
50 input: DeliveryAttemptInput,
51 ) => Effect.Effect<void, DeliveryAttemptRecordPersistenceError>;
52 readonly claimSourceJob: (
53 input: DeliveryAttemptInput & { readonly sourceJobId: string },
54 ) => Effect.Effect<DeliverySourceJobClaimResult, DeliveryAttemptRecordPersistenceError>;
55 readonly completeSourceJob: (
56 input: DeliveryAttemptCompletionInput,
57 ) => Effect.Effect<void, DeliveryAttemptRecordPersistenceError>;
58 }
59>()("t3code-relay/agentActivity/DeliveryAttempts") {}
6160
6261const SOURCE_JOB_CLAIM_LEASE_MINUTES = 10;
6362
infra/relay/src/agentActivity/Devices.ts+16-17
......@@ -40,23 +40,22 @@ export class DeviceListPersistenceError extends Schema.TaggedErrorClass<DeviceLi
4040 }
4141}
4242
43export interface DevicesShape {
44 readonly register: (input: {
45 readonly userId: string;
46 readonly registration: RelayDeviceRegistrationRequest;
47 }) => Effect.Effect<void, DeviceRegistrationPersistenceError>;
48 readonly unregister: (input: {
49 readonly userId: string;
50 readonly deviceId: string;
51 }) => Effect.Effect<void, DeviceUnregistrationPersistenceError>;
52 readonly listForUser: (input: {
53 readonly userId: string;
54 }) => Effect.Effect<ReadonlyArray<RelayClientDeviceRecord>, DeviceListPersistenceError>;
55}
56
57export class Devices extends Context.Service<Devices, DevicesShape>()(
58 "t3code-relay/agentActivity/Devices",
59) {}
43export class Devices extends Context.Service<
44 Devices,
45 {
46 readonly register: (input: {
47 readonly userId: string;
48 readonly registration: RelayDeviceRegistrationRequest;
49 }) => Effect.Effect<void, DeviceRegistrationPersistenceError>;
50 readonly unregister: (input: {
51 readonly userId: string;
52 readonly deviceId: string;
53 }) => Effect.Effect<void, DeviceUnregistrationPersistenceError>;
54 readonly listForUser: (input: {
55 readonly userId: string;
56 }) => Effect.Effect<ReadonlyArray<RelayClientDeviceRecord>, DeviceListPersistenceError>;
57 }
58>()("t3code-relay/agentActivity/Devices") {}
6059
6160const make = Effect.gen(function* () {
6261 const db = yield* RelayDb.RelayDb;
infra/relay/src/agentActivity/LiveActivities.ts+36-37
......@@ -7,7 +7,7 @@ import { RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSch
77import * as Context from "effect/Context";
88import * as DateTime from "effect/DateTime";
99import * as Effect from "effect/Effect";
10import { cast } from "effect/Function";
10import * as Function from "effect/Function";
1111import * as Layer from "effect/Layer";
1212import * as Schema from "effect/Schema";
1313import { and, eq, sql } from "drizzle-orm";
......@@ -64,41 +64,40 @@ export interface LiveActivityRow {
6464
6565export type TargetRow = DeviceRow & LiveActivityRow;
6666
67export interface LiveActivitiesShape {
68 readonly register: (input: {
69 readonly userId: string;
70 readonly registration: RelayLiveActivityRegistrationRequest;
71 }) => Effect.Effect<void, LiveActivityRegistrationPersistenceError>;
72 readonly listTargets: (input: {
73 readonly userId: string;
74 }) => Effect.Effect<ReadonlyArray<TargetRow>, LiveActivityTargetListPersistenceError>;
75 readonly markDelivery: (input: {
76 readonly userId: string;
77 readonly deviceId: string;
78 readonly kind: RelayDeliveryKind;
79 readonly aggregate: RelayAgentActivityAggregateState | null;
80 readonly deliveredAt: string;
81 }) => Effect.Effect<void, LiveActivityDeliveryMarkPersistenceError>;
82 readonly markStartQueued: (input: {
83 readonly userId: string;
84 readonly deviceId: string;
85 readonly queuedAt: string;
86 }) => Effect.Effect<void, LiveActivityDeliveryMarkPersistenceError>;
87 readonly clearStartQueued: (input: {
88 readonly userId: string;
89 readonly deviceId: string;
90 }) => Effect.Effect<void, LiveActivityDeliveryMarkPersistenceError>;
91 readonly invalidateDeliveryToken: (input: {
92 readonly userId: string;
93 readonly deviceId: string;
94 readonly kind: RelayDeliveryKind;
95 readonly invalidatedAt: string;
96 }) => Effect.Effect<void, LiveActivityDeliveryMarkPersistenceError>;
97}
98
99export class LiveActivities extends Context.Service<LiveActivities, LiveActivitiesShape>()(
100 "t3code-relay/agentActivity/LiveActivities",
101) {}
67export class LiveActivities extends Context.Service<
68 LiveActivities,
69 {
70 readonly register: (input: {
71 readonly userId: string;
72 readonly registration: RelayLiveActivityRegistrationRequest;
73 }) => Effect.Effect<void, LiveActivityRegistrationPersistenceError>;
74 readonly listTargets: (input: {
75 readonly userId: string;
76 }) => Effect.Effect<ReadonlyArray<TargetRow>, LiveActivityTargetListPersistenceError>;
77 readonly markDelivery: (input: {
78 readonly userId: string;
79 readonly deviceId: string;
80 readonly kind: RelayDeliveryKind;
81 readonly aggregate: RelayAgentActivityAggregateState | null;
82 readonly deliveredAt: string;
83 }) => Effect.Effect<void, LiveActivityDeliveryMarkPersistenceError>;
84 readonly markStartQueued: (input: {
85 readonly userId: string;
86 readonly deviceId: string;
87 readonly queuedAt: string;
88 }) => Effect.Effect<void, LiveActivityDeliveryMarkPersistenceError>;
89 readonly clearStartQueued: (input: {
90 readonly userId: string;
91 readonly deviceId: string;
92 }) => Effect.Effect<void, LiveActivityDeliveryMarkPersistenceError>;
93 readonly invalidateDeliveryToken: (input: {
94 readonly userId: string;
95 readonly deviceId: string;
96 readonly kind: RelayDeliveryKind;
97 readonly invalidatedAt: string;
98 }) => Effect.Effect<void, LiveActivityDeliveryMarkPersistenceError>;
99 }
100>()("t3code-relay/agentActivity/LiveActivities") {}
102101
103102const decodeJsonString = Schema.decodeEffect(Schema.UnknownFromJsonString);
104103const encodeJsonValue = Schema.encodeEffect(Schema.UnknownFromJsonString);
......@@ -223,7 +222,7 @@ const make = Effect.gen(function* () {
223222 ? null
224223 : yield* encodeRelayAgentActivityAggregateStateJson(input.aggregate).pipe(
225224 Effect.flatMap(decodeJsonString),
226 Effect.map(cast<unknown, RelayAgentActivityAggregateState>),
225 Effect.map(Function.cast<unknown, RelayAgentActivityAggregateState>),
227226 );
228227
229228 yield* db
infra/relay/src/agentActivity/MobileRegistrations.test.ts+21-18
......@@ -38,7 +38,9 @@ const device: RelayDeviceRegistrationRequest = {
3838 },
3939};
4040
41function makeDevices(overrides: Partial<Devices.DevicesShape> = {}): Devices.DevicesShape {
41function makeDevices(
42 overrides: Partial<Devices.Devices["Service"]> = {},
43): Devices.Devices["Service"] {
4244 return {
4345 register: () => Effect.void,
4446 unregister: () => Effect.void,
......@@ -48,8 +50,8 @@ function makeDevices(overrides: Partial<Devices.DevicesShape> = {}): Devices.Dev
4850}
4951
5052function makeLiveActivities(
51 overrides: Partial<LiveActivities.LiveActivitiesShape> = {},
52): LiveActivities.LiveActivitiesShape {
53 overrides: Partial<LiveActivities.LiveActivities["Service"]> = {},
54): LiveActivities.LiveActivities["Service"] {
5355 return {
5456 register: () => Effect.void,
5557 listTargets: () => Effect.succeed([]),
......@@ -62,8 +64,8 @@ function makeLiveActivities(
6264}
6365
6466function makeAgentActivityRows(
65 overrides: Partial<AgentActivityRows.AgentActivityRowsShape> = {},
66): AgentActivityRows.AgentActivityRowsShape {
67 overrides: Partial<AgentActivityRows.AgentActivityRows["Service"]> = {},
68): AgentActivityRows.AgentActivityRows["Service"] {
6769 return {
6870 upsert: () => Effect.void,
6971 remove: () => Effect.void,
......@@ -108,8 +110,8 @@ function makeEnvironmentLinks(
108110}
109111
110112function makeDeliveryAttempts(
111 overrides: Partial<DeliveryAttempts.DeliveryAttemptsShape> = {},
112): DeliveryAttempts.DeliveryAttemptsShape {
113 overrides: Partial<DeliveryAttempts.DeliveryAttempts["Service"]> = {},
114): DeliveryAttempts.DeliveryAttempts["Service"] {
113115 return {
114116 record: () => Effect.void,
115117 claimSourceJob: () => Effect.succeed("claimed"),
......@@ -138,8 +140,8 @@ const config = RelayConfiguration.RelayConfiguration.of({
138140});
139141
140142function makeRegistrationReplayLayer(input: {
141 readonly devices: Devices.DevicesShape;
142 readonly liveActivities: LiveActivities.LiveActivitiesShape;
143 readonly devices: Devices.Devices["Service"];
144 readonly liveActivities: LiveActivities.LiveActivities["Service"];
143145 readonly queuedJobs: Array<SignedApnsDeliveryJob>;
144146}) {
145147 return MobileRegistrations.layer.pipe(
......@@ -167,8 +169,8 @@ function makeRegistrationReplayLayer(input: {
167169}
168170
169171function makeAgentActivityPublisher(
170 overrides: Partial<AgentActivityPublisher.AgentActivityPublisherShape> = {},
171): AgentActivityPublisher.AgentActivityPublisherShape {
172 overrides: Partial<AgentActivityPublisher.AgentActivityPublisher["Service"]> = {},
173): AgentActivityPublisher.AgentActivityPublisher["Service"] {
172174 return {
173175 publish: () => Effect.succeed({ ok: true, deliveries: [] }),
174176 replayForLiveActivityRegistration: () => Effect.succeed(null),
......@@ -178,10 +180,10 @@ function makeAgentActivityPublisher(
178180
179181describe("MobileRegistrations", () => {
180182 it.effect("registers devices through the device persistence service", () => {
181 let registered: Parameters<Devices.DevicesShape["register"]>[0] | null = null;
183 let registered: Parameters<Devices.Devices["Service"]["register"]>[0] | null = null;
182184 let replayed:
183185 | Parameters<
184 AgentActivityPublisher.AgentActivityPublisherShape["replayForLiveActivityRegistration"]
186 AgentActivityPublisher.AgentActivityPublisher["Service"]["replayForLiveActivityRegistration"]
185187 >[0]
186188 | null = null;
187189
......@@ -263,7 +265,7 @@ describe("MobileRegistrations", () => {
263265 });
264266
265267 it.effect("unregisters the current user's device", () => {
266 let unregistered: Parameters<Devices.DevicesShape["unregister"]>[0] | null = null;
268 let unregistered: Parameters<Devices.Devices["Service"]["unregister"]>[0] | null = null;
267269
268270 return Effect.gen(function* () {
269271 const result = yield* Effect.gen(function* () {
......@@ -310,10 +312,11 @@ describe("MobileRegistrations", () => {
310312 deviceId: "device-1" as const,
311313 activityPushToken: "activity-token" as const,
312314 };
313 let registered: Parameters<LiveActivities.LiveActivitiesShape["register"]>[0] | null = null;
315 let registered: Parameters<LiveActivities.LiveActivities["Service"]["register"]>[0] | null =
316 null;
314317 let replayed:
315318 | Parameters<
316 AgentActivityPublisher.AgentActivityPublisherShape["replayForLiveActivityRegistration"]
319 AgentActivityPublisher.AgentActivityPublisher["Service"]["replayForLiveActivityRegistration"]
317320 >[0]
318321 | null = null;
319322
......@@ -372,9 +375,9 @@ describe("MobileRegistrations", () => {
372375 () => {
373376 const queuedJobs: Array<SignedApnsDeliveryJob> = [];
374377 const queuedStarts: Array<
375 Parameters<LiveActivities.LiveActivitiesShape["markStartQueued"]>[0]
378 Parameters<LiveActivities.LiveActivities["Service"]["markStartQueued"]>[0]
376379 > = [];
377 const registeredDevices: Array<Parameters<Devices.DevicesShape["register"]>[0]> = [];
380 const registeredDevices: Array<Parameters<Devices.Devices["Service"]["register"]>[0]> = [];
378381 const devices = makeDevices({
379382 register: (input) =>
380383 Effect.sync(() => {
infra/relay/src/agentActivity/MobileRegistrations.ts+14-16
......@@ -15,24 +15,22 @@ export type MobileRegistrationError =
1515 | Devices.DeviceUnregistrationPersistenceError
1616 | LiveActivities.LiveActivityRegistrationPersistenceError;
1717
18export interface MobileRegistrationsShape {
19 readonly registerDevice: (input: {
20 readonly userId: string;
21 readonly payload: RelayDeviceRegistrationRequest;
22 }) => Effect.Effect<{ readonly ok: true }, MobileRegistrationError>;
23 readonly registerLiveActivity: (input: {
24 readonly userId: string;
25 readonly payload: RelayLiveActivityRegistrationRequest;
26 }) => Effect.Effect<{ readonly ok: true }, MobileRegistrationError>;
27 readonly unregisterDevice: (input: {
28 readonly userId: string;
29 readonly deviceId: string;
30 }) => Effect.Effect<{ readonly ok: true }, MobileRegistrationError>;
31}
32
3318export class MobileRegistrations extends Context.Service<
3419 MobileRegistrations,
35 MobileRegistrationsShape
20 {
21 readonly registerDevice: (input: {
22 readonly userId: string;
23 readonly payload: RelayDeviceRegistrationRequest;
24 }) => Effect.Effect<{ readonly ok: true }, MobileRegistrationError>;
25 readonly registerLiveActivity: (input: {
26 readonly userId: string;
27 readonly payload: RelayLiveActivityRegistrationRequest;
28 }) => Effect.Effect<{ readonly ok: true }, MobileRegistrationError>;
29 readonly unregisterDevice: (input: {
30 readonly userId: string;
31 readonly deviceId: string;
32 }) => Effect.Effect<{ readonly ok: true }, MobileRegistrationError>;
33 }
3634>()("t3code-relay/agentActivity/MobileRegistrations") {}
3735
3836const make = Effect.gen(function* () {
infra/relay/src/agentActivity/apnsDeliveryJobs.ts+48-12
......@@ -52,9 +52,45 @@ export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type;
5252export class ApnsDeliveryJobInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobInvalid>()(
5353 "ApnsDeliveryJobInvalid",
5454 {
55 message: Schema.String,
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 ]),
5667 },
57) {}
68) {
69 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 }
92 }
93}
5894
5995export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDeliveryJobExpired>()(
6096 "ApnsDeliveryJobExpired",
......@@ -106,31 +142,31 @@ function validatePayloadShape(payload: ApnsDeliveryJobPayload): ApnsDeliveryJobI
106142 case "live_activity_update":
107143 if (payload.aggregate === null) {
108144 return new ApnsDeliveryJobInvalid({
109 message: "Live Activity start/update jobs require an aggregate.",
145 reason: "missing-live-activity-aggregate",
110146 });
111147 }
112148 if (payload.notification !== null) {
113149 return new ApnsDeliveryJobInvalid({
114 message: "Live Activity jobs must not carry push notification payloads.",
150 reason: "unexpected-live-activity-notification",
115151 });
116152 }
117153 return null;
118154 case "live_activity_end":
119155 if (payload.notification !== null) {
120156 return new ApnsDeliveryJobInvalid({
121 message: "Live Activity jobs must not carry push notification payloads.",
157 reason: "unexpected-live-activity-notification",
122158 });
123159 }
124160 return null;
125161 case "push_notification":
126162 if (payload.notification === null) {
127163 return new ApnsDeliveryJobInvalid({
128 message: "Push notification jobs require a notification payload.",
164 reason: "missing-push-notification",
129165 });
130166 }
131167 if (payload.aggregate !== null) {
132168 return new ApnsDeliveryJobInvalid({
133 message: "Push notification jobs must not carry aggregate state.",
169 reason: "unexpected-push-notification-aggregate",
134170 });
135171 }
136172 return null;
......@@ -177,19 +213,19 @@ export function verifySignedApnsDeliveryJob(input: {
177213 }
178214 const createdAt = DateTime.make(input.job.payload.createdAt);
179215 if (Option.isNone(createdAt)) {
180 return new ApnsDeliveryJobInvalid({ message: "Invalid APNs delivery job creation time." });
216 return new ApnsDeliveryJobInvalid({ reason: "invalid-created-at" });
181217 }
182218 const expiresAt = DateTime.make(input.job.payload.expiresAt);
183219 if (Option.isNone(expiresAt)) {
184 return new ApnsDeliveryJobInvalid({ message: "Invalid APNs delivery job expiry." });
220 return new ApnsDeliveryJobInvalid({ reason: "invalid-expires-at" });
185221 }
186222 const createdAtMs = createdAt.value.epochMilliseconds;
187223 const expiresAtMs = expiresAt.value.epochMilliseconds;
188224 if (expiresAtMs <= createdAtMs) {
189 return new ApnsDeliveryJobInvalid({ message: "Invalid APNs delivery job time window." });
225 return new ApnsDeliveryJobInvalid({ reason: "invalid-time-window" });
190226 }
191227 if (expiresAtMs - createdAtMs > MAX_JOB_AGE_MS) {
192 return new ApnsDeliveryJobInvalid({ message: "APNs delivery job time window is too long." });
228 return new ApnsDeliveryJobInvalid({ reason: "time-window-too-long" });
193229 }
194230 if (expiresAtMs <= input.nowMs) {
195231 return new ApnsDeliveryJobExpired({ expiresAt: input.job.payload.expiresAt });
......@@ -199,7 +235,7 @@ export function verifySignedApnsDeliveryJob(input: {
199235 payload: input.job.payload,
200236 });
201237 if (!timingSafeEqualBase64Url(input.job.signature, expected)) {
202 return new ApnsDeliveryJobInvalid({ message: "Invalid APNs delivery job signature." });
238 return new ApnsDeliveryJobInvalid({ reason: "invalid-signature" });
203239 }
204240 return input.job.payload;
205241}