mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(server-v2): treat already-authenticated OAuth start as success
When the Kimi OAuth toolkit short-circuits via its `ensureFresh` fast path (the cached refresh token is still usable), `toolkit.login` resolves without firing `onDeviceCode`. The v2 `OAuthService.startLogin` previously rejected in this case, so `POST /api/v1/oauth/login` returned a 500 that the web client swallowed and mislabeled as "current daemon does not support login". CLI-authenticated users could not (re)enter the v2 daemon through the web UI. Treat that path as already authenticated: provision the managed provider (idempotent), refresh OAuth provider models so `defaultModel` is seeded (`/auth` then reports ready, matching v1 parity), and return a new `status: 'authenticated'` `OAuthFlowStart` variant so the web LoginDialog can skip the device-code UI entirely instead of falling into its catch-all "unsupported daemon" state. The protocol `OAuthFlowStart` schema is now a discriminated union on `status`; kimi-web's wire / composable / LoginDialog types are updated to handle the new `authenticated` branch. Tests: updated the v2 OAuthService test that previously asserted the buggy rejection to instead assert the fast path returns authenticated and provisions the provider.
This commit is contained in:
parent
c51d8df461
commit
ebe16e39ff
8 changed files with 283 additions and 79 deletions
|
|
@ -26,6 +26,7 @@ import type {
|
|||
KimiEventConnection,
|
||||
KimiEventHandlers,
|
||||
KimiWebApi,
|
||||
OAuthLoginStartResult,
|
||||
Page,
|
||||
PageRequest,
|
||||
PromptSubmission,
|
||||
|
|
@ -1184,27 +1185,24 @@ export class DaemonKimiWebApi implements KimiWebApi {
|
|||
};
|
||||
}
|
||||
|
||||
async startOAuthLogin(): Promise<{
|
||||
flowId: string;
|
||||
provider: string;
|
||||
verificationUri: string;
|
||||
verificationUriComplete: string;
|
||||
userCode: string;
|
||||
expiresIn: number;
|
||||
interval: number;
|
||||
status: 'pending';
|
||||
expiresAt: string;
|
||||
}> {
|
||||
async startOAuthLogin(): Promise<OAuthLoginStartResult> {
|
||||
const data = await this.http.post<WireOAuthLoginStartResult>('/oauth/login', {});
|
||||
if (data.status === 'authenticated') {
|
||||
return {
|
||||
flowId: data.flow_id,
|
||||
provider: data.provider,
|
||||
status: 'authenticated',
|
||||
};
|
||||
}
|
||||
return {
|
||||
flowId: data.flow_id,
|
||||
provider: data.provider,
|
||||
status: 'pending',
|
||||
verificationUri: data.verification_uri,
|
||||
verificationUriComplete: data.verification_uri_complete,
|
||||
userCode: data.user_code,
|
||||
expiresIn: data.expires_in,
|
||||
interval: data.interval,
|
||||
status: data.status,
|
||||
expiresAt: data.expires_at,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -413,18 +413,35 @@ export interface WireAuthResult {
|
|||
managed_provider: WireManagedProvider | null;
|
||||
}
|
||||
|
||||
export interface WireOAuthLoginStartResult {
|
||||
// `POST /oauth/login` returns one of two shapes, discriminated by `status`:
|
||||
// - `pending`: a real device-code flow was started; all device fields are
|
||||
// populated so the client can render the device-code step and poll.
|
||||
// - `authenticated`: the toolkit already had a usable token and short-
|
||||
// circuited via its `ensureFresh` fast path, so no device code was
|
||||
// issued; the client can skip the device-code step and treat the login
|
||||
// as already complete.
|
||||
interface WireOAuthLoginStartPending {
|
||||
flow_id: string;
|
||||
provider: string;
|
||||
status: 'pending';
|
||||
verification_uri: string;
|
||||
verification_uri_complete: string;
|
||||
user_code: string;
|
||||
expires_in: number;
|
||||
interval: number;
|
||||
status: 'pending';
|
||||
expires_at: string;
|
||||
}
|
||||
|
||||
interface WireOAuthLoginStartAuthenticated {
|
||||
flow_id: string;
|
||||
provider: string;
|
||||
status: 'authenticated';
|
||||
}
|
||||
|
||||
export type WireOAuthLoginStartResult =
|
||||
| WireOAuthLoginStartPending
|
||||
| WireOAuthLoginStartAuthenticated;
|
||||
|
||||
export interface WireOAuthLoginPollResult {
|
||||
flow_id: string;
|
||||
status: 'pending' | 'authenticated' | 'expired' | 'cancelled';
|
||||
|
|
|
|||
|
|
@ -734,17 +734,7 @@ export interface KimiWebApi {
|
|||
defaultModel: string | null;
|
||||
managedProvider: { status: string } | null;
|
||||
}>;
|
||||
startOAuthLogin(): Promise<{
|
||||
flowId: string;
|
||||
provider: string;
|
||||
verificationUri: string;
|
||||
verificationUriComplete: string;
|
||||
userCode: string;
|
||||
expiresIn: number;
|
||||
interval: number;
|
||||
status: 'pending';
|
||||
expiresAt: string;
|
||||
}>;
|
||||
startOAuthLogin(): Promise<OAuthLoginStartResult>;
|
||||
pollOAuthLogin(): Promise<{
|
||||
flowId: string;
|
||||
status: 'pending' | 'authenticated' | 'expired' | 'cancelled';
|
||||
|
|
@ -753,3 +743,22 @@ export interface KimiWebApi {
|
|||
cancelOAuthLogin(): Promise<{ cancelled: boolean; status: string }>;
|
||||
logout(): Promise<{ loggedOut: boolean }>;
|
||||
}
|
||||
|
||||
/** Result of `startOAuthLogin()`, mirroring the wire discriminated union. */
|
||||
export type OAuthLoginStartResult =
|
||||
| {
|
||||
flowId: string;
|
||||
provider: string;
|
||||
status: 'pending';
|
||||
verificationUri: string;
|
||||
verificationUriComplete: string;
|
||||
userCode: string;
|
||||
expiresIn: number;
|
||||
interval: number;
|
||||
expiresAt: string;
|
||||
}
|
||||
| {
|
||||
flowId: string;
|
||||
provider: string;
|
||||
status: 'authenticated';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,17 +33,25 @@ const emit = defineEmits<{
|
|||
// -------------------------------------------------------------------------
|
||||
|
||||
const props = defineProps<{
|
||||
onStartOAuthLogin: () => Promise<{
|
||||
flowId: string;
|
||||
provider: string;
|
||||
verificationUri: string;
|
||||
verificationUriComplete: string;
|
||||
userCode: string;
|
||||
expiresIn: number;
|
||||
interval: number;
|
||||
status: 'pending';
|
||||
expiresAt: string;
|
||||
} | null>;
|
||||
onStartOAuthLogin: () => Promise<
|
||||
| {
|
||||
flowId: string;
|
||||
provider: string;
|
||||
status: 'pending';
|
||||
verificationUri: string;
|
||||
verificationUriComplete: string;
|
||||
userCode: string;
|
||||
expiresIn: number;
|
||||
interval: number;
|
||||
expiresAt: string;
|
||||
}
|
||||
| {
|
||||
flowId: string;
|
||||
provider: string;
|
||||
status: 'authenticated';
|
||||
}
|
||||
| null
|
||||
>;
|
||||
onPollOAuthLogin: () => Promise<{
|
||||
flowId: string;
|
||||
status: 'pending' | 'authenticated' | 'expired' | 'cancelled';
|
||||
|
|
@ -107,6 +115,19 @@ async function startFlow(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Already-authenticated fast path: the server had a usable cached token and
|
||||
// did not issue a device code. Skip the device-code UI entirely and surface
|
||||
// the success state — the poller is irrelevant here.
|
||||
if (result.status === 'authenticated') {
|
||||
stopTimers();
|
||||
step.value = 'success';
|
||||
setTimeout(() => {
|
||||
emit('success');
|
||||
emit('close');
|
||||
}, 800);
|
||||
return;
|
||||
}
|
||||
|
||||
flow.value = {
|
||||
flowId: result.flowId,
|
||||
verificationUri: result.verificationUri,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@
|
|||
|
||||
import { ref, type ComputedRef } from 'vue';
|
||||
import { getKimiWebApi } from '../../api';
|
||||
import type { AppMessage, AppModel, AppProvider, AppSession, AppSkill, ThinkingLevel } from '../../api/types';
|
||||
import type {
|
||||
AppMessage,
|
||||
AppModel,
|
||||
AppProvider,
|
||||
AppSession,
|
||||
AppSkill,
|
||||
OAuthLoginStartResult,
|
||||
ThinkingLevel,
|
||||
} from '../../api/types';
|
||||
import { safeGetString, safeSetString, STORAGE_KEYS } from '../../lib/storage';
|
||||
import { coerceThinkingForModel } from '../../lib/modelThinking';
|
||||
import type { ActivityState } from '../../types';
|
||||
|
|
@ -340,17 +348,7 @@ export function useModelProviderState(
|
|||
}
|
||||
|
||||
/** Start managed Kimi OAuth device flow. Returns flow data or null on error. */
|
||||
async function startOAuthLogin(): Promise<{
|
||||
flowId: string;
|
||||
provider: string;
|
||||
verificationUri: string;
|
||||
verificationUriComplete: string;
|
||||
userCode: string;
|
||||
expiresIn: number;
|
||||
interval: number;
|
||||
status: 'pending';
|
||||
expiresAt: string;
|
||||
} | null> {
|
||||
async function startOAuthLogin(): Promise<OAuthLoginStartResult | null> {
|
||||
try {
|
||||
const api = getKimiWebApi();
|
||||
return await api.startOAuthLogin();
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
import type {
|
||||
OAuthFlowSnapshot,
|
||||
OAuthFlowStart,
|
||||
OAuthFlowStartPending,
|
||||
OAuthFlowStatus,
|
||||
OAuthLoginCancelResponse,
|
||||
OAuthLogoutResponse,
|
||||
|
|
@ -134,20 +135,28 @@ export class OAuthService extends Disposable implements IOAuthService {
|
|||
resolveDevice(auth);
|
||||
},
|
||||
});
|
||||
const fastPath: Promise<OAuthFlowStart | undefined> = loginPromise.then(async () => {
|
||||
if (state.device !== undefined) return undefined;
|
||||
this.log.info('oauth startLogin: toolkit resolved without device code (already authenticated)', {
|
||||
provider,
|
||||
});
|
||||
await this.completeAlreadyAuthenticatedLogin(state);
|
||||
return {
|
||||
flow_id: state.flowId,
|
||||
provider: state.provider,
|
||||
status: 'authenticated',
|
||||
};
|
||||
});
|
||||
|
||||
loginPromise.then(
|
||||
() => {
|
||||
this.log.info('oauth startLogin: toolkit.login resolved', {
|
||||
provider,
|
||||
deviceArrived: state.device !== undefined,
|
||||
});
|
||||
if (state.device === undefined) {
|
||||
this.flows.delete(provider);
|
||||
rejectDevice(
|
||||
new Error('OAuth login completed without issuing a device code (already authenticated).'),
|
||||
);
|
||||
return;
|
||||
if (state.device !== undefined) {
|
||||
this.handleSuccess(state);
|
||||
}
|
||||
this.handleSuccess(state);
|
||||
},
|
||||
(error) => {
|
||||
this.log.warn('oauth startLogin: toolkit.login rejected', {
|
||||
|
|
@ -159,8 +168,16 @@ export class OAuthService extends Disposable implements IOAuthService {
|
|||
},
|
||||
);
|
||||
|
||||
this.log.info('oauth startLogin: awaiting deviceReady', { provider });
|
||||
const device = await deviceReady;
|
||||
this.log.info('oauth startLogin: awaiting device flow start', { provider });
|
||||
const winner = await Promise.race([
|
||||
deviceReady.then((device) => ({ kind: 'device' as const, device })),
|
||||
fastPath.then((result) => ({ kind: 'fast' as const, result })),
|
||||
]);
|
||||
if (winner.kind === 'fast' && winner.result !== undefined) {
|
||||
this.log.info('oauth startLogin: fast path returned authenticated', { provider });
|
||||
return winner.result;
|
||||
}
|
||||
const device = winner.kind === 'device' ? winner.device : await deviceReady;
|
||||
this.log.info('oauth startLogin: deviceReady resolved', { provider });
|
||||
return this.toFlowStart(state, device);
|
||||
}
|
||||
|
|
@ -347,23 +364,42 @@ export class OAuthService extends Disposable implements IOAuthService {
|
|||
private handleSuccess(state: FlowState): void {
|
||||
if (state.status !== 'pending') return;
|
||||
this.setTerminal(state, 'authenticated');
|
||||
void this.provisionProvider(state.provider, state.oauthRef);
|
||||
void this.provisionProvider(state.provider, state.oauthRef).catch((error: unknown) => {
|
||||
this.log.warn('oauth provider provisioning failed', {
|
||||
provider: state.provider,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async completeAlreadyAuthenticatedLogin(state: FlowState): Promise<void> {
|
||||
if (state.status !== 'pending') return;
|
||||
await this.provisionProvider(state.provider, state.oauthRef);
|
||||
if (state.status !== 'pending') return;
|
||||
if (state.provider === KIMI_CODE_PROVIDER_NAME) {
|
||||
await this.refreshOAuthProviderModelsBestEffort(state.provider);
|
||||
if (state.status !== 'pending') return;
|
||||
}
|
||||
this.setTerminal(state, 'authenticated');
|
||||
}
|
||||
|
||||
private async provisionProvider(provider: string, oauthRef: OAuthRef | undefined): Promise<void> {
|
||||
if (oauthRef === undefined) return;
|
||||
const baseUrl = this.providerService.get(provider)?.baseUrl ?? kimiCodeBaseUrl();
|
||||
try {
|
||||
await this.providerService.set(provider, {
|
||||
type: 'kimi',
|
||||
baseUrl,
|
||||
apiKey: '',
|
||||
oauth: oauthRef,
|
||||
});
|
||||
} catch (error) {
|
||||
this.log.warn('oauth provider provisioning failed', {
|
||||
await this.providerService.set(provider, {
|
||||
type: 'kimi',
|
||||
baseUrl,
|
||||
apiKey: '',
|
||||
oauth: oauthRef,
|
||||
});
|
||||
}
|
||||
|
||||
private async refreshOAuthProviderModelsBestEffort(provider: string): Promise<void> {
|
||||
const result = await this.refreshOAuthProviderModels();
|
||||
if (result.failed.length > 0) {
|
||||
this.log.warn('oauth startLogin: model refresh failed on already-authenticated fast path', {
|
||||
provider,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
failures: result.failed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -416,7 +452,7 @@ export class OAuthService extends Disposable implements IOAuthService {
|
|||
state.gcTimer = timer;
|
||||
}
|
||||
|
||||
private toFlowStart(state: FlowState, device: DeviceAuthorization): OAuthFlowStart {
|
||||
private toFlowStart(state: FlowState, device: DeviceAuthorization): OAuthFlowStartPending {
|
||||
const expiresIn = device.expiresIn ?? DEFAULT_DEVICE_EXPIRES_IN_SEC;
|
||||
return {
|
||||
flow_id: state.flowId,
|
||||
|
|
|
|||
|
|
@ -70,13 +70,39 @@ describe('OAuthService', () => {
|
|||
},
|
||||
[NON_OAUTH_PROVIDER]: { type: 'openai', apiKey: 'sk-test' },
|
||||
};
|
||||
providerSet = vi.fn().mockResolvedValue(undefined);
|
||||
providerSet = vi.fn(async (name: string, config: ProviderConfig) => {
|
||||
providers = { ...providers, [name]: config };
|
||||
});
|
||||
models = {};
|
||||
services = undefined;
|
||||
defaultModel = undefined;
|
||||
defaultThinking = undefined;
|
||||
configSet = vi.fn().mockResolvedValue(undefined);
|
||||
configReplace = vi.fn().mockResolvedValue(undefined);
|
||||
configSet = vi.fn(async (domain: string, value: unknown) => {
|
||||
if (domain === 'defaultModel') {
|
||||
defaultModel = value as string | undefined;
|
||||
return;
|
||||
}
|
||||
if (domain === 'defaultThinking') {
|
||||
defaultThinking = value as boolean | undefined;
|
||||
return;
|
||||
}
|
||||
throw new Error(`unexpected config set: ${domain}`);
|
||||
});
|
||||
configReplace = vi.fn(async (domain: string, value: unknown) => {
|
||||
if (domain === 'providers') {
|
||||
providers = value as Record<string, ProviderConfig>;
|
||||
return;
|
||||
}
|
||||
if (domain === 'models') {
|
||||
models = value as Record<string, ModelAlias>;
|
||||
return;
|
||||
}
|
||||
if (domain === 'services') {
|
||||
services = value as Record<string, unknown> | undefined;
|
||||
return;
|
||||
}
|
||||
throw new Error(`unexpected config replace: ${domain}`);
|
||||
});
|
||||
events = [];
|
||||
toolkit = {
|
||||
login: vi.fn(),
|
||||
|
|
@ -135,6 +161,24 @@ describe('OAuthService', () => {
|
|||
return { providers, models, services, defaultModel, defaultThinking };
|
||||
}
|
||||
|
||||
function stubManagedModelsFetch(): ReturnType<typeof vi.fn> {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [
|
||||
{
|
||||
id: 'kimi-k2',
|
||||
context_length: 131072,
|
||||
supports_reasoning: true,
|
||||
display_name: 'Kimi K2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
it('startLogin resolves a device-code flow and flips to authenticated on success', async () => {
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
|
|
@ -156,8 +200,7 @@ describe('OAuthService', () => {
|
|||
expect.objectContaining({ oauthRef: { storage: 'file', key: 'oauth/kimi-code' } }),
|
||||
);
|
||||
|
||||
await flush();
|
||||
expect(svc.getFlow(OAUTH_PROVIDER)?.status).toBe('authenticated');
|
||||
await vi.waitFor(() => expect(svc.getFlow(OAUTH_PROVIDER)?.status).toBe('authenticated'));
|
||||
});
|
||||
|
||||
it('provisions the managed provider through the provider service after login', async () => {
|
||||
|
|
@ -213,11 +256,68 @@ describe('OAuthService', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('startLogin rejects when login completes without issuing a device code', async () => {
|
||||
it('startLogin returns authenticated when login resolves without issuing a device code (already-authenticated fast path)', async () => {
|
||||
const fetchMock = stubManagedModelsFetch();
|
||||
toolkit.login.mockResolvedValue({ providerName: OAUTH_PROVIDER, ok: true });
|
||||
const svc = createService();
|
||||
await expect(svc.startLogin(OAUTH_PROVIDER)).rejects.toThrow('already authenticated');
|
||||
expect(svc.getFlow(OAUTH_PROVIDER)).toBeUndefined();
|
||||
|
||||
const start = await svc.startLogin(OAUTH_PROVIDER);
|
||||
expect(start).toMatchObject({
|
||||
provider: OAUTH_PROVIDER,
|
||||
status: 'authenticated',
|
||||
flow_id: expect.any(String),
|
||||
});
|
||||
expect(providerSet).toHaveBeenCalledWith(
|
||||
OAUTH_PROVIDER,
|
||||
expect.objectContaining({
|
||||
type: 'kimi',
|
||||
baseUrl: 'https://api.example.com',
|
||||
oauth: { storage: 'file', key: 'oauth/kimi-code' },
|
||||
}),
|
||||
);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(configSet).toHaveBeenCalledWith('defaultModel', 'kimi-code/kimi-k2');
|
||||
});
|
||||
|
||||
it('startLogin returns authenticated when model refresh fails on the already-authenticated fast path', async () => {
|
||||
const fetchMock = vi.fn().mockRejectedValue(new Error('network disabled in test'));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
toolkit.login.mockResolvedValue({ providerName: OAUTH_PROVIDER, ok: true });
|
||||
const svc = createService();
|
||||
|
||||
await expect(svc.startLogin(OAUTH_PROVIDER)).resolves.toMatchObject({
|
||||
provider: OAUTH_PROVIDER,
|
||||
status: 'authenticated',
|
||||
flow_id: expect.any(String),
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(providerSet).toHaveBeenCalledWith(
|
||||
OAUTH_PROVIDER,
|
||||
expect.objectContaining({
|
||||
type: 'kimi',
|
||||
baseUrl: 'https://api.example.com',
|
||||
oauth: { storage: 'file', key: 'oauth/kimi-code' },
|
||||
}),
|
||||
);
|
||||
expect(configSet).not.toHaveBeenCalledWith('defaultModel', expect.any(String));
|
||||
});
|
||||
|
||||
it('keeps a device-code login authenticated when model fetch is unavailable after authorization', async () => {
|
||||
const fetchMock = vi.fn().mockRejectedValue(new Error('network disabled in test'));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return { providerName: OAUTH_PROVIDER, ok: true };
|
||||
});
|
||||
const svc = createService();
|
||||
|
||||
await expect(svc.startLogin(OAUTH_PROVIDER)).resolves.toMatchObject({
|
||||
provider: OAUTH_PROVIDER,
|
||||
status: 'pending',
|
||||
});
|
||||
await vi.waitFor(() => expect(svc.getFlow(OAUTH_PROVIDER)?.status).toBe('authenticated'));
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(configSet).not.toHaveBeenCalledWith('defaultModel', expect.any(String));
|
||||
});
|
||||
|
||||
it('cancelLogin aborts a pending flow and marks it cancelled', async () => {
|
||||
|
|
|
|||
|
|
@ -22,17 +22,42 @@ export const oauthLoginStartRequestSchema = z.object({
|
|||
});
|
||||
export type OAuthLoginStartRequest = z.infer<typeof oauthLoginStartRequestSchema>;
|
||||
|
||||
export const oauthFlowStartSchema = z.object({
|
||||
/**
|
||||
* Result of `POST /v1/oauth/login`.
|
||||
*
|
||||
* Two shapes, discriminated by `status`:
|
||||
* - `pending`: a real device-code flow was started; the `verification_*`,
|
||||
* `user_code`, `expires_*`, and `interval` fields are populated so the
|
||||
* client can render the device-code step and start polling.
|
||||
* - `authenticated`: the toolkit already had a usable token and short-
|
||||
* circuited via its `ensureFresh` fast path, so no device code was
|
||||
* issued. The client can skip the device-code step and treat the login
|
||||
* as already complete.
|
||||
*/
|
||||
export const oauthFlowStartPendingSchema = z.object({
|
||||
flow_id: z.string().min(1),
|
||||
provider: z.string().min(1),
|
||||
status: z.literal('pending'),
|
||||
verification_uri: z.string().url(),
|
||||
verification_uri_complete: z.string().url(),
|
||||
user_code: z.string().min(1),
|
||||
expires_in: z.number().int().positive(),
|
||||
interval: z.number().int().positive(),
|
||||
status: z.literal('pending'),
|
||||
expires_at: isoDateTimeSchema,
|
||||
});
|
||||
export type OAuthFlowStartPending = z.infer<typeof oauthFlowStartPendingSchema>;
|
||||
|
||||
export const oauthFlowStartAuthenticatedSchema = z.object({
|
||||
flow_id: z.string().min(1),
|
||||
provider: z.string().min(1),
|
||||
status: z.literal('authenticated'),
|
||||
});
|
||||
export type OAuthFlowStartAuthenticated = z.infer<typeof oauthFlowStartAuthenticatedSchema>;
|
||||
|
||||
export const oauthFlowStartSchema = z.discriminatedUnion('status', [
|
||||
oauthFlowStartPendingSchema,
|
||||
oauthFlowStartAuthenticatedSchema,
|
||||
]);
|
||||
export type OAuthFlowStart = z.infer<typeof oauthFlowStartSchema>;
|
||||
|
||||
export const oauthFlowSnapshotSchema = z.object({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue