Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions surface/server/src/iron-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ export interface IronControlSecret {
id: string;
namespace: string;
foreign_id: string;
name?: string;
labels?: Record<string, unknown>;
inject_config?: { header?: string; formatter?: string; query_param?: string };
replace_config?: Record<string, unknown>;
rules?: Array<{
host?: string;
cidr?: string;
http_methods?: string[];
paths?: string[];
}>;
updated_at?: string;
}

export interface IronControlBrokerCredential {
Expand Down Expand Up @@ -212,6 +223,16 @@ export class IronControlAdminClient {
});
}

async listStaticSecrets(
args: { labels?: Record<string, string | boolean | number> } = {},
): Promise<IronControlSecret[]> {
const params = new URLSearchParams({ namespace: this.namespace });
for (const [key, value] of Object.entries(args.labels ?? {})) {
params.set(`labels[${key}]`, String(value));
}
return this.getList<IronControlSecret>(`/api/v1/static_secrets?${params.toString()}`);
}

/**
* Register the no-op secret that puts an egress host set on the proxy
* allowlist (see `EGRESS_HOST_SETS`). Shape constraints, all load-bearing:
Expand Down
302 changes: 300 additions & 2 deletions surface/server/src/routes/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ImportLocalAgentProfileBodySchema,
} from '@atrium/surface-client/agentProfiles';
import { normalizePrefs, normalizePrefsPatch } from '@atrium/surface-client/prefs';
import { createHmac, timingSafeEqual } from 'node:crypto';
import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { Schema } from 'effect';
import { githubConnectionAuditMetadata } from '../connection-audit.js';
Expand All @@ -24,6 +24,10 @@ import type { WsHub } from '../hub.js';
import {
type IronControlAdminClient,
atriumPrincipalForeignId,
claudeStaticSecretForeignId,
codexAccountIdSecretForeignId,
codexBearerSecretForeignId,
codexBrokerCredentialForeignId,
githubAppUserBrokerCredentialForeignId,
} from '../iron-control.js';
import {
Expand All @@ -35,7 +39,7 @@ import {
upsertGitHubInstallationBrokerCredential,
} from '../github-iron-control.js';
import { type AgentProfiles, providerFromProfileValue } from '../agent-profiles.js';
import { CODEX_PROVIDER, type ProviderCredentials } from '../provider-credentials.js';
import { CODEX_PROVIDER, PROXY_CREDENTIAL_SENTINEL, type ProviderCredentials } from '../provider-credentials.js';
import { PendingOAuthStore } from '../provider-oauth.js';
import type { SessionRuns } from '../session-runs.js';
import { decodeRouteBody, decodeRouteParams, decodeRouteQuery } from '../route-schema.js';
Expand All @@ -51,6 +55,31 @@ export interface MeRouteDeps extends AppMutationContext {
sessionRuns: Pick<SessionRuns, 'clearClaudeAuthRequired' | 'clearProviderAuthRequired'>;
}

type CredentialStoreItemJson = {
id: string;
kind: 'agent_provider' | 'connection_identity' | 'static_header';
provider: string;
label: string;
connected: boolean;
status: 'connected' | 'needs_auth' | 'public_read' | 'unavailable';
workspaceId: string | null;
accountLabel: string | null;
tokenKind: string | null;
scope: string | null;
backingStore: 'atrium_local' | 'iron_control' | 'public_read' | 'unavailable';
active: boolean;
ironControl: {
namespace: string | null;
principalForeignId: string | null;
brokerCredentialId: string | null;
staticSecretId: string | null;
staticSecretForeignId: string | null;
};
lastValidatedAt: string | null;
lastError: string | null;
updatedAt: string | null;
};

function isPlainObject(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
Expand Down Expand Up @@ -111,6 +140,15 @@ const CodexAuthJsonBodySchema = Schema.Struct({
authJson: Schema.optional(Schema.Unknown),
});

const StaticHeaderCredentialBodySchema = Schema.Struct({
workspaceId: Schema.optional(Schema.Unknown),
name: Schema.optional(Schema.Unknown),
host: Schema.optional(Schema.Unknown),
header: Schema.optional(Schema.Unknown),
secret: Schema.optional(Schema.Unknown),
formatter: Schema.optional(Schema.Unknown),
});

const AgentProfileParamsSchema = Schema.Struct({
id: Schema.String,
});
Expand Down Expand Up @@ -150,6 +188,236 @@ export function registerMeRoutes(app: FastifyInstance, deps: MeRouteDeps): void
return { providers: await providerCredentials.list(user.id) };
});

const resolveCredentialStoreWorkspace = async (
userId: string,
requestedWorkspaceId: string | undefined,
reply: FastifyReply,
): Promise<string | null> => {
const workspaceId = await connections.resolveWorkspaceId(userId, requestedWorkspaceId);
if (!workspaceId) {
const error = requestedWorkspaceId ? 'workspace_not_found' : 'no_workspace';
reply.code(requestedWorkspaceId ? 404 : 403).send({ error, message: 'workspace not found' });
return null;
}
return workspaceId;
};

const loadCredentialStore = async (userId: string, workspaceId: string) => {
const [providers, connectionList, backingRows] = await Promise.all([
providerCredentials.list(userId),
connections.list(userId, workspaceId),
pool.query<{ provider: string; token_ciphertext: string }>(
`SELECT provider, token_ciphertext
FROM user_provider_credentials
WHERE user_id = $1
AND provider = ANY($2::text[])`,
[userId, ['claude-code', 'codex']],
),
]);
const backingByProvider = new Map(backingRows.rows.map((row) => [row.provider, row.token_ciphertext]));
const principalForeignId = atriumPrincipalForeignId(workspaceId, userId);
const providerItems: CredentialStoreItemJson[] = providers.map((provider) => {
const proxyBacked = backingByProvider.get(provider.provider) === PROXY_CREDENTIAL_SENTINEL;
const codex = provider.provider === 'codex';
return {
id: `agent:${provider.provider}`,
kind: 'agent_provider',
provider: provider.provider,
label: codex ? 'Codex' : 'Claude Code',
connected: provider.connected,
status: provider.status,
workspaceId,
accountLabel: null,
tokenKind: proxyBacked ? 'oauth_proxy' : provider.connected ? 'local_secret' : null,
scope: provider.provider === 'codex' ? 'chatgpt.com' : 'api.anthropic.com',
backingStore: proxyBacked ? 'iron_control' : provider.connected ? 'atrium_local' : 'unavailable',
active: provider.connected,
ironControl: {
namespace: ironControl.namespace,
principalForeignId,
brokerCredentialId: proxyBacked && codex ? codexBrokerCredentialForeignId(workspaceId, userId) : null,
staticSecretId: null,
staticSecretForeignId: proxyBacked
? codex
? [
codexBearerSecretForeignId(workspaceId, userId),
codexAccountIdSecretForeignId(workspaceId, userId),
].join(', ')
: claudeStaticSecretForeignId(workspaceId, userId)
: null,
},
lastValidatedAt: provider.lastValidatedAt,
lastError: provider.lastError,
updatedAt: provider.updatedAt,
};
});
const connectionItems: CredentialStoreItemJson[] = [];
for (const connection of connectionList) {
const base = connection.identities.length > 0 ? connection.identities : [];
if (base.length === 0) {
connectionItems.push({
id: `connection:${connection.provider}:public-read`,
kind: 'connection_identity',
provider: connection.provider,
label: 'GitHub public read',
connected: connection.connected,
status: connection.status,
workspaceId: connection.workspaceId,
accountLabel: connection.accountLabel,
tokenKind: connection.tokenKind,
scope: 'github.com',
backingStore: connection.status === 'public_read' ? 'public_read' : 'unavailable',
active: true,
ironControl: {
namespace: ironControl.namespace,
principalForeignId,
brokerCredentialId: null,
staticSecretId: null,
staticSecretForeignId: null,
},
lastValidatedAt: connection.lastValidatedAt,
lastError: connection.lastError,
updatedAt: connection.updatedAt,
});
continue;
}
for (const identity of base) {
const metadata = identity.metadata ?? {};
const brokerCredentialId = metadataString(metadata, 'brokerCredentialId');
const staticSecretId = metadataString(metadata, 'staticSecretId');
const staticSecretForeignId = metadataString(metadata, 'staticSecretForeignId');
connectionItems.push({
id: `connection:${connection.provider}:${identity.id}`,
kind: 'connection_identity',
provider: connection.provider,
label: githubCredentialStoreLabel(identity.tokenKind, identity.accountLabel),
connected: identity.connected,
status: identity.status,
workspaceId: identity.workspaceId,
accountLabel: identity.accountLabel,
tokenKind: identity.tokenKind,
scope: 'github.com',
backingStore: brokerCredentialId || staticSecretId || staticSecretForeignId ? 'iron_control' : 'unavailable',
active: identity.active,
ironControl: {
namespace: ironControl.namespace,
principalForeignId,
brokerCredentialId,
staticSecretId,
staticSecretForeignId,
},
lastValidatedAt: identity.lastValidatedAt,
lastError: identity.lastError,
updatedAt: identity.updatedAt,
});
}
}
const customItems: CredentialStoreItemJson[] = ironControl.configured
? (
await ironControl.listStaticSecrets({
labels: {
source: 'atrium',
kind: 'custom-header',
atrium_workspace_id: workspaceId,
atrium_user_id: userId,
},
})
).map((secret): CredentialStoreItemJson => {
const host = secret.rules?.find((rule) => rule.host)?.host ?? null;
const header = secret.inject_config?.header ?? 'header';
return {
id: `custom:${secret.id}`,
kind: 'static_header',
provider: 'custom',
label: secret.name ?? secret.foreign_id,
connected: true,
status: 'connected',
workspaceId,
accountLabel: null,
tokenKind: `static ${header}`,
scope: host,
backingStore: 'iron_control',
active: true,
ironControl: {
namespace: secret.namespace,
principalForeignId,
brokerCredentialId: null,
staticSecretId: secret.id,
staticSecretForeignId: secret.foreign_id,
},
lastValidatedAt: null,
lastError: null,
updatedAt: secret.updated_at ?? null,
};
})
: [];
return {
credentialStore: {
configured: ironControl.configured,
namespace: ironControl.configured ? ironControl.namespace : null,
workspaceId,
items: [...providerItems, ...connectionItems, ...customItems],
},
};
};

app.get('/api/me/credential-store', async (req, reply) => {
const user = requireUser(req, reply);
if (!user) return;
const query = decodeRouteQuery(WorkspaceQuerySchema, req.query);
const requestedWorkspaceId = typeof query.workspaceId === 'string' ? query.workspaceId : undefined;
const workspaceId = await resolveCredentialStoreWorkspace(user.id, requestedWorkspaceId, reply);
if (!workspaceId) return;
return loadCredentialStore(user.id, workspaceId);
});

app.post('/api/me/credential-store/static-header', async (req, reply) => {
const user = requireUser(req, reply);
if (!user) return;
if (!ironControl.configured) {
return reply.code(503).send({ error: 'iron_control_unconfigured', message: 'iron-control is not configured' });
}
const body = decodeRouteBody(StaticHeaderCredentialBodySchema, req.body);
const requestedWorkspaceId = typeof body.workspaceId === 'string' ? body.workspaceId : undefined;
const workspaceId = await resolveCredentialStoreWorkspace(user.id, requestedWorkspaceId, reply);
if (!workspaceId) return;
const name = stringOrNull(body.name);
const host = normalizeCredentialHost(body.host);
const header = normalizeCredentialHeader(body.header);
const secret = typeof body.secret === 'string' ? body.secret.trim() : '';
const formatter = body.formatter === 'bearer' ? 'Bearer {{.Value}}' : undefined;
if (!name) return reply.code(400).send({ error: 'bad_request', message: 'Name is required' });
if (!host) return reply.code(400).send({ error: 'bad_request', message: 'Host must be a bare hostname' });
if (!header) return reply.code(400).send({ error: 'bad_request', message: 'Header name is invalid' });
if (!secret) return reply.code(400).send({ error: 'bad_request', message: 'Secret value is required' });

const principalForeignId = atriumPrincipalForeignId(workspaceId, user.id);
const principal = await ironControl.upsertPrincipal({
foreignId: principalForeignId,
name: `Atrium Workspace ${workspaceId} User ${user.id}`,
labels: { source: 'atrium', atrium_workspace_id: workspaceId, atrium_user_id: user.id },
});
const staticSecret = await ironControl.upsertInjectSecret({
foreignId: `custom-header-${workspaceId}-${user.id}-${randomUUID()}`,
name,
header,
...(formatter ? { formatter } : {}),
host,
source: { kind: 'control_plane', secret },
labels: {
source: 'atrium',
kind: 'custom-header',
provider: 'custom',
atrium_workspace_id: workspaceId,
atrium_user_id: user.id,
host,
header,
},
});
await ironControl.createPrincipalStaticGrant(principal.id, staticSecret.id);
return reply.code(201).send(await loadCredentialStore(user.id, workspaceId));
});

app.get('/api/me/connections', async (req, reply) => {
const user = requireUser(req, reply);
if (!user) return;
Expand Down Expand Up @@ -948,6 +1216,36 @@ function metadataString(metadata: Record<string, unknown>, key: string): string
return typeof value === 'string' && value.trim() ? value.trim() : null;
}

function normalizeCredentialHost(value: unknown): string | null {
if (typeof value !== 'string') return null;
const host = value.trim().toLowerCase();
if (!host || host.includes('/') || host.includes(':') || host.includes('@')) return null;
if (!/^[a-z0-9.-]+$/.test(host)) return null;
if (!host.includes('.') || host.startsWith('.') || host.endsWith('.') || host.includes('..')) return null;
return host;
}

function normalizeCredentialHeader(value: unknown): string | null {
if (typeof value !== 'string') return null;
const header = value.trim();
if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(header)) return null;
return header;
}

function githubCredentialStoreLabel(tokenKind: string, accountLabel: string | null): string {
const account = accountLabel ? ` ${accountLabel}` : '';
switch (tokenKind) {
case 'app_installation':
return `GitHub app installation${account}`;
case 'app_user':
return `GitHub user${account}`;
case 'pat':
return `GitHub PAT${account}`;
default:
return `GitHub${account}`;
}
}

class RouteResponse extends Error {
constructor(
readonly statusCode: number,
Expand Down
Loading
Loading