Add provider auth lifecycle contracts

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 19:54:02 -04:00
1 parent d6e797e2b9
commit 520299ce3d
4 files changed
+94 -2

No files matched your search

@@ -1,6 +1,14 @@
import { PROVIDER_AUTH_TYPE, type ProviderAuthType } from "../../shared/domain";
import { listProviderModels, type ProviderModelEntry } from "../../shared/provider-models";
import type { ProviderObservedRateLimitState } from "../../shared/provider-runtime-domain";
import {
authorizeWithParser,
refreshUnsupported,
revokeLocalProfile,
type ProviderAuthorizeHandler,
type ProviderRefreshHandler,
type ProviderRevokeHandler,
} from "./provider-auth-lifecycle";
import { parseProviderRateLimitHeaders } from "./provider-rate-limit-registry";
export interface ProviderExecutionInput {
@@ -42,6 +50,9 @@ export interface ProviderRateLimitState {
export interface ProviderAdapter {
providerKey: string;
authType: ProviderAuthType;
authorize: ProviderAuthorizeHandler;
refresh: ProviderRefreshHandler;
revoke: ProviderRevokeHandler;
parseAuthProfile(secret: string): ProviderAuthProfile;
listModels(ctx: ProviderContext): Promise<ProviderModelEntry[]>;
getRateLimitState(ctx: ProviderContext): Promise<ProviderRateLimitState | null>;
@@ -86,6 +97,9 @@ export const providerAdapters: ProviderAdapterRegistry = {
openai: {
providerKey: "openai",
authType: PROVIDER_AUTH_TYPE.API_KEY,
authorize: authorizeWithParser(parseApiKey),
refresh: refreshUnsupported("OpenAI API key"),
revoke: revokeLocalProfile((profile) => { requireApiKeyProfile(profile, "OpenAI"); }),
parseAuthProfile: parseApiKey,
listModels: () => listStaticModels("openai"),
getRateLimitState: providerRateLimitUnknown,
@@ -107,6 +121,9 @@ export const providerAdapters: ProviderAdapterRegistry = {
anthropic: {
providerKey: "anthropic",
authType: PROVIDER_AUTH_TYPE.API_KEY,
authorize: authorizeWithParser(parseApiKey),
refresh: refreshUnsupported("Anthropic API key"),
revoke: revokeLocalProfile((profile) => { requireApiKeyProfile(profile, "Anthropic"); }),
parseAuthProfile: parseApiKey,
listModels: () => listStaticModels("anthropic"),
getRateLimitState: providerRateLimitUnknown,
@@ -152,6 +169,9 @@ export const providerAdapters: ProviderAdapterRegistry = {
"azure-openai": {
providerKey: "azure-openai",
authType: PROVIDER_AUTH_TYPE.CLOUD,
authorize: authorizeWithParser(parseAzureProfile),
refresh: refreshUnsupported("Azure OpenAI cloud credential"),
revoke: revokeLocalProfile((profile) => { requireAzureProfile(profile); }),
parseAuthProfile: parseAzureProfile,
listModels: () => listStaticModels("azure-openai"),
getRateLimitState: providerRateLimitUnknown,
@@ -164,6 +184,9 @@ export const providerAdapters: ProviderAdapterRegistry = {
"aws-bedrock": {
providerKey: "aws-bedrock",
authType: PROVIDER_AUTH_TYPE.CLOUD,
authorize: authorizeWithParser(parseBedrockProfile),
refresh: refreshUnsupported("AWS Bedrock cloud credential"),
revoke: revokeLocalProfile((profile) => { requireBedrockProfile(profile); }),
parseAuthProfile: parseBedrockProfile,
listModels: () => listStaticModels("aws-bedrock"),
getRateLimitState: providerRateLimitUnknown,
@@ -175,6 +198,9 @@ export const providerAdapters: ProviderAdapterRegistry = {
"vertex-ai": {
providerKey: "vertex-ai",
authType: PROVIDER_AUTH_TYPE.CLOUD,
authorize: authorizeWithParser(parseVertexProfile),
refresh: refreshUnsupported("Vertex AI cloud credential"),
revoke: revokeLocalProfile((profile) => { requireVertexProfile(profile); }),
parseAuthProfile: parseVertexProfile,
listModels: () => listStaticModels("vertex-ai"),
getRateLimitState: providerRateLimitUnknown,
@@ -271,6 +297,9 @@ function openAiCompatibleAdapter({
return {
providerKey,
authType: PROVIDER_AUTH_TYPE.API_KEY,
authorize: authorizeWithParser(parseApiKey),
refresh: refreshUnsupported(`${label} API key`),
revoke: revokeLocalProfile((profile) => { requireApiKeyProfile(profile, label); }),
parseAuthProfile: parseApiKey,
listModels: () => listStaticModels(providerKey),
getRateLimitState: providerRateLimitUnknown,
@@ -301,10 +330,15 @@ async function checkOpenAiHealth(authProfile: ProviderAuthProfile, healthUrl: st
}
function browserTokenAdapter(providerKey: string, authType: ProviderAuthType): ProviderAdapter {
const parseAuthProfile = authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION ? parseAccountToken : parseAccessToken;
const assertProfile = authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION ? requireAccountTokenProfile : requireAccessTokenProfile;
return {
providerKey,
authType,
parseAuthProfile: authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION ? parseAccountToken : parseAccessToken,
authorize: authorizeWithParser(parseAuthProfile),
refresh: refreshUnsupported(`${providerKey} token`),
revoke: revokeLocalProfile((profile) => { assertProfile(profile); }),
parseAuthProfile,
listModels: () => listStaticModels(providerKey),
getRateLimitState: providerRateLimitUnknown,
checkHealth: async (input) => {
@@ -0,0 +1,43 @@
import type { ProviderAuthProfile } from "./provider-adapters";
export interface ProviderAuthorizeInput {
secret: string;
}
export interface ProviderAuthorizeResult {
authProfile: ProviderAuthProfile;
}
export interface ProviderRefreshInput {
authProfile: ProviderAuthProfile;
}
export interface ProviderRefreshResult {
authProfile: ProviderAuthProfile;
rotatedSecret: string | null;
refreshedAt: string;
}
export interface ProviderRevokeInput {
authProfile: ProviderAuthProfile;
}
export type ProviderAuthorizeHandler = (input: ProviderAuthorizeInput) => Promise<ProviderAuthorizeResult>;
export type ProviderRefreshHandler = (input: ProviderRefreshInput) => Promise<ProviderRefreshResult>;
export type ProviderRevokeHandler = (input: ProviderRevokeInput) => Promise<void>;
export function authorizeWithParser(parseAuthProfile: (secret: string) => ProviderAuthProfile): ProviderAuthorizeHandler {
return async (input) => ({ authProfile: parseAuthProfile(input.secret) });
}
export function refreshUnsupported(label: string): ProviderRefreshHandler {
return async () => {
throw new Error(`${label} does not support token refresh.`);
};
}
export function revokeLocalProfile(assertProfile: (profile: ProviderAuthProfile) => void): ProviderRevokeHandler {
return async (input) => {
assertProfile(input.authProfile);
};
}
@@ -244,7 +244,11 @@ export class SdlcService {
const missing = catalogKeys.filter((key) => !adapterKeys.includes(key));
const runtimeContractReady = PROVIDER_CATALOG.every((provider) => {
const adapter = providerAdapters[provider.key];
return typeof adapter.listModels === "function" && typeof adapter.getRateLimitState === "function";
return typeof adapter.authorize === "function"
&& typeof adapter.refresh === "function"
&& typeof adapter.revoke === "function"
&& typeof adapter.listModels === "function"
&& typeof adapter.getRateLimitState === "function";
});
const schemaVersion = count(this.db, "select coalesce(max(version), 0) as count from schema_migrations");
const rateLimitRegistry = count(this.db, `
@@ -90,6 +90,17 @@ describe("provider contracts", () => {
}
});
test("exposes auth lifecycle adapter contracts", async () => {
for (const provider of PROVIDER_CATALOG) {
const adapter = providerAdapters[provider.key];
const secret = PROVIDER_SECRET_SAMPLES[provider.key];
const profile = adapter.parseAuthProfile(secret);
await expect(adapter.authorize({ secret })).resolves.toEqual({ authProfile: profile });
await expect(adapter.revoke({ authProfile: profile })).resolves.toBeUndefined();
await expect(adapter.refresh({ authProfile: profile })).rejects.toThrow("does not support token refresh");
}
});
test("runs the seeded contract eval suite", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);