From 0dfc46a8ae91674e7065ecd1b027099b06978d1c Mon Sep 17 00:00:00 2001 From: geek007 Date: Sun, 30 Aug 2026 17:21:55 +0530 Subject: [PATCH] fix(leases): project status lease row to lease columns so joined connection PII never escapes getExclusiveConnectionLeaseStatus built the returned lease via lease(row) where lease() was rowToCamel() cast to ExclusiveConnectionLease, so the runtime object also carried the joined connectionEmail, connectionDisplayName, connectionName, connectionAuthType, and connectionProvider columns from the status query, bypassing the configuredConnectionName() privacy fencing introduced in #11910. No consumer leaks today because the session-leases route whitelists five lifecycle fields, but any future consumer serializing status.lease directly would type-check fine and expose the connection owner identity. lease() is now an explicit projection of the twelve declared lease columns, so joined rows can never smuggle extra values onto the object, while the status-level fenced displayName/provider fields are unchanged. Validated by node --import tsx/esm --test tests/unit/exclusive-lease-status-projection.test.ts (failing before, passing after) plus the existing exclusive-connection-leases and session-leases-route suites. --- src/lib/db/exclusiveConnectionLeases.ts | 20 +++- .../exclusive-lease-status-projection.test.ts | 103 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/unit/exclusive-lease-status-projection.test.ts diff --git a/src/lib/db/exclusiveConnectionLeases.ts b/src/lib/db/exclusiveConnectionLeases.ts index 8b6920a09e6..e606a88e0d0 100644 --- a/src/lib/db/exclusiveConnectionLeases.ts +++ b/src/lib/db/exclusiveConnectionLeases.ts @@ -59,7 +59,25 @@ type LeaseUpdateResult = const database = () => getDbInstance(); const ACTIVE_SQL = "SELECT * FROM exclusive_connection_leases WHERE state = 'ACTIVE' AND "; -const lease = (row: LeaseRow) => rowToCamel(row) as ExclusiveConnectionLease; +// Projection, not a cast: joined SELECTs (the status query) carry connection_* +// identity columns (email/display name) that must never escape on the lease object. +const lease = (row: LeaseRow): ExclusiveConnectionLease => { + const camel = rowToCamel(row) as ExclusiveConnectionLease; + return { + id: camel.id, + leaseOwnerHash: camel.leaseOwnerHash, + apiKeyId: camel.apiKeyId, + provider: camel.provider, + connectionId: camel.connectionId, + generation: camel.generation, + state: camel.state, + acquiredAt: camel.acquiredAt, + renewedAt: camel.renewedAt, + expiresAt: camel.expiresAt, + endedAt: camel.endedAt, + endReason: camel.endReason, + }; +}; function timestamp(value?: string): string { const parsed = Date.parse(value ?? new Date().toISOString()); if (!Number.isFinite(parsed)) throw new Error("now must be a valid ISO timestamp"); diff --git a/tests/unit/exclusive-lease-status-projection.test.ts b/tests/unit/exclusive-lease-status-projection.test.ts new file mode 100644 index 00000000000..63acd8df116 --- /dev/null +++ b/tests/unit/exclusive-lease-status-projection.test.ts @@ -0,0 +1,103 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-lease-status-projection-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const leases = await import("../../src/lib/db/exclusiveConnectionLeases.ts"); + +const OWNER = "vlo_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; +const EMAIL = "lease-connection-owner@example.com"; +const DISPLAY_NAME = "Lease Connection Owner"; + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("status lease object never carries the joined connection identity columns", async () => { + const connection = (await providersDb.createProviderConnection({ + provider: "glm", + authType: "access_token", + accessToken: "lease-status-projection-token", + name: "Team GLM", + email: EMAIL, + displayName: DISPLAY_NAME, + isActive: true, + testStatus: "active", + })) as { id: string }; + + const acquired = leases.acquireExclusiveConnectionLease({ + leaseOwnerId: OWNER, + apiKeyId: "key-status-projection", + provider: "glm", + connectionId: connection.id, + now: "2026-08-30T12:00:00.000Z", + }); + assert.equal(acquired.kind, "ACQUIRED"); + if (acquired.kind !== "ACQUIRED") return; + + const status = leases.getExclusiveConnectionLeaseStatus({ + leaseOwnerId: OWNER, + generation: acquired.lease.generation, + apiKeyId: "key-status-projection", + now: "2026-08-30T12:00:01.000Z", + }); + assert.notEqual(status, null); + if (!status) return; + + // Status-level fenced fields stay exactly as before: the configured (safe) + // name plus the joined connection's provider. + assert.equal(status.provider, "glm"); + assert.equal(status.connectionName, "Team GLM"); + + // The lease object must be a projection of lease columns only — the joined + // connection_* identity values (owner email / display name) must never ride + // along on the runtime object, even though the current route consumer + // whitelists what it serializes. + for (const forbidden of [ + "connectionEmail", + "connectionDisplayName", + "connectionName", + "connectionAuthType", + "connectionProvider", + ]) { + assert.equal(forbidden in status.lease, false, `lease must not carry ${forbidden}`); + } + const serialized = JSON.stringify(status.lease); + assert.equal(serialized.includes(EMAIL), false); + assert.equal(serialized.includes(DISPLAY_NAME), false); + + // Legitimate lease lifecycle fields are intact. + assert.equal(status.lease.state, "ACTIVE"); + assert.equal(status.lease.generation, acquired.lease.generation); + assert.equal(status.lease.connectionId, connection.id); + assert.equal(status.lease.apiKeyId, "key-status-projection"); + assert.equal(status.lease.provider, "glm"); + assert.equal(status.lease.acquiredAt, "2026-08-30T12:00:00.000Z"); + assert.equal(status.lease.renewedAt, "2026-08-30T12:00:00.000Z"); + assert.equal(status.lease.expiresAt, "2026-08-30T12:02:00.000Z"); + assert.equal(status.lease.leaseOwnerHash, leases.hashLeaseOwnerId(OWNER)); + assert.equal(status.lease.endedAt, null); + assert.equal(status.lease.endReason, null); + assert.deepEqual(Object.keys(status.lease).sort(), [ + "acquiredAt", + "apiKeyId", + "connectionId", + "endReason", + "endedAt", + "expiresAt", + "generation", + "id", + "leaseOwnerHash", + "provider", + "renewedAt", + "state", + ]); +});