/** The slice of ShellPathService {@link sharedSubscriptionsPath} needs. */ export interface SubscriptionShellPaths { resolveUserPath(...segments: string[]): string; } /** * Where provider subscriptions (OAuth sessions for providers like * 'openai-subscriber') live: `~/.goodvibes/shared/subscriptions.json`, one * file read and written by every surface on the machine, the daemon, the * TUI, the agent. * * A login is one event; every surface that later needs the token must see * it. Before this, each surface constructed its own `SubscriptionManager` * against `~/.goodvibes//subscriptions.json`, so a login * completed in the TUI was invisible to the daemon that actually hosts * conversation turns, the daemon kept refreshing whatever it already had * (or nothing), and a successful login changed nothing from its point of * view. */ export declare function sharedSubscriptionsPath(shellPaths: SubscriptionShellPaths): string; export interface OAuthProviderConfig { readonly authUrl: string; readonly tokenUrl: string; readonly clientId: string; readonly redirectUri: string; readonly manualRedirectUri?: string | undefined; readonly scopes?: readonly string[] | undefined; readonly audience?: string | undefined; readonly usePkce?: boolean | undefined; readonly authParams?: Readonly> | undefined; readonly tokenRequestEncoding?: 'form' | 'json' | undefined; readonly includeStateInTokenRequest?: boolean | undefined; readonly tokenRequestExtras?: Readonly> | undefined; readonly refreshRequestEncoding?: 'form' | 'json' | undefined; readonly refreshRequestExtras?: Readonly> | undefined; readonly refreshScopes?: readonly string[] | undefined; readonly overrideAmbientApiKeys?: boolean | undefined; readonly localCallback?: { readonly host?: string | undefined; readonly port?: number | undefined; readonly path?: string | undefined; readonly autoComplete?: boolean | undefined; }; } export interface PendingSubscriptionLogin { readonly provider: string; readonly state: string; readonly verifier: string; readonly redirectUri: string; readonly createdAt: number; } export interface ProviderSubscription { readonly provider: string; readonly accessToken: string; readonly refreshToken?: string | undefined; readonly tokenType: string; readonly expiresAt?: number | undefined; readonly scopes?: readonly string[] | undefined; readonly authMode: 'oauth'; readonly overrideAmbientApiKeys: boolean; readonly createdAt: number; readonly updatedAt: number; /** * Set when the authorization server REFUSED this session's grant (an * answered 4xx on refresh): the session is over regardless of what * `expiresAt` says, and every surface must show it as signed-out instead * of deriving "healthy" from the timestamp. Cleared by a successful * refresh; a completed login replaces the whole record. */ readonly revokedAt?: number | undefined; } /** * SubscriptionManager, OAuth flows for **provider subscriptions**. * * Manages OAuth-based subscriptions to external AI providers (OpenAI, * Anthropic, Gemini, etc.) including authorization URL generation, code * exchange, token refresh, and persisting credentials to disk. * * This class handles provider-subscription OAuth on behalf of the daemon. * For OAuth flows that authenticate the SDK client with the goodvibes daemon * itself, see {@link OAuthClient} in `../runtime/auth/oauth-client.ts`. * * @see OAuthClient, OAuth flows for daemon authentication. */ export interface SubscriptionManagerOptions { /** * A surface-scoped store this manager used to own before subscriptions * moved to the shared tier (e.g. the old `~/.goodvibes//subscriptions.json`). * Folded in once, synchronously, at construction: see {@link SubscriptionManager.foldLegacyStore}. * Omit when there is no legacy surface store to migrate from (a brand-new * install, or a caller that never had a surface-scoped store to begin with). */ readonly legacyPath?: string; } export declare class SubscriptionManager { private readonly path; constructor(path: string, options?: SubscriptionManagerOptions); /** * Best-effort, READ-ONLY parse of a legacy store for migration. Never * throws and never quarantines: quarantining renames the file, and a * legacy path is not this manager's file to touch, an older build still * pointed at it must find it exactly as it left it, corrupt or not. Any * parse failure just yields nothing to fold, which is the same outcome as * "no legacy store existed". */ private readLegacyStoreReadOnly; /** * One-time fold of a legacy per-surface store into this (now shared) * store, run synchronously at construction so every code path that reads * `get()`/`resolveAccessToken()` right after construction already sees the * folded result. * * Per provider, the newer `updatedAt` wins; a provider already in the * shared store with an equal-or-newer record is left untouched. That * strict `>` comparison is what makes a second boot a no-op instead of a * re-fold or a downgrade: once the shared record's `updatedAt` is at least * as new as the legacy one (which it is, immediately after the first * fold), the same legacy file folds in nothing on every later boot. Only * `subscriptions` are folded, `pending` OAuth logins carry a verifier tied * to one in-flight browser round trip and are meaningless to resume across * processes or after a restart. * * Never writes to or deletes the legacy file. Writes the shared store, and * logs one info line naming what was adopted, only when something actually * changed. */ private foldLegacyStore; /** * Read the subscription store, never throwing. * * Corrupt content is quarantined (moved aside with a `.why` receipt and * logged at error level) instead of being silently discarded the way the * bare `catch` here used to discard it, the caller still gets the same * empty store, but the evidence of what went wrong survives on disk. A read * failure that leaves nothing to quarantine (permissions) also degrades to * the empty store, preserving this method's never-throws contract. */ private read; private write; list(): ProviderSubscription[]; listPending(): PendingSubscriptionLogin[]; get(provider: string): ProviderSubscription | null; getAccessToken(provider: string): string | null; resolveAccessToken(provider: string, config: OAuthProviderConfig): Promise; beginOAuthLogin(provider: string, config: OAuthProviderConfig): Promise<{ authorizationUrl: string; pending: PendingSubscriptionLogin; }>; completeOAuthLogin(provider: string, config: OAuthProviderConfig, code: string): Promise; refreshOAuthToken(provider: string, config: OAuthProviderConfig): Promise; logout(provider: string): boolean; getPending(provider: string): PendingSubscriptionLogin | null; savePending(pending: PendingSubscriptionLogin): void; clearPending(provider: string): void; saveSubscription(subscription: ProviderSubscription): ProviderSubscription; } //# sourceMappingURL=subscriptions.d.ts.map