diff --git a/changelog.d/features/11910-owner-authenticated-lease-connection-name.md b/changelog.d/features/11910-owner-authenticated-lease-connection-name.md new file mode 100644 index 0000000000..4d95abdd98 --- /dev/null +++ b/changelog.d/features/11910-owner-authenticated-lease-connection-name.md @@ -0,0 +1 @@ +- **feat(leases):** add an explicit owner-authenticated status action that returns only the active lease's privacy-safe configured connection and provider labels, with generation fencing and no credential or internal-id disclosure ([#11910](https://github.com/diegosouzapw/OmniRoute/pull/11910)) — thanks @KaspaPulse diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 91bfdef1e9..9c7952520d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -111,13 +111,15 @@ paths: post: tags: - Session Leases - summary: Acquire, renew, or release an exclusive managed connection lease + summary: Acquire, inspect, renew, or release an exclusive managed connection lease description: | Requires an API key with `lease:exclusive` and an explicit non-empty `allowedConnections` policy. The opaque owner is bound to the authenticated API key; the lease owns an eligible connection, not a provider or model. Managed inference requests present the owner and exact generation headers. Temporary foreign occupancy - returns 429 `WAITING_FOR_CAPACITY` with `Retry-After`. + returns 429 `WAITING_FOR_CAPACITY` with `Retry-After`. Acquire, renew, and release retain + their connection-free response shapes. The explicit status action is owner-, key-, and + generation-fenced and returns only privacy-safe display metadata for an active binding. security: - BearerAuth: [] parameters: @@ -138,6 +140,11 @@ paths: properties: action: { type: string, const: acquire } model: { type: string, minLength: 1, maxLength: 512 } + - type: object + required: [action, generation] + properties: + action: { type: string, const: status } + generation: { type: integer, minimum: 1 } - type: object required: [action, generation] properties: @@ -153,7 +160,7 @@ paths: enum: [OWNER_EXIT, CLIENT_CANCELLED] responses: "200": - description: Lease lifecycle state without connection or credential disclosure + description: Lease lifecycle state, with privacy-safe connection display metadata only for status content: application/json: schema: @@ -8002,6 +8009,10 @@ components: schemas: ExclusiveConnectionLeaseLifecycle: type: object + description: >- + Shared lease lifecycle response. The optional connection object is present only for an + explicit, successful owner-authenticated status action; acquire, renew, and release never + include connection metadata. required: [state, generation, acquiredAt, renewedAt, expiresAt] properties: state: { type: string, enum: [ACTIVE, RELEASED] } @@ -8009,6 +8020,22 @@ components: acquiredAt: { type: string, format: date-time } renewedAt: { type: string, format: date-time } expiresAt: { type: string, format: date-time } + connection: + type: object + description: >- + Privacy-safe metadata for the exact active binding. Wrong-key, wrong-owner, + stale-generation, missing, expired, released, or invalidated leases return a generic + 409 without this object. No credential, internal id, owner identity, or email fallback + is serialized. + required: [displayName, provider] + properties: + displayName: + type: [string, "null"] + description: Trimmed operator-configured name, or null when no privacy-safe name exists. + provider: + type: string + minLength: 1 + description: Non-sensitive provider display label, never a generated compatible-provider id. ExclusiveConnectionLeaseCapacity: type: object required: [state, error, reason, retryAfter, eligibleCount, freeCount] diff --git a/docs/reference/API_REFERENCE.md b/docs/reference/API_REFERENCE.md index 7a924cf9b1..a146957d99 100644 --- a/docs/reference/API_REFERENCE.md +++ b/docs/reference/API_REFERENCE.md @@ -107,9 +107,9 @@ X-OmniRoute-Lease-Owner: vlo_<43-base64url-characters> {"action":"acquire","model":"glm/glm-4.6"} ``` -Successful lifecycle responses expose timestamps, `state`, and the exact positive `generation`, -but never the selected connection or credentials. Renew and release supply the generation in the -JSON body: +Successful acquire, renew, and release responses expose timestamps, `state`, and the exact positive +`generation`, but never the selected connection or credentials. Renew and release supply the +generation in the JSON body: ```json { "action": "renew", "generation": 1 } @@ -119,6 +119,44 @@ JSON body: { "action": "release", "generation": 1, "reason": "OWNER_EXIT" } ``` +An active lease owner can explicitly request privacy-safe display metadata for its current binding: + +```json +{ "action": "status", "generation": 1 } +``` + +```json +{ + "state": "ACTIVE", + "generation": 1, + "acquiredAt": "2026-08-28T12:00:00.000Z", + "renewedAt": "2026-08-28T12:00:30.000Z", + "expiresAt": "2026-08-28T12:02:30.000Z", + "connection": { + "displayName": "Primary Codex", + "provider": "codex" + } +} +``` + +This opt-in status action is fenced by the opaque owner, authenticated managed API key, and exact +active generation in one database transaction. `displayName` is only the trimmed configured +connection name; it is `null` when no safe configured name exists. OmniRoute never substitutes an +email or generated account identity. The provider value is a non-sensitive display label and never +a generated compatible-provider identifier. Credentials, tokens, cookies, raw connection or API +key ids, owner hashes, fencing secrets, and internal routing data are excluded. + +Wrong-key, wrong-owner, stale-generation, missing, expired, released, and invalidated lookups all +return the same `409 LEASE_FENCE_STALE` error without connection metadata. A client that received the capacity-wait response has no active binding to inspect. When routing transitions an active lease, +the same generation remains valid and status atomically returns the new binding, never the old one. +Existing clients remain unchanged because acquire, renew, release, and waiting responses retain +their previous shapes. + +This server contract does not change stock OpenAI Codex `/status`. Stock Codex currently reports its +model provider and built-in authentication/account state but does not render arbitrary custom +provider account metadata; a later client integration must call this action and decide how to +display `connection.displayName`. + Every managed inference request then supplies both control headers: ```http diff --git a/skills/omni-inference/SKILL.md b/skills/omni-inference/SKILL.md index 1f33e692bc..ba178839a3 100644 --- a/skills/omni-inference/SKILL.md +++ b/skills/omni-inference/SKILL.md @@ -16,13 +16,15 @@ All requests require a valid Bearer token or session cookie. Obtain a token via ### POST /api/v1/session-leases -Acquire, renew, or release an exclusive managed connection lease +Acquire, inspect, renew, or release an exclusive managed connection lease Requires an API key with `lease:exclusive` and an explicit non-empty `allowedConnections` policy. The opaque owner is bound to the authenticated API key; the lease owns an eligible connection, not a provider or model. Managed inference requests present the owner and exact generation headers. Temporary foreign occupancy -returns 429 `WAITING_FOR_CAPACITY` with `Retry-After`. +returns 429 `WAITING_FOR_CAPACITY` with `Retry-After`. Acquire, renew, and release retain +their connection-free response shapes. The explicit status action is owner-, key-, and +generation-fenced and returns only privacy-safe display metadata for an active binding. ```bash diff --git a/src/app/api/v1/session-leases/route.ts b/src/app/api/v1/session-leases/route.ts index 00cfb863e4..8c58349b2f 100644 --- a/src/app/api/v1/session-leases/route.ts +++ b/src/app/api/v1/session-leases/route.ts @@ -5,9 +5,11 @@ import { isCommonChatGptWebRetirementError } from "@/shared/constants/chatgptWeb import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors"; import { + getExclusiveConnectionLeaseStatus, releaseExclusiveConnectionLease, renewExclusiveConnectionLease, } from "@/lib/db/exclusiveConnectionLeases"; +import { getProviderDisplayName } from "@/lib/display/names"; import { extractApiKey, getProviderCredentialsWithQuotaPreflight, @@ -29,6 +31,7 @@ const action = (name: T, shape: z.ZodRawShape) => const generation = z.number().int().positive().safe(); const actionSchema = z.discriminatedUnion("action", [ action("acquire", { model: z.string().trim().min(1).max(512) }), + action("status", { generation }), action("renew", { generation }), z.object({ action: z.literal("release"), @@ -95,6 +98,18 @@ export async function POST(request: Request): Promise { generation: parsed.data.generation, apiKeyId: policy.apiKeyInfo.id, }; + if (parsed.data.action === "status") { + const status = getExclusiveConnectionLeaseStatus(input); + return status + ? json(200, { + ...lifecycle(status.lease), + connection: { + displayName: status.connectionName, + provider: getProviderDisplayName(status.provider), + }, + }) + : error(409, "LEASE_FENCE_STALE", "The lease generation is stale"); + } const result = parsed.data.action === "renew" ? renewExclusiveConnectionLease(input) diff --git a/src/lib/db/exclusiveConnectionLeases.ts b/src/lib/db/exclusiveConnectionLeases.ts index f93453d58f..8b6920a09e 100644 --- a/src/lib/db/exclusiveConnectionLeases.ts +++ b/src/lib/db/exclusiveConnectionLeases.ts @@ -40,6 +40,13 @@ type LeaseRow = { state: ExclusiveLeaseState; expires_at: string; }; +type LeaseStatusRow = LeaseRow & { + connection_provider: string; + connection_auth_type: string | null; + connection_name: string | null; + connection_email: string | null; + connection_display_name: string | null; +}; type LeaseSuccess = { kind: "ACQUIRED" | "REUSED" | "TRANSITIONED"; lease: ExclusiveConnectionLease; @@ -92,6 +99,21 @@ function active(column: "lease_owner_hash" | "connection_id", value: string) { return database().prepare(`${ACTIVE_SQL}${column} = ?`).get(value) as LeaseRow | undefined; } +function configuredConnectionName(row: LeaseStatusRow): string | null { + const name = row.connection_name?.trim(); + if (!name || name.includes("@")) return null; + + if (row.connection_auth_type === "oauth" || row.connection_auth_type === "access_token") { + const normalized = name.toLowerCase(); + const generatedFallbacks = [row.connection_email, row.connection_display_name] + .map((value) => value?.trim().toLowerCase()) + .filter((value): value is string => Boolean(value)); + if (generatedFallbacks.includes(normalized)) return null; + } + + return name; +} + function historical(ownerHash: string, generation: number) { return database() .prepare( @@ -192,6 +214,44 @@ export function reconcileExpiredExclusiveConnectionLeases(now?: string): number return immediate(() => expire(timestamp(now))); } +export function getExclusiveConnectionLeaseStatus(input: { + leaseOwnerId: string; + generation: number; + apiKeyId: string; + now?: string; +}): { + lease: ExclusiveConnectionLease; + provider: string; + connectionName: string | null; +} | null { + const ownerHash = hashLeaseOwnerId(input.leaseOwnerId); + const now = timestamp(input.now); + return immediate(() => { + expire(now); + const row = database() + .prepare( + `SELECT leases.*, connections.provider AS connection_provider, + connections.auth_type AS connection_auth_type, + connections.name AS connection_name, + connections.email AS connection_email, + connections.display_name AS connection_display_name + FROM exclusive_connection_leases leases + INNER JOIN provider_connections connections ON connections.id = leases.connection_id + WHERE leases.lease_owner_hash = ? AND leases.api_key_id = ? + AND leases.generation = ? AND leases.state = 'ACTIVE' AND leases.expires_at > ? + LIMIT 1` + ) + .get(ownerHash, input.apiKeyId, input.generation, now) as LeaseStatusRow | undefined; + return row + ? { + lease: lease(row), + provider: row.connection_provider, + connectionName: configuredConnectionName(row), + } + : null; + }); +} + export function acquireExclusiveConnectionLease(input: { leaseOwnerId: string; apiKeyId: string; diff --git a/tests/unit/session-leases-route.test.ts b/tests/unit/session-leases-route.test.ts index c3e5e77f5c..2d00fcffcd 100644 --- a/tests/unit/session-leases-route.test.ts +++ b/tests/unit/session-leases-route.test.ts @@ -15,6 +15,7 @@ const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); const modelAliasesDb = await import("../../src/lib/db/models/aliases.ts"); const settingsDb = await import("../../src/lib/db/settings.ts"); const modelAliasResolver = await import("../../src/lib/modelAliasResolver.ts"); +const leaseDb = await import("../../src/lib/db/exclusiveConnectionLeases.ts"); const route = await import("../../src/app/api/v1/session-leases/route.ts"); const OWNER_A = "vlo_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; @@ -229,6 +230,7 @@ test("acquires, reuses, renews, releases, and fences a stale lifecycle", async ( request(managed.key, { action: "renew", generation: 1 }, OWNER_A) ); assert.equal(renewed.status, 200); + assert.equal("connection" in (await json(renewed)), false); const staleRenew = await route.POST( request(managed.key, { action: "renew", generation: 2 }, OWNER_A) @@ -240,7 +242,9 @@ test("acquires, reuses, renews, releases, and fences a stale lifecycle", async ( request(managed.key, { action: "release", generation: 1, reason: "CLIENT_CANCELLED" }, OWNER_A) ); assert.equal(released.status, 200); - assert.equal((await json(released)).state, "RELEASED"); + const releasedBody = await json(released); + assert.equal(releasedBody.state, "RELEASED"); + assert.equal("connection" in releasedBody, false); const idempotent = await route.POST( request(managed.key, { action: "release", generation: 1 }, OWNER_A) @@ -250,6 +254,257 @@ test("acquires, reuses, renews, releases, and fences a stale lifecycle", async ( assert.equal(attemptedExternalCalls, 0); }); +test("status explicitly returns only the active owner's privacy-safe connection display metadata", async () => { + const connection = await seedConnection(1); + const managed = await seedKey([connection.id]); + const acquired = await route.POST( + request(managed.key, { action: "acquire", model: "glm/glm-4.6" }, OWNER_A) + ); + assert.equal(acquired.status, 200); + + const response = await route.POST( + request(managed.key, { action: "status", generation: 1 }, OWNER_A) + ); + assert.equal(response.status, 200); + const body = await json(response); + assert.deepEqual(Object.keys(body).sort(), [ + "acquiredAt", + "connection", + "expiresAt", + "generation", + "renewedAt", + "state", + ]); + assert.deepEqual(body.connection, { + displayName: "lease-route-1", + provider: "glm", + }); + + const serialized = JSON.stringify(body); + for (const forbidden of [ + connection.id, + managed.id, + managed.key, + OWNER_A, + leaseDb.hashLeaseOwnerId(OWNER_A), + "sk-route-1", + "connectionId", + "apiKeyId", + "leaseOwnerHash", + "leaseOwnerId", + "credentials", + "accessToken", + "refreshToken", + "cookie", + "fencing", + ]) { + assert.equal(serialized.includes(forbidden), false, `status must not contain ${forbidden}`); + } + assert.equal(attemptedExternalCalls, 0); +}); + +test("status fails closed for a foreign key, different owner, or stale generation", async () => { + const connection = await seedConnection(1); + const ownerKey = await seedKey([connection.id]); + const foreignKey = await seedKey([connection.id]); + assert.equal( + (await route.POST(request(ownerKey.key, { action: "acquire", model: "glm/glm-4.6" }, OWNER_A))) + .status, + 200 + ); + + const attempts = [ + request(foreignKey.key, { action: "status", generation: 1 }, OWNER_A), + request(ownerKey.key, { action: "status", generation: 1 }, OWNER_B), + request(ownerKey.key, { action: "status", generation: 2 }, OWNER_A), + ]; + for (const attempt of attempts) { + const response = await route.POST(attempt); + assert.equal(response.status, 409); + const body = await json(response); + assert.equal((body.error as { code: string }).code, "LEASE_FENCE_STALE"); + const serialized = JSON.stringify(body); + assert.equal("connection" in body, false); + assert.equal(serialized.includes("lease-route-1"), false); + assert.equal(serialized.includes(connection.id), false); + } + assert.equal(attemptedExternalCalls, 0); +}); + +test("status never uses provider identity as a connection display-name fallback", async () => { + for (const identity of [ + { email: "private-lease-owner@example.com" }, + { displayName: "Private Provider Account" }, + ]) { + await resetStorage(); + const privateIdentity = identity.email ?? identity.displayName; + const providerToken = `private-provider-access-token-${privateIdentity}`; + const connection = await providersDb.createProviderConnection({ + provider: "glm", + authType: "access_token", + accessToken: providerToken, + ...identity, + isActive: true, + testStatus: "active", + }); + assert.equal(connection.name, privateIdentity, "the stored name is a generated fallback"); + const managed = await seedKey([connection.id]); + assert.equal( + (await route.POST(request(managed.key, { action: "acquire", model: "glm/glm-4.6" }, OWNER_A))) + .status, + 200 + ); + + const response = await route.POST( + request(managed.key, { action: "status", generation: 1 }, OWNER_A) + ); + assert.equal(response.status, 200); + const body = await json(response); + assert.deepEqual(body.connection, { displayName: null, provider: "glm" }); + const serialized = JSON.stringify(body); + assert.equal(serialized.includes(privateIdentity), false); + assert.equal(serialized.includes(providerToken), false); + assert.equal(serialized.includes(connection.id), false); + } + assert.equal(attemptedExternalCalls, 0); +}); + +test("status replaces a generated compatible-provider id with a non-sensitive label", async () => { + const providerId = "openai-compatible-chat-01234567-89ab-cdef-0123-456789abcdef"; + await providersDb.createProviderNode({ + id: providerId, + type: "openai-compatible", + name: "Internal routing node", + prefix: "internal-routing-node", + apiType: "chat", + baseUrl: "https://private-provider.invalid/v1", + }); + const connection = await providersDb.createProviderConnection({ + provider: providerId, + authType: "apikey", + name: "Safe gateway label", + apiKey: "private-compatible-provider-key", + isActive: true, + testStatus: "active", + providerSpecificData: {}, + }); + const managed = await seedKey([connection.id]); + const acquired = leaseDb.acquireExclusiveConnectionLease({ + leaseOwnerId: OWNER_A, + apiKeyId: managed.id, + provider: providerId, + connectionId: connection.id, + }); + assert.equal(acquired.kind, "ACQUIRED"); + + const response = await route.POST( + request(managed.key, { action: "status", generation: 1 }, OWNER_A) + ); + assert.equal(response.status, 200); + const body = await json(response); + assert.deepEqual(body.connection, { + displayName: "Safe gateway label", + provider: "Compatible (openai)", + }); + const serialized = JSON.stringify(body); + assert.equal(serialized.includes(providerId), false); + assert.equal(serialized.includes("internal-routing-node"), false); + assert.equal(serialized.includes("private-provider.invalid"), false); + assert.equal(serialized.includes("private-compatible-provider-key"), false); + assert.equal(attemptedExternalCalls, 0); +}); + +test("status reads the new binding after transition and never returns the old label", async () => { + const oldConnection = await seedConnection(1); + const newConnection = await seedConnection(2); + const managed = await seedKey([oldConnection.id, newConnection.id]); + assert.equal( + (await route.POST(request(managed.key, { action: "acquire", model: "glm/glm-4.6" }, OWNER_A))) + .status, + 200 + ); + + const transitioned = leaseDb.transitionExclusiveConnectionLease({ + leaseOwnerId: OWNER_A, + generation: 1, + apiKeyId: managed.id, + provider: "glm", + connectionId: newConnection.id, + reason: "CONNECTION_INELIGIBLE", + }); + assert.equal(transitioned.kind, "TRANSITIONED"); + if (transitioned.kind !== "TRANSITIONED") return; + + const currentResponse = await route.POST( + request(managed.key, { action: "status", generation: transitioned.lease.generation }, OWNER_A) + ); + assert.equal(currentResponse.status, 200); + const currentBody = await json(currentResponse); + assert.deepEqual(currentBody.connection, { + displayName: "lease-route-2", + provider: "glm", + }); + const serialized = JSON.stringify(currentBody); + assert.equal(serialized.includes("lease-route-1"), false); + assert.equal(serialized.includes(oldConnection.id), false); + assert.equal(serialized.includes(newConnection.id), false); + assert.equal(attemptedExternalCalls, 0); +}); + +test("status exposes no connection metadata for released, expired, invalidated, or missing leases", async () => { + for (const inactiveState of ["released", "expired", "invalidated"] as const) { + await resetStorage(); + const connection = await seedConnection(1); + const managed = await seedKey([connection.id]); + assert.equal( + (await route.POST(request(managed.key, { action: "acquire", model: "glm/glm-4.6" }, OWNER_A))) + .status, + 200 + ); + + if (inactiveState === "released") { + assert.equal( + (await route.POST(request(managed.key, { action: "release", generation: 1 }, OWNER_A))) + .status, + 200 + ); + } else if (inactiveState === "expired") { + leaseDb.reconcileExpiredExclusiveConnectionLeases( + new Date(Date.now() + 180_000).toISOString() + ); + } else { + assert.equal( + leaseDb.invalidateExclusiveConnectionLease({ + leaseOwnerId: OWNER_A, + generation: 1, + apiKeyId: managed.id, + reason: "AUTHORIZATION_CHANGED", + }).kind, + "INVALIDATED" + ); + } + + const response = await route.POST( + request(managed.key, { action: "status", generation: 1 }, OWNER_A) + ); + assert.equal(response.status, 409, inactiveState); + const body = await json(response); + assert.equal("connection" in body, false, inactiveState); + assert.equal(JSON.stringify(body).includes("lease-route-1"), false, inactiveState); + assert.equal(JSON.stringify(body).includes(connection.id), false, inactiveState); + } + + await resetStorage(); + const connection = await seedConnection(1); + const managed = await seedKey([connection.id]); + const missing = await route.POST( + request(managed.key, { action: "status", generation: 1 }, OWNER_A) + ); + assert.equal(missing.status, 409); + assert.equal("connection" in (await json(missing)), false); + assert.equal(attemptedExternalCalls, 0); +}); + test("renew and release require the API key that owns the active authorization", async () => { const connection = await seedConnection(1); const ownerKey = await seedKey([connection.id]); @@ -330,5 +585,6 @@ test("returns bounded WAITING_FOR_CAPACITY without credential or owner disclosur assert.equal(serialized.includes(OWNER_B), false); assert.equal(serialized.includes("apiKey"), false); assert.equal(serialized.includes("at /"), false); + assert.equal("connection" in body, false); assert.equal(attemptedExternalCalls, 0); });