diff --git a/.changeset/session-projection-current-iframe-url.md b/.changeset/session-projection-current-iframe-url.md new file mode 100644 index 0000000000..6dec478cbe --- /dev/null +++ b/.changeset/session-projection-current-iframe-url.md @@ -0,0 +1,5 @@ +--- +"@prosopo/database": patch +--- + +Include `currentUrl` and `iframeUrl` in the `getSessionRecordBySessionId` projection so `buildEscalation` forwards them onto the escalated session. Without this, every post-PoW routed session was persisted with `currentUrl: undefined`, dropping URL attribution on the PoW → image/puzzle hop. diff --git a/packages/database/src/databases/provider.ts b/packages/database/src/databases/provider.ts index 496f100bf0..991486a24e 100644 --- a/packages/database/src/databases/provider.ts +++ b/packages/database/src/databases/provider.ts @@ -1661,6 +1661,8 @@ export class ProviderDatabase userSitekeyIpHash: 1, simdReadings: 1, dnsEvent: 1, + currentUrl: 1, + iframeUrl: 1, // captchaType is required by the peek-before-consume path // in `CaptchaManager.isValidRequest` — without it, every // escalation peek would compare `undefined !== ` diff --git a/packages/database/src/tests/integration/sessionRecordProjection.integration.test.ts b/packages/database/src/tests/integration/sessionRecordProjection.integration.test.ts index 303d907a38..46f7e50fe2 100644 --- a/packages/database/src/tests/integration/sessionRecordProjection.integration.test.ts +++ b/packages/database/src/tests/integration/sessionRecordProjection.integration.test.ts @@ -115,6 +115,8 @@ describe("getSessionRecordBySessionId projection", () => { decryptedHeadHash: "head-hash-xyz", siteKey: "site-key-1", reason: "user-passed", + currentUrl: "https://example.com/checkout", + iframeUrl: "https://widget.example.com/embed", headers: { "user-agent": "Mozilla/5.0", "accept-language": "en-GB", @@ -151,6 +153,13 @@ describe("getSessionRecordBySessionId projection", () => { expect(got.iFrame).toBe(false); expect(got.decryptedHeadHash).toBe("head-hash-xyz"); expect(got.reason).toBe("user-passed"); + // currentUrl / iframeUrl are forwarded onto the escalation session so + // downstream analytics (attack attribution, per-URL routing) survive + // the PoW → image/puzzle hop. Missed on the original projection + // (2026-07-09): 100% of escalation sessions were storing + // `currentUrl: undefined` in prod. + expect(got.currentUrl).toBe("https://example.com/checkout"); + expect(got.iframeUrl).toBe("https://widget.example.com/embed"); // Headers come back so `buildEscalation` can forward them onto the // escalation session, but the multi-KB `x-tls-clienthello` is