/**
 * Authentication service
 *
 * SQLite-backed user store with bcrypt password hashing and JWT sessions.
 * Database lives at .studiograph/auth.db inside the workspace.
 *
 * Auth state is portable via .studiograph/auth-seed.json — a JSON export of
 * all users (with bcrypt hashes) and the JWT signing secret. The seed file
 * is committed to the config repo so it syncs between local and Railway:
 *   local setup → commit → push → Railway redeploy → users restored from seed
 *
 * ─────────────────────────────────────────────────────────────────────────
 * SECURITY — config repo is a secret store
 * ─────────────────────────────────────────────────────────────────────────
 * `auth-seed.json` carries:
 *   • the JWT signing secret (anyone holding this can mint sessions for any
 *     user in the workspace, bypassing password auth entirely)
 *   • bcrypt password hashes for every user (offline-crackable; bcrypt cost
 *     12 buys time but not invulnerability against weak passwords)
 *
 * Treat the .studiograph/ config repo with the same operational care as any
 * other secret store: private remote, restricted contributors, never made
 * public. The audit (2026-05) flagged this surface specifically — `getSeedData`
 * returns these fields by design (they're how the seed-restore flow works on
 * Railway redeploys) but every caller needs to understand they are exporting
 * raw secrets.
 *
 * To override the file-based JWT secret in production (e.g. Railway), set
 * the STUDIOGRAPH_JWT_SECRET environment variable. The env var wins over the
 * `auth-secret` file and the seed file: a leaked seed file alone won't let
 * an attacker mint tokens for an environment with a different in-process
 * secret. Rotation: change the env var and restart — all extant tokens
 * invalidate together.
 */
import { type OAuthStateClaims } from '../mcp/state-signer.js';
import { OAuthServerStore } from './oauth-server-store.js';
export type UserRole = 'owner' | 'member';
export type FolderRole = 'admin' | 'editor';
export interface AuthUser {
    id: number;
    email: string;
    /**
     * Stable, human-readable identifier used for `created_by` / `updated_by`
     * audit fields in entity frontmatter and as the git author identity on
     * workspace commits. Email-free so the workspace git history doesn't carry
     * PII. Generated at user creation, immutable thereafter.
     */
    handle: string;
    displayName: string;
    role: UserRole;
    createdAt: string;
}
/** Check if a user has workspace-owner privileges. */
export declare function isWorkspaceOwner(role: string): boolean;
export interface UserView {
    id: number;
    name: string;
    viewType: string;
    config: Record<string, any>;
    /** Structured scope key: 'recent' | 'type:<t>' | 'folder:<id>'. */
    scope: string;
    sortOrder: number;
    visibility: 'private' | 'workspace';
    createdByName: string | null;
    createdAt: string;
    updatedAt: string;
}
export interface AuthSeed {
    jwtSecret: string;
    /** @deprecated removed in favor of per-user tokens (api_tokens table). Retained so pre-existing seeds don't fail to parse. */
    apiKey?: string;
    users: Array<{
        email: string;
        handle: string;
        password_hash: string;
        display_name: string;
        role: string;
        created_at: string;
    }>;
    folder_access?: Array<{
        user_email: string;
        folder_name: string;
        role?: FolderRole;
    }>;
}
/**
 * Optional encryption hookup for per-user secrets stored in auth.db.
 *
 * When `masterKey` is provided, AuthService treats `oauth_tokens.data_ciphertext`
 * and `user_preferences.settings_ciphertext` as the source of truth for
 * encrypted user-secrets, falling back to legacy plaintext columns only on
 * pre-migration databases. Without a master key (tests, flag-off boots),
 * AuthService stays on the legacy plaintext path — but it refuses to run
 * against a database that has already been migrated, since reading
 * post-migration rows without the key would silently return empty data.
 */
