Patron support sync keeps Bifrost's answer by status alone
Soul pass 4, F2: the route stored Bifrost's whole response body in its audit event and returned it to the caller, as `detail` on a refusal and as `bifrostResponse` on success. Another service's body can quote the signed fact or its own configuration. Both audit events now carry bifrostStatus and no body; a refusal answers 502 with fixed text and Bifrost's status; success answers bifrostStatus in place of the body, which no caller reads (Bifrost's docs name only the route). The body is discarded unread. Tests drive the route against a Bifrost that answers 200 and 403 with the canary in its body, and pin the reply and the audit event exactly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b5b980578c
commit
2e06678301
2 changed files with 54 additions and 11 deletions
10
src/app.ts
10
src/app.ts
|
|
@ -1689,7 +1689,10 @@ export async function buildApp(options: BuildAppOptions = {}): Promise<FastifyIn
|
|||
},
|
||||
body,
|
||||
});
|
||||
const bifrostBody = await bifrostResponse.text();
|
||||
// Bifrost's answer is kept by its status alone, in the audit event and
|
||||
// in the reply: another service's body can quote the signed fact or
|
||||
// its own configuration, and both of those are stored and relayed.
|
||||
await bifrostResponse.body?.cancel();
|
||||
|
||||
await store.createAuditEvent({
|
||||
accountId: request.body.accountId,
|
||||
|
|
@ -1700,7 +1703,6 @@ export async function buildApp(options: BuildAppOptions = {}): Promise<FastifyIn
|
|||
requiredTierTitle: request.body.requiredTierTitle,
|
||||
providerEventId: supportFact.providerEventId,
|
||||
bifrostStatus: bifrostResponse.status,
|
||||
bifrostResponse: bifrostBody,
|
||||
},
|
||||
createdAt: now,
|
||||
});
|
||||
|
|
@ -1710,14 +1712,14 @@ export async function buildApp(options: BuildAppOptions = {}): Promise<FastifyIn
|
|||
return {
|
||||
error: "bifrost_patron_support_rejected",
|
||||
statusCode: bifrostResponse.status,
|
||||
detail: bifrostBody,
|
||||
detail: "Bifrost refused the patron support fact.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "synced",
|
||||
supportFact,
|
||||
bifrostResponse: bifrostBody,
|
||||
bifrostStatus: bifrostResponse.status,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@ describe("Heimdall service", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("syncs a linked Patreon membership into a signed Bifrost support fact", async () => {
|
||||
async function syncPatronSupport(bifrostAnswer: () => Response) {
|
||||
const baseConfig = createTestConfig();
|
||||
const config = {
|
||||
...baseConfig,
|
||||
|
|
@ -589,7 +589,7 @@ describe("Heimdall service", () => {
|
|||
bifrostBody = String(init?.body ?? "");
|
||||
const headers = init?.headers as Record<string, string>;
|
||||
bifrostSignature = headers["x-heimdall-signature-256"] ?? "";
|
||||
return new Response("processed", { status: 200 });
|
||||
return bifrostAnswer();
|
||||
}
|
||||
|
||||
return new Response("unexpected request", { status: 500 });
|
||||
|
|
@ -608,6 +608,15 @@ describe("Heimdall service", () => {
|
|||
},
|
||||
});
|
||||
|
||||
const auditEvents = [...(store as unknown as { auditEvents: Map<string, { eventType: string; eventPayloadJson: unknown }> }).auditEvents.values()];
|
||||
return { response, auditEvents, bifrostBody, bifrostSignature, now };
|
||||
}
|
||||
|
||||
it("syncs a linked Patreon membership into a signed Bifrost support fact", async () => {
|
||||
const { response, auditEvents, bifrostBody, bifrostSignature, now } = await syncPatronSupport(
|
||||
() => new Response("processed CANARYbifrost", { status: 200 })
|
||||
);
|
||||
|
||||
expect(response.statusCode, response.body).toBe(200);
|
||||
expect(bifrostSignature).toMatch(/^sha256=[0-9a-f]{64}$/);
|
||||
expect(JSON.parse(bifrostBody)).toEqual({
|
||||
|
|
@ -624,12 +633,44 @@ describe("Heimdall service", () => {
|
|||
providerSubscriptionId: "member-789",
|
||||
notes: "Verified active Patreon membership for tier Inner Sanctum.",
|
||||
});
|
||||
expect(response.json()).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "synced",
|
||||
bifrostResponse: "processed",
|
||||
})
|
||||
expect(response.json()).toEqual(expect.objectContaining({ status: "synced", bifrostStatus: 200 }));
|
||||
expect(auditEvents.map((event) => [event.eventType, event.eventPayloadJson])).toEqual([
|
||||
[
|
||||
"bifrost_patron_support_synced",
|
||||
{
|
||||
provider: "patreon",
|
||||
requiredTierTitle: "Inner Sanctum",
|
||||
providerEventId: "patreon-membership-snapshot:patreon-user-456:member-789:2026-06-09:1500",
|
||||
bifrostStatus: 200,
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(response.body + JSON.stringify(auditEvents)).not.toContain("CANARY");
|
||||
});
|
||||
|
||||
it("answers a Bifrost refusal with fixed text and its status, never its body", async () => {
|
||||
const { response, auditEvents } = await syncPatronSupport(
|
||||
() => new Response('{"error":"bad signature","echo":"CANARYrefusal"}', { status: 403 })
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(502);
|
||||
expect(response.json()).toEqual({
|
||||
error: "bifrost_patron_support_rejected",
|
||||
statusCode: 403,
|
||||
detail: "Bifrost refused the patron support fact.",
|
||||
});
|
||||
expect(auditEvents.map((event) => [event.eventType, event.eventPayloadJson])).toEqual([
|
||||
[
|
||||
"bifrost_patron_support_sync_failed",
|
||||
{
|
||||
provider: "patreon",
|
||||
requiredTierTitle: "Inner Sanctum",
|
||||
providerEventId: "patreon-membership-snapshot:patreon-user-456:member-789:2026-06-09:1500",
|
||||
bifrostStatus: 403,
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(response.body + JSON.stringify(auditEvents)).not.toContain("CANARY");
|
||||
});
|
||||
|
||||
it("redeems a completion code exactly once", async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue