From 941e743f8e142986216f2eb000b2d3e79c688f3f Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 7 Oct 2026 16:53:59 -0700 Subject: [PATCH] fix: await router calls so unroutable trigger mail is not dropped --- src/deliver.test.ts | 37 ++++++++++++++++++++++++++++++++++++- src/deliver.ts | 31 ++++++++++++++++--------------- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/deliver.test.ts b/src/deliver.test.ts index abaf70c..d97ebe6 100644 --- a/src/deliver.test.ts +++ b/src/deliver.test.ts @@ -47,7 +47,7 @@ function recordingRouter() { return { grants, mail, - router: { + router: { routeMail: ( _address: string, rawMessage: string, @@ -203,6 +203,41 @@ describe("createRunTriggerDeliverer", () => { expect(recorded.grants).toHaveLength(1); }); + test("an async router answering false for grants is unroutable", async () => { + const sender = await durableSystemSender(); + const recorded = recordingRouter(); + recorded.router.sendRunGrants = async () => false; + const error = await deliverer(recorded.router, sender.sender) + .to(ADDRESS, "tick", "tnt_1", undefined) + .then( + () => { + throw new Error("the dead run delivered"); + }, + (e: unknown) => e, + ); + + expect(isRunTriggerUnroutable(error)).toBe(true); + expect((error as { code: string }).code).toBe(RUN_GRANTS_NOT_ROUTABLE); + expect(recorded.mail).toEqual([]); + }); + + test("an async router answering false for mail is unroutable", async () => { + const sender = await durableSystemSender(); + const recorded = recordingRouter(); + recorded.router.routeMail = async () => false; + const error = await deliverer(recorded.router, sender.sender) + .to(ADDRESS, "tick", "tnt_1", undefined) + .then( + () => { + throw new Error("the dead run delivered"); + }, + (e: unknown) => e, + ); + + expect(isRunTriggerUnroutable(error)).toBe(true); + expect((error as { code: string }).code).toBe(RUN_MAIL_NOT_ROUTABLE); + }); + test("rejected grants deliver nothing", async () => { const sender = await durableSystemSender(); const recorded = recordingRouter(); diff --git a/src/deliver.ts b/src/deliver.ts index 3c5a3f4..d811914 100644 --- a/src/deliver.ts +++ b/src/deliver.ts @@ -20,13 +20,13 @@ export type HookMailRouter = { rawMessage: string, authenticatedSender: string, messageId?: string, - ) => boolean; + ) => boolean | Promise; sendRunGrants: ( address: string, runId: string, stepGrants: RunGrantsFrame["stepGrants"], senderIdentities: RunGrantsFrame["senderIdentities"], - ) => boolean; + ) => boolean | Promise; }; export type RunTriggerMaterialize = ReturnType< @@ -130,11 +130,13 @@ export function createRunTriggerDeliverer( // Co-deliver the system sender's durable key on the grants barrier so // the recipient can verify the trigger mail against the key the hub // vouches for, exactly as a person-originated trigger does. - if ( - !opts.router.sendRunGrants(address, runId, grants.stepGrants, [ - { address: sender.address, publicKey: sender.publicKey }, - ]) - ) { + const grantsRouted = await opts.router.sendRunGrants( + address, + runId, + grants.stepGrants, + [{ address: sender.address, publicKey: sender.publicKey }], + ); + if (!grantsRouted) { throw new RunTriggerUnroutableError( RUN_GRANTS_NOT_ROUTABLE, address, @@ -150,14 +152,13 @@ export function createRunTriggerDeliverer( subject, sender, }); - if ( - !opts.router.routeMail( - address, - raw.base64, - sender.address, - raw.messageId, - ) - ) { + const mailRouted = await opts.router.routeMail( + address, + raw.base64, + sender.address, + raw.messageId, + ); + if (!mailRouted) { throw new RunTriggerUnroutableError( RUN_MAIL_NOT_ROUTABLE, address,