import type { StorageClient } from "../storage/client.js"; import { type BillingTier } from "../billing/tierStore.js"; export declare const PAGE_LIMIT = 100; export declare const STORAGE_LIMIT_BYTES: number; /** * Per-page size cap. 100 pages x 100 KB = the 10 MB total we advertise, so * bounding each page locally gives the same guarantee the aggregate byte cap * used to — without needing a total-bytes figure, which could only be had by * listing every object in storage (GH #413). * * Sized against real usage: across all non-system pages in production, no * account outside god mode has a page over 21 KB (p99 ~19 KB). 100 KB is ~5x * the largest page any real user has written. */ export declare const PAGE_SIZE_LIMIT_BYTES: number; /** * Inline read cap for read_page. Independent of PAGE_SIZE_LIMIT_BYTES: that * cap gates writes and only applies to subjects with a quota, while this one * gates the body read_page hands back over MCP, for every page regardless of * who wrote it. Pages written before the write cap existed (or by a god-mode * subject, which is exempt from it) can still be larger than * PAGE_SIZE_LIMIT_BYTES, and read_page must not try to serialize an unbounded * body back to the client — that's what dropped the MCP connection outright * on a ~110 KB page instead of erroring (GH #701). * * Set equal to PAGE_SIZE_LIMIT_BYTES so a page any current subject could * legally write is always readable in full. */ export declare const READ_INLINE_CAP_BYTES: number; /** * Truncates UTF-8 text to at most maxBytes, never splitting a multi-byte * character. Backs off from the byte cap one byte at a time past any * continuation byte (0b10xxxxxx) so the returned string is always valid * UTF-8 — content this truncates is arbitrary wiki prose, not * ASCII-guaranteed. */ export declare function truncateUtf8(text: string, maxBytes: number): string; export declare function isLimitEnforcementEnabled(): boolean; /** * Page allowance per billing tier, matching what the pricing page sells * (`apps/web/src/components/Pricing.tsx`). `null` means uncapped. * * `free` is the post-beta allowance: 50. During beta the free allowance is 100, * which is what `free_beta` carries — see BETA_FREE_PAGE_LIMIT below. Users who * signed up during beta keep 100 permanently by holding a `free_beta` row, so * ending beta changes what new signups get and nothing else. * * `launch_user` is the legacy name `src/admin/seedUsers.ts` writes and is * treated as a beta-era account. */ export declare const TIER_PAGE_LIMITS: Record; /** Free allowance while the beta is running. */ export declare const BETA_FREE_PAGE_LIMIT = 100; /** Free allowance once the beta has ended. */ export declare const POST_BETA_FREE_PAGE_LIMIT = 50; /** * Is the beta still running? * * Defaults to **true**: ending the beta must be a deliberate act, because it * halves the free allowance. A missing or malformed env var therefore leaves * users with the more generous limit rather than silently tightening it. */ export declare function isBetaActive(): boolean; /** * Returns the free-tier page limit for a subject with no assigned tier. * Returns null for god-mode subjects (unlimited). * * During beta this is 100; afterwards 50. Overridable with * BRAINS_BETA_PAGE_LIMIT / BRAINS_FREE_PAGE_LIMIT respectively. * * **This no longer keys off how many pages the subject has written.** The * previous implementation used `existingPageCount > 0` as a proxy for "is a * beta user", which got it backwards: a genuine new signup during the beta has * zero pages and was given 50 — the post-beta allowance — and then jumped to * 100 the moment they saved their first page. The cap moved as a side effect of * using the product, and the people it under-served were exactly the ones the * beta was meant to be generous to (GH #473). * * `existingPageCount` is retained in the signature because callers already * compute it and dropping it would churn them for nothing. * * This is the fallback used when a subject has no assigned tier. Callers that * can await should prefer resolveTieredPageLimit(), which honours the tier. */ export declare function getPageLimit(subject: string, _existingPageCount: number): number | null; /** * Returns the per-page byte cap for a subject, or null when uncapped * (god mode). Overridable via BRAINS_PAGE_SIZE_LIMIT_BYTES. */ export declare function getPageSizeLimit(subject: string): number | null; export declare class PageSizeError extends Error { readonly code: "PAGE_TOO_LARGE"; readonly size: number; readonly limit: number; constructor(name: string, size: number, limit: number); toJSON(): { ok: boolean; error: "PAGE_TOO_LARGE"; message: string; size: number; limit: number; }; } /** * Enforces the per-page size cap. Synchronous — no storage or DB call — so it * adds nothing to write latency. No-op for god-mode subjects and for * unattributed writes (api-key / connector-key modes, which have no quota). */ export declare function assertPageSize(subject: string, name: string, content: string): void; /** * Names the cheapest tier that would actually raise the caller's current limit. * * The old message hard-coded "Upgrade to Solo for 2,000 pages" for everyone, * which told a Solo customer sitting at 2,000 pages to upgrade to the plan they * were already on — and, before tiers were honoured at all, pointed everyone at * an upgrade that raised nothing (GH #473). */ export declare function upgradeHint(limit: number): string; export declare class PageLimitError extends Error { readonly code: "LIMIT_EXCEEDED"; readonly current: number; readonly limit: number; constructor(current: number, limit: number); toJSON(): { ok: boolean; error: "LIMIT_EXCEEDED"; message: string; current: number; limit: number; }; } /** * Resolves a subject's page limit, honouring their assigned billing tier. * * Order matters. God mode wins outright, so an admin never has a lookup stand * between them and a write. Otherwise an assigned tier decides, and only when * there is no tier row do we fall back to the env-var defaults — which keeps * every existing account on exactly the limit it had before tiers existed. * * A failed lookup is indistinguishable from "no tier assigned", deliberately: * see the note in tierStore.ts on why a database that has not had the migration * applied must degrade to the old behaviour rather than refuse writes. */ export declare function resolveTieredPageLimit(subject: string, existingPageCount: number): Promise; /** * Checks the tier page limit for a subject before creating newPagesCount pages. * No-op when subject is empty, Postgres is not configured, or the subject is in god mode. * Throws PageLimitError when current + newPagesCount would exceed the limit. */ export declare function assertPageLimit(subject: string, newPagesCount: number): Promise; /** * Resolves the page limit context for a subject in a single Postgres query. * Returns null limit for god-mode subjects or when Postgres is not configured. * Use this when you need to check the limit multiple times (e.g. per create in a bundle), * instead of calling assertPageLimit() per page which would query Postgres repeatedly. */ export declare function resolvePageLimitContext(subject: string): Promise<{ currentCount: number; limit: number | null; }>; export declare class StorageLimitError extends Error { readonly code: "STORAGE_LIMIT_EXCEEDED"; readonly limit: { type: "pages" | "storage_bytes"; current: number; max: number; }; constructor(type: "pages" | "storage_bytes", current: number, max: number); toJSON(): { error: "STORAGE_LIMIT_EXCEEDED"; message: string; limit: { type: "pages" | "storage_bytes"; current: number; max: number; }; }; } /** * Checks page count and total storage bytes against beta limits. * No-op unless BRAINS_ENFORCE_LIMITS=true. * Throws StorageLimitError when a limit is exceeded. */ export declare function assertWithinLimits(drive: StorageClient): Promise; //# sourceMappingURL=storageLimits.d.ts.map