/** * NotebookLM Quota Manager * * Manages license tier detection, usage tracking, and limit enforcement. */ import type { Page } from "patchright"; export type LicenseTier = "free" | "pro" | "ultra" | "unknown"; export interface QuotaLimits { notebooks: number; sourcesPerNotebook: number; wordsPerSource: number; queriesPerDay: number; } export interface QuotaUsage { notebooks: number; queriesUsedToday: number; lastQueryDate: string; lastUpdated: string; } export interface QuotaSettings { tier: LicenseTier; limits: QuotaLimits; usage: QuotaUsage; autoDetected: boolean; } export declare class QuotaManager { private settings; private settingsPath; constructor(); /** * Load settings from disk or create defaults */ private loadSettings; /** * Validate and sanitise an untrusted (user-writable) settings object. * * The quota.json file is user-writable, so a tampered or stale file must not * be able to disable enforcement (e.g. by setting limits to 1e12/0/strings or * omitting usage fields, which would otherwise yield NaN comparisons). Tier is * constrained to a known key (falls back to "unknown"); limits are ALWAYS * derived from TIER_LIMITS[tier] and never trusted from disk; usage fields are * coerced to finite numbers and defaulted when missing. */ private validateSettings; /** * Get default settings */ private getDefaultSettings; /** * Save settings to disk */ private saveSettings; /** * Effective (rolled-over) query count for TODAY, computed WITHOUT mutating or * persisting settings. If the stored lastQueryDate is not today, the count is * treated as 0 (the day has rolled over) but no write occurs — persisting the * rollover is the exclusive responsibility of incrementQueryCountAtomic / * checkAndReserveQuery (see I285). Shared by all readers (getStatus, * getDetailedStatus, updateQuotaMetrics, canMakeQuery) so they never report a * stale pre-rollover count. */ private effectiveQueriesUsedToday; /** Percentage guard: avoid NaN/Infinity when the limit is zero or invalid. */ private static safePercent; private evaluateWithTimeout; /** * Detect license tier from NotebookLM UI * Tiers: free, pro, ultra (Google AI Ultra $249.99/month) */ detectTierFromPage(page: Page): Promise; /** * Extract source limit from source dialog (e.g., "0/300") */ extractSourceLimitFromDialog(page: Page): Promise; /** * Extract query usage from NotebookLM UI * * Looks for patterns like: * - "X/50 queries" or "X of 50 queries" * - "X queries remaining" * - Usage indicators in settings/account area * * Returns { used, limit } or null if not found */ extractQueryUsageFromUI(page: Page): Promise<{ used: number; limit: number; } | null>; /** * Check for rate limit error message on page */ checkForRateLimitError(page: Page): Promise; /** * Count notebooks from homepage */ countNotebooksFromPage(page: Page): Promise; /** * Update quota from UI scraping */ updateFromUI(page: Page): Promise<{ tier: LicenseTier; queryUsageFromGoogle: { used: number; limit: number; } | null; rateLimitDetected: boolean; }>; /** * Manually set tier (for user override). * * Records a ChangeLog entry for SOC2 change-management audit trail. */ setTier(tier: LicenseTier): Promise; /** * Get current settings */ getSettings(): QuotaSettings; /** * Get current limits */ getLimits(): QuotaLimits; /** * Get current usage */ getUsage(): QuotaUsage; /** * Increment notebook count * * Uses the same withLock(settingsPath) transaction as * incrementQueryCountAtomic (reload-under-lock → increment → persist) so the * notebook counter shares ONE concurrency mechanism with the query counters * (no separate ad-hoc promise queue). The lock provides mutual exclusion and * the reload-under-lock prevents lost updates across concurrent callers; * strict FIFO ordering is not required for a counter. * * Fail-soft contract preserved: this method logs and resolves on error rather * than rejecting, since external callers may not catch. */ incrementNotebookCount(): Promise; /** * Increment query count (synchronous, for backwards compatibility) * Note: For concurrent safety, use incrementQueryCountAtomic() instead */ incrementQueryCount(): void; /** * Increment query count atomically with file locking * * This method is safe for concurrent access from multiple processes/sessions. * It reloads settings from disk before incrementing to ensure accuracy. */ incrementQueryCountAtomic(): Promise; /** * Atomically check the daily quota and reserve (increment) a slot in a single * locked, disk-reloaded transaction. * * This closes the TOCTOU window where concurrent sessions/processes all pass a * stale in-memory check (canMakeQuery) and then increment only after the slow * browser query completes, collectively exceeding the daily limit. The slot is * reserved up front, BEFORE the query runs. * * Returns { allowed, reason? }. Callers MUST run the query only when allowed, * and call releaseReservation() if the query subsequently fails. */ checkAndReserveQuery(): Promise<{ allowed: boolean; reason?: string; }>; /** * Release a previously reserved query slot (e.g. when the query failed after * reservation in checkAndReserveQuery). Atomic and floored at zero so it can * never underflow. */ releaseReservation(): Promise; /** * Persist current settings to disk while a file lock is already held. * Mirrors the write performed inside incrementQueryCountAtomic. */ private persistWithLockHeld; /** * Refresh settings from disk with file locking * * Use this to ensure you have the latest quota state from disk. */ refreshSettings(): Promise; /** * Check if can create notebook */ canCreateNotebook(): { allowed: boolean; reason?: string; }; /** * Check if can add source to notebook */ canAddSource(currentSourceCount: number): { allowed: boolean; reason?: string; }; /** * Check if can make query */ canMakeQuery(): { allowed: boolean; reason?: string; }; private updateQuotaMetrics; /** * Get quota status summary */ getStatus(): { tier: LicenseTier; notebooks: { used: number; limit: number; percent: number; }; sources: { limit: number; }; queries: { used: number; limit: number; percent: number; }; }; /** * Get detailed quota status with remaining counts, warnings, and stop signals * Used to provide visibility to users about when to stop querying for the day */ getDetailedStatus(): { tier: LicenseTier; queries: { used: number; limit: number; remaining: number; percentUsed: number; shouldStop: boolean; resetTime: string; }; notebooks: { used: number; limit: number; remaining: number; percentUsed: number; }; sources: { limit: number; }; warnings: string[]; }; } export declare function getQuotaManager(): QuotaManager;