export interface AuthServiceCryptoOptions {
    masterKey: Uint8Array;
    /** Defaults to 'mk_v1'. Bumped by master-key rotation. */
    keyVersion?: string;
}
export declare class AuthService {
    private db;
    private jwtSecret;
    private sgDir;
    private crypto?;
    /**
     * True once we've confirmed `data_ciphertext` exists on `oauth_tokens`
     * (post-migration shape). The encrypted code path keys off this rather
     * than re-running PRAGMA on every call.
     */
    private oauthTokensEncrypted;
    /** True once `settings_ciphertext` is available on `user_preferences`. */
    private userPrefsEncrypted;
    /**
     * Listeners for OAuth-token mutations. Fires after `saveOAuthData` or
     * `clearOAuthData` lands a row. Used by the agent orchestrator to evict
     * the user's pooled agent so the next chat builds a fresh tool list
     * that reflects the new per-user connector state (otherwise the agent
     * keeps the toolset it had at first session creation, missing any
     * connector the user just OAuthed). Mirrors the `onSecretMutation`
     * pattern in `src/core/secrets/index.ts`.
     *
     * Listener errors are swallowed — never let a stray subscriber take
     * down the OAuth write path.
     */
    private oauthMutationListeners;
    /**
     * OAuth state signer. Initialized lazily from the master key (HKDF-derived,
     * domain-separated from the AEAD key). When `cryptoOpts` is missing — e.g.
     * tests that don't exercise the OAuth init/callback paths — sign/verify
     * throw on use rather than silently falling back to unsigned state.
     */
    private oauthStateSigner;
    /** OAuth 2.0 authorization-server store. Initialized at the end of the constructor. */
    private oauthServerStore;
    constructor(workspacePath: string, cryptoOpts?: AuthServiceCryptoOptions);
    /** The OAuth authorization-server store (clients/codes/refresh + minting). */
    get oauthServer(): OAuthServerStore;
    createUser(email: string, password: string, displayName: string, role?: UserRole): AuthUser;
    upsertCloudManagedUser(input: {
        email: string;
        displayName: string;
        role: UserRole;
    }): AuthUser;
    /**
     * Pick a unique handle by slugifying the display name (or email local-part
     * fallback) and appending `-2`, `-3`, … on collision. Handles are immutable
     * once assigned, so we don't have to worry about two parallel inserts
     * racing — SQLite's UNIQUE constraint will reject the loser and the caller
     * can retry.
     */
    private allocateHandle;
    authenticate(email: string, password: string): {
        user: AuthUser;
        token: string;
    } | null;
    createSessionToken(user: AuthUser): string;
    /** Expose the resolved JWT secret to sibling services that mint their own
     *  signed tokens against the same key (e.g. `asset-upload-token` for the
     *  MCP presign flow). Reusing one secret keeps "rotate the JWT" a single
     *  operational lever instead of N parallel ones. The secret never leaves
     *  the server process. */
    getJwtSecret(): string;
    /**
     * Resolve a session JWT to its user. **Rejects OAuth access tokens**
     * (`oauth:true`) — those are authorization-server-issued and confined to the
     * MCP surface, so they must never satisfy a session check. This is the
     * choke point: every hook-bypassing surface that authenticates a JWT
     * (`requireUser` for `/api/auth/*`, Git Basic-auth, the WebSocket route, the
     * cookie branch of the global hook) calls this, so rejecting OAuth tokens
     * here closes all of them at once. The single place that intentionally
     * accepts an OAuth token — the global hook's Bearer→`/mcp` branch — uses
     * `verifyTokenWithClaims` directly and enforces audience scoping there.
     */
    verifyToken(token: string): AuthUser | null;
    /**
     * Verify a session/OAuth JWT in a single decode and surface the OAuth-specific
     * claims the auth hook needs: `oauth` marks an authorization-server-issued
     * token (which the hook scopes to `/mcp` + `/oauth/*`), `cid` is the issuing
     * client_id (used as the integration label), and `aud` is the bound resource.
     * Returns null for any invalid/expired token or unknown user.
     */
    verifyTokenWithClaims(token: string): {
        user: AuthUser;
        oauth: boolean;
        cid: string | null;
        aud: string | null;
    } | null;
    findUserById(userId: number): AuthUser | null;
    /** Look up a user by their stable handle (e.g. `christian-marc-schmidt`). */
    findUserByHandle(handle: string): AuthUser | null;
    /** Look up a user by email (case-insensitive). */
    findUserByEmail(email: string): AuthUser | null;
    listUsers(): AuthUser[];
    deleteUser(email: string): boolean;
    /**
     * Before deleting `targetUserId`, transfer folder-admin rights to
     * `actorUserId` for every folder where the target is the sole admin.
     * Inserts an admin folder_access row for the actor (or upgrades an
     * existing editor row). Returns the list of folder names that were
     * transferred so the caller can surface them in UX.
     *
     * This prevents folders from becoming orphaned (no admins) after the
     * target user's folder_access rows cascade-delete. Folders with other
     * admins are left alone — no redundant membership added.
     */
    transferFoldersOnDelete(targetUserId: number, actorUserId: number): string[];
    updateProfile(userId: number, fields: {
        displayName?: string;
        email?: string;
    }): AuthUser | null;
    updatePassword(email: string, newPassword: string): boolean;
    /**
     * Get the workspace owner (the single user with role 'owner').
     * Returns null if no owner exists (e.g., legacy workspace not yet migrated).
     */
    getOwner(): AuthUser | null;
    /**
     * Transfer ownership to another user. The current owner becomes a regular member.
     * Both changes happen in a single transaction.
     */
    transferOwnership(currentOwnerId: number, newOwnerId: number): void;
    /** Grant folder membership. `role` defaults to 'editor'. Upserts the role if the row exists. */
    grantFolderAccess(userId: number, folder: string, role?: FolderRole): void;
    revokeFolderAccess(userId: number, folder: string): void;
    /** Folder names the user is a member of. */
    getUserFolders(userId: number): string[];
    /** All users with any role on a folder. */
    getFolderUsers(folder: string): AuthUser[];
    /** (user, role) pairs for every member of a folder. */
    getFolderMembers(folder: string): Array<{
        user: AuthUser;
        role: FolderRole;
    }>;
    /** Folder role for a specific user, or null if they aren't a member. */
    getFolderRole(userId: number, folder: string): FolderRole | null;
    /** True if the user is a folder admin on that folder. */
    isFolderAdmin(userId: number, folder: string): boolean;
    /** All users with folder-admin role on a folder. */
    getFolderAdmins(folder: string): AuthUser[];
    /**
     * Transfer folder admin role from one user to another. Used during member
     * removal to pass orphaned folders to the workspace owner.
     */
    transferFolderAdmin(folder: string, fromUserId: number, toUserId: number): void;
    /** Rename the folder slug across all membership rows. */
    renameFolder(oldName: string, newName: string): void;
    /** Get stored OAuth data (tokens, clientInfo, codeVerifier) for a user+connector. */
    getOAuthData(userId: number, connectorName: string): Record<string, any> | null;
    /**
     * Sign an OAuth state parameter for outbound flows. Throws when the service
     * was constructed without crypto options (the signing key is HKDF-derived
     * from the master key). Callers in OAuth init paths must ensure the
     * AuthService is constructed with crypto options — production always is;
     * tests that exercise OAuth flows should provide a master key.
     */
    signOAuthState(claims: OAuthStateClaims): string;
    /**
     * Verify a signed OAuth state. Returns null on:
     *   - missing/invalid signing key (defensive — should not happen in
     *     production where serve.ts wires the master key)
     *   - tampered / forged / unsigned state
     *   - state older than 10 minutes (replay window)
     *   - state with future-skewed `t` beyond a small tolerance
     * Callers should treat null as "invalid/expired state" and surface a
     * generic error to the browser without echoing details.
     */
    verifyOAuthState(state: string): OAuthStateClaims | null;
    /**
     * Subscribe to OAuth-token mutations. Returns an unsubscribe fn.
     * Listeners fire AFTER the row write commits. Both connect and disconnect
     * paths emit; subscribers that only care about one should filter
     * themselves.
     */
    onOAuthMutation(fn: (userId: number, connectorName: string) => void): () => void;
    private notifyOAuthMutation;
    /**
     * Save OAuth data (upsert).
     *
     * `opts.silent` suppresses the OAuth-mutation event. Pass it for writes
     * that do NOT change the user's connected-connector set — chiefly a token
     * REFRESH and the SDK's pre-token machinery (PKCE verifier, DCR client
     * info). Those writes fire while ConnectorManager.buildTools constructs the
     * pooled agent (inside withAgent's `await toolsLoaded`); notifying there
     * makes the orchestrator evict the in-flight agent, after which
     * findSessionForAgent() returns null for the rest of the turn and every
     * session-scoped tool loses context (create_artifact then fails with
     * "No artifact canvas is available in this context"). An INITIAL token
     * grant (none → tokens) and a disconnect DO change the tool list and must
     * still notify so the agent rebuilds.
     */
    saveOAuthData(userId: number, connectorName: string, data: Record<string, any>, opts?: {
        silent?: boolean;
    }): void;
    /** List all connected connectors for a user. */
    getUserConnectedConnectors(userId: number): Array<{
        connector_name: string;
    }>;
    /** Clear OAuth data for a user+connector. */
    clearOAuthData(userId: number, connectorName: string): void;
    /** Check if a user has OAuth tokens for a connector. */
    hasOAuthTokens(userId: number, connectorName: string): boolean;
    hasUsers(): boolean;
    getUserCount(): number;
    /**
     * Mint a new per-user API token. Returns the raw token once — only its
     * sha256 hash is stored in the database. The raw value cannot be recovered
     * later, matching how GitHub personal access tokens work.
     */
    createApiToken(userId: number, name: string): {
        id: number;
        token: string;
        name: string;
    };
    /**
     * Resolve a raw API token to its owning user, updating `last_used_at`
     * on success. Returns null if the token is unknown or the owning user
     * has been deleted.
     */
    verifyApiToken(token: string): AuthUser | null;
    /**
     * Same as verifyApiToken but also returns the token's user-set name (e.g.
     * "Cursor", "Claude Desktop"). Used by surfaces that need to attribute an
     * action to a specific named integration — most importantly the
     * agent-activity feature, which renders "<owner>'s <token name>" chips.
     */
    verifyApiTokenWithMeta(token: string): {
        user: AuthUser;
        tokenName: string;
    } | null;
    /** List a user's API tokens. Never returns the raw token — only metadata. */
    listApiTokens(userId: number): Array<{
        id: number;
        name: string;
        created_at: string;
        last_used_at: string | null;
    }>;
    /** Revoke (delete) an API token owned by the given user. No-op if not found. */
    revokeApiToken(tokenId: number, userId: number): boolean;
    /**
     * Ensure auth.db and auth-secret are in .studiograph/.gitignore.
     * Only the seed JSON file should be committed to the config repo.
     */
    private ensureGitignore;
    /**
     * Build and return the current auth seed data (users + JWT secret + folder access).
     * Does not write to disk — use exportSeed() for that.
     *
     * SECURITY: the returned object embeds raw secrets:
     *   • `jwtSecret` — signing key for ALL session tokens. Anyone holding this
     *     can mint a valid session for any user. Rotate by setting
     *     STUDIOGRAPH_JWT_SECRET in production and restarting.
     *   • `users[].password_hash` — bcrypt hashes (cost 12). Offline-crackable
     *     against weak passwords; treat as compromise of the listed accounts
     *     if leaked.
     *
     * Only call this from paths that write to a trusted location (auth-seed.json
     * inside the .studiograph/ config repo). Never expose over HTTP, never log,
     * never include in error messages or telemetry.
     */
    getSeedData(): AuthSeed;
    /**
     * Export all users and JWT secret to .studiograph/auth-seed.json.
     * If .studiograph/ is a git repo with a remote, auto-commits and pushes
     * the seed file so it syncs between environments.
     */
    exportSeed(): void;
    /**
     * Commit and push auth-seed.json if .studiograph/ is a git repo with a remote.
     * Non-fatal — logs warnings on failure but never throws.
     */
    private pushSeed;
    /**
     * Import users and JWT secret from .studiograph/auth-seed.json.
     * Two modes:
     *   • `merge` (default) — INSERT OR IGNORE on email. Existing local rows win
     *     on conflict. Safe for boot-time recovery when the local DB may carry
     *     legitimate state the seed shouldn't clobber.
     *   • `overwrite` — UPSERT on email. Seed values win on conflict. Correct
     *     for explicit "remote is authoritative" callers (`studiograph pull`,
     *     `studiograph clone`, admin bundle import) where any local divergence
     *     was almost certainly the post-clone setup-wizard stamping fresh
     *     credentials over the real ones. `merge` mode silently shadowed
     *     production credentials in that exact scenario — see 2026-05 incident.
     */
    applySeed(options?: {
        mode?: 'merge' | 'overwrite';
    }): void;
    getUserViews(userId: number): UserView[];
    createUserView(userId: number, name: string, viewType?: string, config?: Record<string, any>, createdByName?: string, scope?: string): UserView;
    setViewVisibility(viewId: number, userId: number, visibility: 'private' | 'workspace'): void;
    getUserView(viewId: number, userId: number): UserView | null;
    updateUserView(viewId: number, userId: number, fields: {
        name?: string;
        config?: Record<string, any>;
        scope?: string;
        viewType?: string;
        sortOrder?: number;
    }): void;
    deleteUserView(viewId: number, userId: number): void;
    /**
     * Persist a user's pinned-view order atomically: `sort_order` becomes the
     * row's index in `orderedIds`. Scoped to `user_id` so a caller can only
     * reorder their own views (a foreign id in the list is a no-op). Runs in one
     * transaction so a drag-reorder never leaves a half-applied (scrambled) order.
     */
    reorderUserViews(userId: number, orderedIds: number[]): void;
    private toUserView;
    getAccentColor(userId: number): string | null;
    /** Get accent colors for all users. Returns userId → color (only users with a color set). */
    getAllAccentColors(): Record<number, string>;
    setAccentColor(userId: number, color: string | null): void;
    getOnboarded(userId: number): boolean;
    setOnboarded(userId: number, value: boolean): void;
    /** Whether the user deferred ("skip for now") onboarding — distinct from
     *  completing it. Silences the auto-greet + interview pill until they return. */
    getOnboardingDeferred(userId: number): boolean;
    setOnboardingDeferred(userId: number, value: boolean): void;
    /**
     * Reset per-user state used by the agent: onboarding flag and meeting-capture
     * settings (Deepgram key, calendar, default folder). Preserves the user
     * account, accent color, and minted API tokens. Used by `POST /api/reset` to
     * put a tester back to a clean slate without a redeploy or DB surgery.
     */
    resetUserState(userId: number): void;
    getUserSettings(userId: number): Record<string, any>;
    setUserSettings(userId: number, settings: Record<string, any>): void;
    getUserSetting(userId: number, key: string): any;
    setUserSetting(userId: number, key: string, value: any): void;
    close(): void;
    private toAuthUser;
}
