Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2c56c36
feat(credential-proxy): add bounded bearer forwarding
Nov 11, 2026
41d09a3
feat(credential-proxy): stream authorized tunnel requests
Nov 11, 2026
f00b2f0
feat(credential-proxy): connect authenticated VM tunnel
Nov 11, 2026
08fddf2
Bind enrollment acceptance to proxy boot epoch
Nov 11, 2026
adfefc5
Bind tunnel enrollment to authenticated VM peer
Nov 11, 2026
5b44328
Add authenticated proxy boot registration
Nov 11, 2026
1ed566e
Add secure proxy bootstrap provisioning boundary
Nov 11, 2026
edf98ea
Provision credential proxy with isolated bootstrap
Nov 11, 2026
e631553
Retry proxy tunnel enrollment before forwarding
Nov 11, 2026
545d9e7
Add authenticated proxy enrollment control API
Nov 11, 2026
f1ff6ad
Wire proxy-side run enrollment into tunnel auth
Nov 11, 2026
b4462a2
Wire proxy control enrollment into tunnel startup
Nov 11, 2026
a96a584
Merge remote-tracking branch 'origin/main' into GEN-2824-proxy-forwar…
Nov 11, 2026
ef4b952
Retain proxy control credential after boot registration
Nov 11, 2026
59e6bea
Authenticate tunnel enrollment with run credential
Nov 11, 2026
2188c7d
Keep run credential out of consume request
Nov 11, 2026
78b86ee
Cover invalid run enrollment bindings
Nov 11, 2026
c46c3a2
Narrow active run identity before enrollment
Nov 11, 2026
fd74010
Close proxy tunnels when a run is revoked
Nov 11, 2026
68d348e
Add decrypted run integration grant resolver
Nov 11, 2026
eb90329
Expose selected integrations to authenticated proxy
Nov 11, 2026
3e79721
Merge remote-tracking branch 'origin/main' into GEN-2824-proxy-forwar…
Nov 11, 2026
6f13d7b
Fix encrypted snapshot proxy test fixture
Nov 11, 2026
fea379c
Wire proxy snapshots into local grant resolution
Nov 11, 2026
b765691
Preserve grant resolver selection boundaries
Nov 11, 2026
15335fa
Resolve empty and locked grants safely
Nov 11, 2026
32414a2
Match snapshot control request contract
Nov 11, 2026
c22a412
Bind tunnel VM identity to certificate SAN
Nov 11, 2026
3ed3f07
GEN-2824 provision mTLS identities and revoke tunnels
Nov 11, 2026
a3aad85
GEN-2824 include selected model account snapshot
Nov 11, 2026
463ce53
GEN-2824 update snapshot fixture for model selection
Nov 12, 2026
44c5bde
GEN-2824 add isolated Codex auth helper
Nov 12, 2026
69f05bc
GEN-2824 persist encrypted Codex account refreshes
Nov 12, 2026
454232c
GEN-2824 encrypt Codex account caches in proxy
Nov 12, 2026
28f8f1c
GEN-2824 restore encrypted Codex caches on unlock
Nov 12, 2026
d27d95a
GEN-2824 persist encrypted model caches by revision
Nov 12, 2026
df221e5
GEN-2824 compose model cache lifecycle into proxy startup
Nov 12, 2026
a20d434
GEN-2824 add private browser model authorization control
Nov 12, 2026
c876176
GEN-2824 issue paired model account browser capabilities
Nov 12, 2026
dd266cc
GEN-2824 connect model account browser authorization
Nov 12, 2026
2ccb27b
GEN-2824 cover capability endpoint in browser test
Nov 12, 2026
87341ad
GEN-2824 add model account create and delete
Nov 12, 2026
4e4a5c0
GEN-2824 recover stale model account cache writes
Nov 12, 2026
e5a6a97
GEN-2824 clear deleted model caches on unlock
Nov 12, 2026
29cd548
GEN-2824 cover model cache key rotation race
Nov 12, 2026
7a4fdfa
GEN-2824 cover model reset refresh race
Nov 12, 2026
5a5f01a
GEN-2824 cover reset during refresh retry
Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions packages/api-client/src/schema.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,85 @@
// Generated from packages/docs/openapi.yaml. Do not edit.
export interface paths {
"/api/v1/model-accounts": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* List model accounts available to the owner
* @description Requires an interactive owner browser session. Returns account metadata and the collection revision only; encrypted account caches and credentials are never returned.
*
* Requires the integrations:read scope.
*
* An interactive owner session is required; bearer tokens cannot call this operation.
*/
get: operations["get_api_v1_model_accounts"];
put?: never;
/**
* Create a model account
* @description Requires an interactive owner browser session and the current collection revision. Accepts only user-encrypted initial account state; credentials are established directly with the credential proxy.
*
* Requires the integrations:write scope.
*
* An interactive owner session is required; bearer tokens cannot call this operation.
*/
post: operations["post_api_v1_model_accounts"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/model-accounts/{accountId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/**
* Delete a model account
* @description Requires an interactive owner browser session and the current collection revision. Deletion is recorded as a tombstone so future snapshots and unlock reconciliation cannot restore the account.
*
* Requires the integrations:write scope.
*
* An interactive owner session is required; bearer tokens cannot call this operation.
*/
delete: operations["delete_api_v1_model_accounts_accountId"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/model-accounts/{accountId}/browser-capability": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Issue a short-lived browser capability for a paired model account
* @description Requires an interactive owner browser session. Returns a single-use, 60-second capability scoped to the account, action, proxy boot and exact application origin. The browser sends it directly to the paired proxy; Factorize never receives model-account tokens.
*
* Requires the integrations:write scope.
*
* An interactive owner session is required; bearer tokens cannot call this operation.
*/
post: operations["post_api_v1_model_accounts_accountId_browser_capability"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/unlock-key/verification": {
parameters: {
query?: never;
Expand Down Expand Up @@ -2683,6 +2763,176 @@ export interface components {
}
export type $defs = Record<string, never>;
export interface operations {
get_api_v1_model_accounts: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Model account metadata and collection revision */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
revision: number;
accounts: {
/** Format: uuid */
id: string;
name: string;
description: string | null;
revision: number;
}[];
};
};
};
400: components["responses"]["Error400"];
401: components["responses"]["Error401"];
403: components["responses"]["Error403"];
404: components["responses"]["Error404"];
409: components["responses"]["Error409"];
429: components["responses"]["Error429"];
500: components["responses"]["Error500"];
503: components["responses"]["Error503"];
};
};
post_api_v1_model_accounts: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** Format: uuid */
id: string;
expectedRevision: number;
/** Format: uuid */
idempotencyKey: string;
name: string;
description: string | null;
encryptedPayload: string;
};
};
};
responses: {
/** @description Created model account */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
revision: number;
duplicate: boolean;
};
};
};
400: components["responses"]["Error400"];
401: components["responses"]["Error401"];
403: components["responses"]["Error403"];
404: components["responses"]["Error404"];
409: components["responses"]["Error409"];
415: components["responses"]["Error415"];
429: components["responses"]["Error429"];
500: components["responses"]["Error500"];
503: components["responses"]["Error503"];
};
};
delete_api_v1_model_accounts_accountId: {
parameters: {
query?: never;
header?: never;
path: {
accountId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
expectedRevision: number;
/** Format: uuid */
idempotencyKey: string;
};
};
};
responses: {
/** @description Deleted model account */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** Format: uuid */
id: string;
/** @constant */
deleted: true;
revision: number;
duplicate: boolean;
};
};
};
400: components["responses"]["Error400"];
401: components["responses"]["Error401"];
403: components["responses"]["Error403"];
404: components["responses"]["Error404"];
409: components["responses"]["Error409"];
415: components["responses"]["Error415"];
429: components["responses"]["Error429"];
500: components["responses"]["Error500"];
503: components["responses"]["Error503"];
};
};
post_api_v1_model_accounts_accountId_browser_capability: {
parameters: {
query?: never;
header?: never;
path: {
accountId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @enum {string} */
action: "authorize" | "status" | "cancel";
};
};
};
responses: {
/** @description Paired proxy endpoint and one-use capability */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** Format: uri */
endpoint: string;
capability: string;
};
};
};
400: components["responses"]["Error400"];
401: components["responses"]["Error401"];
403: components["responses"]["Error403"];
404: components["responses"]["Error404"];
409: components["responses"]["Error409"];
415: components["responses"]["Error415"];
429: components["responses"]["Error429"];
500: components["responses"]["Error500"];
503: components["responses"]["Error503"];
};
};
get_api_v1_unlock_key_verification: {
parameters: {
query?: never;
Expand Down
10 changes: 10 additions & 0 deletions packages/api/migrations/0036_proxy_control_credentials.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE app.credential_proxy_control_credentials (
tenant_id uuid NOT NULL REFERENCES app.tenants(id) ON DELETE CASCADE,
proxy_identity text NOT NULL,
boot_epoch uuid NOT NULL,
token_digest bytea NOT NULL UNIQUE,
revoked_at timestamptz,
consumed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, proxy_identity)
);
2 changes: 2 additions & 0 deletions packages/api/migrations/0037_proxy_control_credentials.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE app.credential_proxy_control_credentials
ADD COLUMN proxy_token_digest bytea UNIQUE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE app.credential_proxy_browser_pairings (
tenant_id uuid PRIMARY KEY REFERENCES app.tenants(id) ON DELETE CASCADE,
proxy_identity text NOT NULL,
boot_epoch uuid NOT NULL,
browser_origin text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (browser_origin ~ '^https://[^/]+$')
);
29 changes: 29 additions & 0 deletions packages/api/src/api-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,19 @@ const bearerIntegrationCreate = z.object({
const bearerIntegrationUpdate = bearerIntegrationCreate.omit({ id: true });
const bearerIntegrationDelete = z.object({ expectedRevision: z.number().int().nonnegative(), idempotencyKey: z.uuid() }).strict();
const bearerIntegrationList = z.object({ revision: z.number().int().nonnegative(), integrations: z.array(bearerIntegrationMetadata) });
const modelAccountMetadata = z.object({ id: z.uuid(), name: z.string(), description: z.string().nullable(), revision: z.number().int().nonnegative() });
const modelAccountCreate = z.object({
id: z.uuid(), expectedRevision: z.number().int().nonnegative(), idempotencyKey: z.uuid(),
name: z.string().trim().min(1).max(120), description: z.string().max(500).nullable(),
encryptedPayload: z.string().min(1).max(65_536),
}).strict();
const modelAccountDelete = z.object({ expectedRevision: z.number().int().nonnegative(), idempotencyKey: z.uuid() }).strict();
const modelAccountList = z.object({ revision: z.number().int().nonnegative(), accounts: z.array(modelAccountMetadata) });
const bearerIntegrationWriteResult = z.object({ revision: z.number().int().nonnegative(), duplicate: z.boolean() });
const bearerIntegrationDeleteResult = z.object({ id: z.uuid(), deleted: z.literal(true), revision: z.number().int().nonnegative(), duplicate: z.boolean() });
const unlockKeyVerification = z.object({ ownerId: z.string(), verification: z.object({ generationId: z.uuid(), encryptedVerification: z.string() }).strict().nullable() }).strict();
const unlockKeyVerificationInput = z.object({ generationId: z.uuid(), encryptedVerification: z.string().min(1).max(4096) }).strict();
const modelAccountBrowserCapabilityInput = z.object({ action: z.enum(["authorize", "status", "cancel"]) }).strict();

function route(
method: string,
Expand Down Expand Up @@ -213,6 +222,26 @@ function authRoute(method: string, path: string, action: AuthAction, summary: st
}

export const API_ROUTES: RouteContract[] = [
route("GET", "/api/v1/model-accounts", "integrations:read", {
summary: "List model accounts available to the owner",
description: "Requires an interactive owner browser session. Returns account metadata and the collection revision only; encrypted account caches and credentials are never returned.",
responses: { "200": { description: "Model account metadata and collection revision", content: { "application/json": { schema: jsonSchema(modelAccountList, "output") } } } },
}, service => service.listModelAccounts(), { ownerSession: true }),
route("POST", "/api/v1/model-accounts", "integrations:write", {
summary: "Create a model account",
description: "Requires an interactive owner browser session and the current collection revision. Accepts only user-encrypted initial account state; credentials are established directly with the credential proxy.",
responses: { "201": { description: "Created model account", content: { "application/json": { schema: jsonSchema(bearerIntegrationWriteResult, "output") } } } },
}, (service, { body }) => service.createModelAccount(body), { body: modelAccountCreate, maxBodyBytes: 70_000, ownerSession: true }),
route("DELETE", "/api/v1/model-accounts/{accountId}", "integrations:write", {
summary: "Delete a model account",
description: "Requires an interactive owner browser session and the current collection revision. Deletion is recorded as a tombstone so future snapshots and unlock reconciliation cannot restore the account.",
responses: { "200": { description: "Deleted model account", content: { "application/json": { schema: jsonSchema(bearerIntegrationDeleteResult, "output") } } } },
}, (service, { params, body }) => service.deleteModelAccount(params.accountId, body), { body: modelAccountDelete, maxBodyBytes: 4096, ownerSession: true, parameters: z.object({ accountId: z.uuid() }) }),
route("POST", "/api/v1/model-accounts/{accountId}/browser-capability", "integrations:write", {
summary: "Issue a short-lived browser capability for a paired model account",
description: "Requires an interactive owner browser session. Returns a single-use, 60-second capability scoped to the account, action, proxy boot and exact application origin. The browser sends it directly to the paired proxy; Factorize never receives model-account tokens.",
responses: { "200": { description: "Paired proxy endpoint and one-use capability", content: { "application/json": { schema: jsonSchema(z.object({ endpoint: z.url(), capability: z.string() }), "output") } } } },
}, (service, { params, body }) => service.issueModelAccountBrowserCapability(params.accountId, body.action), { body: modelAccountBrowserCapabilityInput, maxBodyBytes: 1024, ownerSession: true, parameters: z.object({ accountId: z.uuid() }) }),
route("GET", "/api/v1/unlock-key/verification", "integrations:read", { summary: "Get the current encrypted unlock-key verification record", description: "Requires an interactive owner browser session. Returns user-encrypted verification ciphertext and its key generation; the unlock key is never accepted or returned.", responses: { "200": { description: "Encrypted verification record or empty setup state", content: { "application/json": { schema: jsonSchema(unlockKeyVerification, "output") } } } } }, service => service.getUnlockKeyVerification(), { ownerSession: true }),
route("POST", "/api/v1/unlock-key/verification", "integrations:write", { summary: "Create the encrypted unlock-key verification record", description: "Requires an interactive owner browser session. Stores only browser-encrypted verification ciphertext and permits one setup per account.", responses: { "200": { description: "Created verification record", content: { "application/json": { schema: jsonSchema(z.object({ generationId: z.uuid() }), "output") } } } } }, (service, { body }) => service.createUnlockKeyVerification(body), { body: unlockKeyVerificationInput, maxBodyBytes: 8192, ownerSession: true }),
route("GET", "/api/v1/integrations/bearer", "flows:read", {
Expand Down
Loading