From 06422fe09685520b2440f5d166ca37b33f60e146 Mon Sep 17 00:00:00 2001 From: Ben Davis <45952064+bmdavis419@users.noreply.github.com> Date: Wed, 9 Sep 2026 07:28:07 -0700 Subject: [PATCH 1/7] FEATURE: Define plans and features as Autumn config autumn.config.ts declares the storage_bytes, ai_ops, and public_sharing features and the free and pro plans; plans.ts mirrors the limits and a unit test imports the config to keep them in step. AutumnClient wraps autumn-js behind a service with a real client when AUTUMN_SECRET_KEY is set, a fail-open null client that logs once otherwise, and an in-memory fake for tests. First sign-in creates the Autumn customer for the org. Co-Authored-By: Claude Fable 5.1 --- apps/web/.dev.vars.example | 7 + apps/web/autumn.config.ts | 56 +++ apps/web/package.json | 3 + apps/web/src/env.d.ts | 4 + apps/web/src/lib/server/config.ts | 14 + apps/web/src/lib/server/edge.ts | 4 +- apps/web/src/lib/server/layer.ts | 5 +- apps/web/src/lib/server/plans.ts | 13 +- apps/web/src/lib/server/services/auth.ts | 24 ++ apps/web/src/lib/server/services/autumn.ts | 212 +++++++++++ apps/web/worker-configuration.d.ts | 18 +- bun.lock | 386 ++++++++++++++++++++- 12 files changed, 735 insertions(+), 11 deletions(-) create mode 100644 apps/web/autumn.config.ts create mode 100644 apps/web/src/lib/server/services/autumn.ts diff --git a/apps/web/.dev.vars.example b/apps/web/.dev.vars.example index eaeffd2..d2c8711 100644 --- a/apps/web/.dev.vars.example +++ b/apps/web/.dev.vars.example @@ -15,6 +15,13 @@ WORKOS_CLIENT_ID="client_replace" WORKOS_COOKIE_PASSWORD="replace-with-at-least-32-characters-of-random-text" WORKOS_WEBHOOK_SECRET="replace" +# Autumn billing. Leave AUTUMN_SECRET_KEY empty to run without billing: +# every plan gate allows and nothing is metered. `fake:` selects the +# in-memory fake used by tests. The webhook secret is the Svix signing +# secret of the /api/webhooks/autumn endpoint. +AUTUMN_SECRET_KEY="" +AUTUMN_WEBHOOK_SECRET="" + # Local Postgres. `docker compose up -d` at the repo root starts one with # these credentials. Wrangler points the HYPERDRIVE binding at it. CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://adrive:adrive@127.0.0.1:5432/adrive" diff --git a/apps/web/autumn.config.ts b/apps/web/autumn.config.ts new file mode 100644 index 0000000..4cbfba8 --- /dev/null +++ b/apps/web/autumn.config.ts @@ -0,0 +1,56 @@ +import { feature, item, plan } from 'atmn'; + +// Plans and features as Autumn config. `bun run billing:push` deploys it; +// src/lib/server/plans.ts mirrors the limits for the local hard stops and +// plans.test.ts keeps the two in step. The customer id is the org id. + +const GIB = 1024 ** 3; + +export const storageBytes = feature({ + id: 'storage_bytes', + name: 'Storage', + type: 'metered', + consumable: false +}); + +export const aiOps = feature({ + id: 'ai_ops', + name: 'AI operations', + type: 'metered', + consumable: true +}); + +export const publicSharing = feature({ + id: 'public_sharing', + name: 'Public sharing', + type: 'boolean' +}); + +export const free = plan({ + id: 'free', + name: 'Free', + autoEnable: true, + items: [ + item({ featureId: storageBytes.id, included: 2 * GIB }), + item({ + featureId: aiOps.id, + included: 500, + reset: { interval: 'month' } + }) + ] +}); + +export const pro = plan({ + id: 'pro', + name: 'Pro', + price: { amount: 8, interval: 'month' }, + items: [ + item({ featureId: storageBytes.id, included: 100 * GIB }), + item({ + featureId: aiOps.id, + included: 10_000, + reset: { interval: 'month' } + }), + item({ featureId: publicSharing.id }) + ] +}); diff --git a/apps/web/package.json b/apps/web/package.json index b31ab5b..0cca41e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "check": "svelte-kit sync && bun run check:types && bun run check:effect && bun run check:svelte", + "billing:push": "atmn push", "check:types": "rm -rf .svelte-kit/output .svelte-kit/cloudflare .svelte-kit/cloudflare-tmp && tsc --noEmit", "check:effect": "effect-tsgo diagnostics --project tsconfig.json --format pretty", "check:svelte": "svelte-check --tsconfig ./tsconfig.json --tsgo", @@ -19,6 +20,7 @@ "@effect/sql-pg": "4.0.0-beta.102", "@modelcontextprotocol/server": "2.0.0", "agents": "^0.20.1", + "autumn-js": "1.3.0", "effect": "4.0.0-beta.102", "markdown-it": "^14.3.0", "mime": "^4.1.0", @@ -39,6 +41,7 @@ "@types/pg": "^8.23.1", "@typescript/native": "npm:typescript@^7.0.2", "@workos-inc/node": "10.13.0", + "atmn": "1.1.24", "concurrently": "^10.0.4", "svelte": "5.56.8", "svelte-check": "4.7.4", diff --git a/apps/web/src/env.d.ts b/apps/web/src/env.d.ts index d78c29d..2f6a264 100644 --- a/apps/web/src/env.d.ts +++ b/apps/web/src/env.d.ts @@ -16,6 +16,8 @@ declare global { CF_ACCOUNT_ID?: string; CF_API_TOKEN?: string; CF_ZONE_ID?: string; + AUTUMN_SECRET_KEY?: string; + AUTUMN_WEBHOOK_SECRET?: string; } namespace Cloudflare { @@ -31,6 +33,8 @@ declare global { CF_ACCOUNT_ID?: string; CF_API_TOKEN?: string; CF_ZONE_ID?: string; + AUTUMN_SECRET_KEY?: string; + AUTUMN_WEBHOOK_SECRET?: string; } } } diff --git a/apps/web/src/lib/server/config.ts b/apps/web/src/lib/server/config.ts index 47968ae..ef61737 100644 --- a/apps/web/src/lib/server/config.ts +++ b/apps/web/src/lib/server/config.ts @@ -26,6 +26,14 @@ export interface CloudflareZoneConfig { readonly zoneId: string; } +// Autumn billing. secretKey is null when AUTUMN_SECRET_KEY is unset, in +// which case every gate fails open; a `fake:` key picks the in-memory +// fake. See services/autumn.ts. +export interface AutumnConfig { + readonly secretKey: string | null; + readonly webhookSecret: string; +} + export interface AppConfigShape { readonly dashboardOrigin: string; // Tenant content is served from `.` over the @@ -41,6 +49,7 @@ export interface AppConfigShape { readonly cloudflareZone: CloudflareZoneConfig | null; // WorkOS user ids allowed on /admin (ADMIN_USER_IDS, comma-separated). readonly adminUserIds: ReadonlySet; + readonly autumn: AutumnConfig; readonly semanticSearch: 'off' | 'auto' | 'required'; readonly embeddingModel: '@cf/baai/bge-small-en-v1.5'; readonly embeddingPooling: 'cls'; @@ -123,6 +132,10 @@ const adminUserIdsFromEnv = (env: Env) => .map((id) => id.trim()) .filter((id) => id.length > 0) ); +const autumnFromEnv = (env: Env): AutumnConfig => ({ + secretKey: optionalString(env.AUTUMN_SECRET_KEY) || null, + webhookSecret: optionalString(env.AUTUMN_WEBHOOK_SECRET) +}); export const configFromEnv = (env: Env) => { const origins = normalizeOrigins({ @@ -160,6 +173,7 @@ export const configFromEnv = (env: Env) => { urlScanner: urlScannerFromEnv(env), cloudflareZone: cloudflareZoneFromEnv(env), adminUserIds: adminUserIdsFromEnv(env), + autumn: autumnFromEnv(env), semanticSearch, embeddingModel: '@cf/baai/bge-small-en-v1.5', embeddingPooling: 'cls', diff --git a/apps/web/src/lib/server/edge.ts b/apps/web/src/lib/server/edge.ts index 3d2d4d4..a4d8cee 100644 --- a/apps/web/src/lib/server/edge.ts +++ b/apps/web/src/lib/server/edge.ts @@ -20,6 +20,7 @@ import { requestLayer } from './layer'; import { PgSql } from './pg'; import type { AppConfig } from './config'; import type { Auth } from './services/auth'; +import type { AutumnClient } from './services/autumn'; import type { AuthGuardStore } from './services/bindings'; import type { Blobs } from './services/blobs'; import type { CurrentOrg, CurrentUser } from './services/current-org'; @@ -64,7 +65,8 @@ export type AppServices = | GrantSecrets | JobQueue | Org - | WorkOSClient; + | WorkOSClient + | AutumnClient; export const isAppError = (failure: unknown): failure is AppError => failure instanceof InvalidRequest || diff --git a/apps/web/src/lib/server/layer.ts b/apps/web/src/lib/server/layer.ts index 9c5b120..5d85cc2 100644 --- a/apps/web/src/lib/server/layer.ts +++ b/apps/web/src/lib/server/layer.ts @@ -4,6 +4,7 @@ import { OrgMissing, StorageError } from './errors'; import type { ProgramTenant } from './identity'; import { PgSql } from './pg'; import { AuthLive } from './services/auth'; +import { AutumnLive } from './services/autumn'; import { AuthGuardStore, Bucket, @@ -107,8 +108,9 @@ export const requestLayer = (env: Env, tenant: ProgramTenant | null) => { Layer.provide(infrastructure) ); const workos = WorkOSLive.pipe(Layer.provide(bindings)); + const autumn = AutumnLive.pipe(Layer.provide(bindings)); const auth = AuthLive.pipe( - Layer.provide(Layer.merge(infrastructure, workos)) + Layer.provide(Layer.mergeAll(infrastructure, workos, autumn)) ); const rateLimits = RateLimitsLive.pipe(Layer.provide(bindings)); const urlReputation = UrlReputationLive.pipe(Layer.provide(bindings)); @@ -140,6 +142,7 @@ export const requestLayer = (env: Env, tenant: ProgramTenant | null) => { infrastructure, semantic, workos, + autumn, auth, rateLimits, urlReputation, diff --git a/apps/web/src/lib/server/plans.ts b/apps/web/src/lib/server/plans.ts index c656977..65aa4fd 100644 --- a/apps/web/src/lib/server/plans.ts +++ b/apps/web/src/lib/server/plans.ts @@ -1,15 +1,18 @@ -// Per-plan limits. Stack F replaces the lookup with an Autumn check; until -// then the org's plan column picks a row here. +// Per-plan limits, mirrored from autumn.config.ts (plans.test.ts keeps them +// in step). The local org_usage counters are the hard stop; Autumn holds +// the same numbers so its balances and the billing page agree. const GIB = 1024 ** 3; export const PLAN_LIMITS = { - free: { storedBytes: 2 * GIB }, - pro: { storedBytes: 100 * GIB } + free: { storedBytes: 2 * GIB, aiOpsPerMonth: 500 }, + pro: { storedBytes: 100 * GIB, aiOpsPerMonth: 10_000 } } as const; export type Plan = keyof typeof PLAN_LIMITS; -const isPlan = (value: string): value is Plan => value in PLAN_LIMITS; +export const PRO_PLAN_ID: Plan = 'pro'; + +export const isPlan = (value: string): value is Plan => value in PLAN_LIMITS; // Unknown plans fall back to free so a typo in the column can only make // an org smaller, never unlimited. diff --git a/apps/web/src/lib/server/services/auth.ts b/apps/web/src/lib/server/services/auth.ts index a41e057..37436e9 100644 --- a/apps/web/src/lib/server/services/auth.ts +++ b/apps/web/src/lib/server/services/auth.ts @@ -18,6 +18,7 @@ import type { AuthContext, ResolvedCredential } from '../identity'; import { PgSql } from '../pg'; import { ensureTenant, personalOrgFor } from '../tenants'; import { promoteVerified } from '../trust'; +import { AutumnClient } from './autumn'; import { CurrentOrg, CurrentUser } from './current-org'; import { WorkOSClient } from './workos'; @@ -233,6 +234,7 @@ const suspendedOrg = () => const makeAuth = Effect.gen(function* () { const sql = yield* PgSql; const workos = yield* WorkOSClient; + const autumn = yield* AutumnClient; const org = yield* CurrentOrg; const user = yield* CurrentUser; @@ -478,6 +480,28 @@ const makeAuth = Effect.gen(function* () { ) ) ); + // The Autumn customer is the org; the free plan auto-attaches. + // A failure here is logged, not fatal: getOrCreate is retried + // on a later sign-in after the local transaction has committed. + yield* autumn + .ensureCustomer({ + customerId: orgId, + name: personalOrgFor(exchanged.user.email).name, + email: exchanged.user.email + }) + .pipe( + Effect.catchCause((cause) => + Effect.sync(() => { + console.error( + JSON.stringify({ + message: 'Autumn customer could not be created', + orgId, + cause: String(cause) + }) + ); + }) + ) + ); // Pin the org on the session so every later request carries it. const pinned = exchanged.organizationId === orgId diff --git a/apps/web/src/lib/server/services/autumn.ts b/apps/web/src/lib/server/services/autumn.ts new file mode 100644 index 0000000..169bc3a --- /dev/null +++ b/apps/web/src/lib/server/services/autumn.ts @@ -0,0 +1,212 @@ +import { Autumn } from 'autumn-js'; +import { Context, Effect, Layer } from 'effect'; +import { AppConfig, type AutumnConfig } from '../config'; +import { StorageError } from '../errors'; + +// The slice of Autumn the app depends on. The customer id is always the +// org id. The SDK is confined to autumnLive; without AUTUMN_SECRET_KEY the +// null client fails open (every check allows, every write is a no-op) so +// the app runs unmetered rather than not at all, and a `fake:` key picks +// the in-memory fake for tests. + +export type FeatureId = 'storage_bytes' | 'ai_ops' | 'public_sharing'; + +export interface FeatureCheck { + readonly allowed: boolean; + // Units left on the feature, or null when Autumn did not say (a boolean + // feature, an unknown balance, or a fail-open answer). + readonly remaining: number | null; +} + +export interface AutumnClientShape { + readonly enabled: boolean; + readonly ensureCustomer: (input: { + readonly customerId: string; + readonly name: string; + readonly email: string; + }) => Effect.Effect; + // Never fails: an unreachable Autumn answers `allowed` (its own + // fail-open default), so the local counters stay the hard stop. + readonly check: (input: { + readonly customerId: string; + readonly featureId: FeatureId; + readonly requiredBalance?: number; + }) => Effect.Effect; + readonly track: (input: { + readonly customerId: string; + readonly featureId: FeatureId; + readonly value: number; + }) => Effect.Effect; + readonly updateBalance: (input: { + readonly customerId: string; + readonly featureId: FeatureId; + readonly usage: number; + }) => Effect.Effect; + // The hosted checkout URL, or null when no payment step was needed and + // the plan is already attached. + readonly checkoutUrl: (input: { + readonly customerId: string; + readonly planId: string; + readonly successUrl: string; + }) => Effect.Effect; + readonly portalUrl: (input: { + readonly customerId: string; + readonly returnUrl: string; + }) => Effect.Effect; +} + +export class AutumnClient extends Context.Service< + AutumnClient, + AutumnClientShape +>()('app/AutumnClient') {} + +const failure = (operation: string) => (cause: unknown) => + new StorageError({ operation, cause }); + +const log = (entry: Record) => + Effect.sync(() => { + console.error(JSON.stringify(entry)); + }); + +// One SDK instance per isolate; it holds nothing but the key and a fetch. +let cachedSdk: { readonly key: string; readonly sdk: Autumn } | undefined; +const sdkFor = (secretKey: string) => { + if (cachedSdk?.key !== secretKey) { + cachedSdk = { key: secretKey, sdk: new Autumn({ secretKey }) }; + } + return cachedSdk.sdk; +}; + +const autumnLive = (secretKey: string): AutumnClientShape => { + const autumn = sdkFor(secretKey); + return { + enabled: true, + ensureCustomer: (input) => + Effect.tryPromise({ + try: () => autumn.customers.getOrCreate(input), + catch: failure('create Autumn customer') + }).pipe(Effect.asVoid), + check: (input) => + Effect.tryPromise(() => autumn.check(input)).pipe( + Effect.map((result): FeatureCheck => ({ + allowed: result.allowed, + remaining: result.balance?.remaining ?? null + })), + Effect.catchCause((cause) => + log({ + message: 'Autumn check failed open', + featureId: input.featureId, + customerId: input.customerId, + cause: String(cause) + }).pipe(Effect.as({ allowed: true, remaining: null })) + ) + ), + track: (input) => + Effect.tryPromise({ + try: () => autumn.track(input), + catch: failure('track Autumn usage') + }).pipe(Effect.asVoid), + updateBalance: (input) => + Effect.tryPromise({ + try: () => autumn.balances.update(input), + catch: failure('update Autumn balance') + }).pipe(Effect.asVoid), + checkoutUrl: (input) => + Effect.tryPromise({ + try: () => + autumn.billing.attach({ + customerId: input.customerId, + planId: input.planId, + redirectMode: 'if_required', + successUrl: input.successUrl + }), + catch: failure('start Autumn checkout') + }).pipe(Effect.map((result) => result.paymentUrl)), + portalUrl: (input) => + Effect.tryPromise({ + try: () => autumn.billing.openCustomerPortal(input), + catch: failure('open Autumn customer portal') + }).pipe(Effect.map((result) => result.url)) + }; +}; + +// Billing not configured: everything is allowed and nothing is recorded. +// Logged once per isolate so a production deploy without the key is +// visible without flooding the logs. +let warnedNull = false; +const warnOnce = Effect.sync(() => { + if (warnedNull) return; + warnedNull = true; + console.warn( + JSON.stringify({ + message: 'AUTUMN_SECRET_KEY is not set; billing gates fail open' + }) + ); +}); + +export const autumnNull: AutumnClientShape = { + enabled: false, + ensureCustomer: () => warnOnce, + check: () => warnOnce.pipe(Effect.as({ allowed: true, remaining: null })), + track: () => warnOnce, + updateBalance: () => warnOnce, + checkoutUrl: () => warnOnce.pipe(Effect.as(null)), + portalUrl: () => warnOnce.pipe(Effect.as(null)) +}; + +// Tests: every call is recorded, checkout and portal answer with stable +// URLs, and checks consult a per-feature table the test can set. +export const FAKE_AUTUMN_PREFIX = 'fake:'; + +export interface AutumnFakeCall { + readonly method: keyof Omit; + readonly input: Record; +} + +export const autumnFakeCalls: Array = []; +export const autumnFakeAllowed = new Map(); + +const record = ( + method: AutumnFakeCall['method'], + input: Record +) => + Effect.sync(() => { + autumnFakeCalls.push({ method, input }); + }); + +export const autumnFake: AutumnClientShape = { + enabled: true, + ensureCustomer: (input) => record('ensureCustomer', input), + check: (input) => + record('check', input).pipe( + Effect.as({ + allowed: autumnFakeAllowed.get(input.featureId) ?? true, + remaining: null + }) + ), + track: (input) => record('track', input), + updateBalance: (input) => record('updateBalance', input), + checkoutUrl: (input) => + record('checkoutUrl', input).pipe( + Effect.as( + `https://checkout.autumn.invalid/${input.customerId}/${input.planId}` + ) + ), + portalUrl: (input) => + record('portalUrl', input).pipe( + Effect.as(`https://portal.autumn.invalid/${input.customerId}`) + ) +}; + +export const clientFor = (config: AutumnConfig): AutumnClientShape => { + if (config.secretKey === null) return autumnNull; + if (config.secretKey.startsWith(FAKE_AUTUMN_PREFIX)) return autumnFake; + return autumnLive(config.secretKey); +}; + +export const AutumnLive = Layer.effect( + AutumnClient, + Effect.map(AppConfig, (config) => clientFor(config.autumn)) +); + +export const AutumnNull = Layer.succeed(AutumnClient, autumnNull); diff --git a/apps/web/worker-configuration.d.ts b/apps/web/worker-configuration.d.ts index ffb627b..bf824e7 100644 --- a/apps/web/worker-configuration.d.ts +++ b/apps/web/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: d76966b5dbd8efc44b28675760946f8e) +// Generated by Wrangler by running `wrangler types` (hash: cf688ecc2cd59e63dfadf515979d3061) // Runtime types generated with workerd@1.20260722.1 2026-07-27 global_fetch_strictly_public,nodejs_compat interface __BaseEnv_Env { AUTH_GUARD: KVNamespace; @@ -26,6 +26,13 @@ interface __BaseEnv_Env { CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE: string; DASHBOARD_ORIGIN: string; CONTENT_DOMAIN: string; + AUTUMN_SECRET_KEY: string; + AUTUMN_WEBHOOK_SECRET: string; + ADMIN_USER_IDS: string; + URLSCAN_API_KEY: string; + CF_ACCOUNT_ID: string; + CF_API_TOKEN: string; + CF_ZONE_ID: string; } declare namespace Cloudflare { interface ProductionEnv { @@ -53,6 +60,13 @@ declare namespace Cloudflare { CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE: string; DASHBOARD_ORIGIN: string; CONTENT_DOMAIN: string; + AUTUMN_SECRET_KEY: string; + AUTUMN_WEBHOOK_SECRET: string; + ADMIN_USER_IDS: string; + URLSCAN_API_KEY: string; + CF_ACCOUNT_ID: string; + CF_API_TOKEN: string; + CF_ZONE_ID: string; } interface Env extends __BaseEnv_Env {} } @@ -61,7 +75,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types diff --git a/bun.lock b/bun.lock index 557448c..8bf4e08 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "@effect/sql-pg": "4.0.0-beta.102", "@modelcontextprotocol/server": "2.0.0", "agents": "^0.20.1", + "autumn-js": "1.3.0", "effect": "4.0.0-beta.102", "markdown-it": "^14.3.0", "mime": "^4.1.0", @@ -41,6 +42,7 @@ "@types/pg": "^8.23.1", "@typescript/native": "npm:typescript@^7.0.2", "@workos-inc/node": "10.13.0", + "atmn": "1.1.24", "concurrently": "^10.0.4", "svelte": "5.56.8", "svelte-check": "4.7.4", @@ -91,6 +93,8 @@ "@adrive/web": ["@adrive/web@workspace:apps/web"], + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], + "@babel/code-frame": ["@babel/code-frame@8.0.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^8.0.6", "js-tokens": "^10.0.0" } }, "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw=="], "@babel/compat-data": ["@babel/compat-data@8.0.5", "", {}, "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw=="], @@ -295,6 +299,38 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="], + "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], + + "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], + + "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="], + + "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="], + + "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="], + + "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="], + + "@inquirer/prompts": ["@inquirer/prompts@7.10.1", "", { "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", "@inquirer/editor": "^4.2.23", "@inquirer/expand": "^4.0.23", "@inquirer/input": "^4.3.1", "@inquirer/number": "^3.0.23", "@inquirer/password": "^4.0.23", "@inquirer/rawlist": "^4.1.11", "@inquirer/search": "^3.2.2", "@inquirer/select": "^4.4.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="], + + "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="], + + "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="], + + "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], + "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -307,6 +343,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@mishieck/ink-titled-box": ["@mishieck/ink-titled-box@0.3.0", "", { "peerDependencies": { "ink": "^6.0.0", "react": "^19.1.0", "typescript": "^5" } }, "sha512-ugzVH9hixp3hwKfQ8On/qnsrdAxS3y9rTu/aGOFed4zVUvtZyGZNIR4rxAwXult8HKI4vJEh0OM8wib9NPrwUg=="], + "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw=="], "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="], @@ -327,6 +365,16 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], + "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], + + "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], + + "@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="], + + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + + "@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="], + "@oxc-project/types": ["@oxc-project/types@0.151.0", "", {}, "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], @@ -371,8 +419,12 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@speed-highlight/core": ["@speed-highlight/core@1.2.24", "", {}, "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -417,6 +469,10 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.103.2", "", {}, "sha512-I8DkFXls5jXLqtm8+QpOhEmG07hIblhSZFzafg9wHsMSEvMzULy9hK17wU1T/ahfhMbtITJhbxutwwCoihkR7A=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.103.2", "", { "dependencies": { "@tanstack/query-core": "5.103.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-B+fWiYZBc+0uUD5zDZAeLw9dKj7XEsdnuu6zRZ+no6LNKpeE3P3bJ+N6HINjcIlWR6fW3vUoTneEaGa2V6ehqw=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], @@ -435,6 +491,8 @@ "@types/pg": ["@types/pg@8.23.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A=="], + "@types/prettier": ["@types/prettier@3.0.0", "", { "dependencies": { "prettier": "*" } }, "sha512-mFMBfMOz8QxhYVbuINtswBp9VL2b4Y0QqYHwqLz3YbgtfAcat2Dl6Y1o4e22S/OVE6Ebl9m7wWiMT2lSbAs1wA=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -487,20 +545,38 @@ "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "agents": ["agents@0.20.1", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^8.0.2", "@cfworker/json-schema": "^4.1.1", "@rolldown/plugin-babel": "^0.2.3", "cron-schedule": "^6.0.0", "esbuild": "^0.28.1", "mimetext": "^3.0.28", "nanoid": "^5.1.16", "partyserver": "^0.5.8", "partysocket": "1.3.0", "yaml": "^2.9.0", "yargs": "^18.0.0" }, "peerDependencies": { "@ai-sdk/react": "^3.0.0 || ^4.0.0", "@cloudflare/codemode": ">=0.5.0", "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/sdk": "1.30.0", "@modelcontextprotocol/server": "2.0.0", "@tanstack/ai": ">=0.10.2 <1.0.0", "@x402/core": "^2.0.0", "@x402/evm": "^2.0.0", "ai": "^6.0.0 || ^7.0.0", "chat": "^4.29.0", "just-bash": "^3.0.0", "react": "^19.0.0", "vite": ">=6.0.0 <9.0.0", "zod": "^4.0.0" }, "optionalPeers": ["@ai-sdk/react", "@cloudflare/codemode", "@tanstack/ai", "@x402/core", "@x402/evm", "ai", "chat", "just-bash", "vite"], "bin": { "agents": "dist/cli/index.js" } }, "sha512-HQRYMeZpD3k8djYBH7atRPojZMee3NvmXkzsmMWXfdHZ94vMljmWqSsD1XZd70LovHyQrw6/R81AZZIsRiFM6Q=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "arctic": ["arctic@3.7.0", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-ZMQ+f6VazDgUJOd+qNV+H7GohNSYal1mVjm5kEaZfE2Ifb7Ss70w+Q7xpJC87qZDkMZIXYf0pTIYZA0OPasSbw=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "atmn": ["atmn@1.1.24", "", { "dependencies": { "@inquirer/prompts": "^7.6.0", "@mishieck/ink-titled-box": "^0.3.0", "@tanstack/react-query": "^5.90.17", "@types/prettier": "^3.0.0", "arctic": "^3.7.0", "axios": "^1.10.0", "cfonts": "^3.3.1", "chalk": "^5.2.0", "clipboardy": "^5.0.2", "commander": "^14.0.0", "conf": "^13.0.1", "dotenv": "^17.2.0", "ervy": "^1.0.7", "ink": "^6.6.0", "ink-big-text": "^2.0.0", "ink-chart": "^0.1.1", "ink-confirm-input": "^2.0.0", "ink-scroll-list": "^0.4.1", "ink-scroll-view": "^0.3.5", "ink-select-input": "^6.2.0", "ink-spinner": "^5.0.0", "ink-table": "^3.1.0", "ink-text-input": "^6.0.0", "inquirer": "^12.7.0", "jiti": "^2.4.2", "open": "^10.1.2", "prettier": "^3.6.2", "react": "^19.2.3", "yocto-spinner": "^1.0.0", "zod": "^4.0.0" }, "bin": { "atmn": "dist/cli.js" } }, "sha512-6fN4JUALZ59Y4H0oBzC6C+h1IDEu4Q66JD/OCrx5hiDtlgt0UcfHzPdUV3ScfKfF1QvFzhOUMT+3oLRbh8xTIg=="], + + "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], + + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + + "autumn-js": ["autumn-js@1.3.0", "", { "dependencies": { "query-string": "^9.2.2", "rou3": "^0.6.1", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "^1.6.0", "better-call": "^1.0.12", "express": "^5.2.1", "hono": "^4.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["better-auth", "better-call", "express", "hono", "next", "react"] }, "sha512-JtPcfr/0rMluE0gW2mx4JGGBBl6lo9X8hmXYCGA3W+OKBM6OEjrsIPdg9Ohp/hheurb636ZzEe76DZt2NTzIGw=="], + + "axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="], + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.11.26", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ=="], @@ -511,6 +587,8 @@ "browserslist": ["browserslist@4.29.1", "", { "dependencies": { "baseline-browser-mapping": "^2.11.25", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.438", "node-releases": "^2.0.57", "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw=="], + "bundle-name": ["bundle-name@4.1.1", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -519,24 +597,56 @@ "caniuse-lite": ["caniuse-lite@1.0.30001812", "", {}, "sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng=="], + "cfonts": ["cfonts@3.3.1", "", { "dependencies": { "supports-color": "^8", "window-size": "^1" }, "bin": { "cfonts": "bin/index.js" } }, "sha512-ZGEmN3W9mViWEDjsuPo4nK4h39sfh6YtoneFYp9WLPI/rw8BaSSrfQC6jkrGW3JMvV3ZnExJB/AEqXc/nHYxkw=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "clipboard-image": ["clipboard-image@0.1.0", "", { "dependencies": { "run-jxa": "^3.0.0" }, "bin": { "clipboard-image": "cli.js" } }, "sha512-SWk7FgaXLNFld19peQ/rTe0n97lwR1WbkqxV6JKCAOh7U52AKV/PeMFCyt/8IhBdqyDA8rdyewQMKZqvWT5Akg=="], + + "clipboardy": ["clipboardy@5.3.2", "", { "dependencies": { "clipboard-image": "^0.1.0", "execa": "^9.6.1", "is-wayland": "^0.1.0", "is-wsl": "^3.1.0", "is64bit": "^2.0.0", "powershell-utils": "^0.2.0" } }, "sha512-R35PENCHFCw6lsd5SjYPuAVV3Zawr74mKc7ogFNzoDPoQsmWDoJgUNNnWCk/czeqdZGZs8Y0M8zkOlVoySfHEQ=="], + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], + "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "concurrently": ["concurrently@10.0.5", "", { "dependencies": { "chalk": "5.6.2", "rxjs": "7.8.2", "shell-quote": "1.9.0", "supports-color": "10.2.2", "tree-kill": "1.2.2", "yargs": "18.0.0" }, "bin": { "conc": "dist/bin/index.js", "concurrently": "dist/bin/index.js" } }, "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg=="], + "conf": ["conf@13.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^9.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.6.3", "uint8array-extras": "^1.4.0" } }, "sha512-Bi6v586cy1CoTFViVO4lGTtx780lfF96fUmS1lSX6wpZf6330NvHUu6fReVuDP1de8Mg0nkZb01c8tAQdz1o3w=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -549,10 +659,26 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "crypto-random-string": ["crypto-random-string@4.0.0", "", { "dependencies": { "type-fest": "^1.0.1" } }, "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA=="], + + "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decode-uri-component": ["decode-uri-component@0.5.0", "", {}, "sha512-1BiQVoK8C9gUbQU6NzAtO/tkz2qOFpEObMWpcFvhx4fYnj4Oc5yzaJN/LD36ihkVUdXyh5ZekzX+yM+ty/SrPg=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "define-property": ["define-property@1.0.0", "", { "dependencies": { "is-descriptor": "^1.0.0" } }, "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -563,6 +689,10 @@ "devalue": ["devalue@5.9.4", "", {}, "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg=="], + "dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="], + + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -581,20 +711,32 @@ "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + "ervy": ["ervy@1.0.7", "", {}, "sha512-LyHLPwIKxCKKtTO/qBMFzwA1BD5IjpM0AA3k6CeK9hrEn4Kbayi93G9eD/Ko4suEIjerSl7YpMmaOoL0g9OCoQ=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-toolkit": ["es-toolkit@1.52.0", "", {}, "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA=="], + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], "esrap": ["esrap@2.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-Dc8aMY0tqJNIN3ahtccEm/UWma05Mt/N7NBGdTMtJ1bR5SyctNEFVaOUfmkzVXe6Psk8MPfiiqUafXIoMbWCsQ=="], @@ -607,6 +749,8 @@ "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express-rate-limit": ["express-rate-limit@8.7.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g=="], @@ -619,12 +763,20 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "filter-obj": ["filter-obj@5.1.0", "", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], "flru": ["flru@1.0.2", "", {}, "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog=="], + "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -643,36 +795,100 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hono": ["hono@4.13.9", "", {}, "sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], + "ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="], + + "ink-big-text": ["ink-big-text@2.0.0", "", { "dependencies": { "cfonts": "^3.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "ink": ">=4", "react": ">=18" } }, "sha512-Juzqv+rIOLGuhMJiE50VtS6dg6olWfzFdL7wsU/EARSL5Eaa5JNXMogMBm9AkjgzO2Y3UwWCOh87jbhSn8aNdw=="], + + "ink-chart": ["ink-chart@0.1.1", "", { "dependencies": { "ervy": "^1.0.4", "prop-types": "^15.7.2" }, "peerDependencies": { "ink": ">=2.0.0", "react": ">=16.8.0" } }, "sha512-A3WxEO4t5ueuTves/zhaTJe/XcPXnlmf//gcKni8W1Nme6Dj0KhtWPTrsNpp6cZQR6k6HG/diEvZVcQ8DMmYyQ=="], + + "ink-confirm-input": ["ink-confirm-input@2.0.0", "", { "dependencies": { "ink-text-input": "^3.2.1", "prop-types": "^15.5.10", "yn": "^3.1.1" }, "peerDependencies": { "ink": ">=2", "react": ">=16" } }, "sha512-YCd7a9XW0DIIbOhF3XiLo3WF86mOart9qI1qN56wT5IDJxU+j8BanEZh5/QXoazyIPSv1iXlHPIlRB5cbZIMIA=="], + + "ink-scroll-list": ["ink-scroll-list@0.4.1", "", { "dependencies": { "ink-scroll-view": "^0.3.5" }, "peerDependencies": { "ink": ">=6", "react": ">=19" } }, "sha512-2GcjnBXRe0sTSMC03SCKX6IkNAsYYgcpvAN+ggAiaqeg+s+qA9Ls0iVTOmB+Ize/9AVts8xloG4aRcrOy/7Xxg=="], + + "ink-scroll-view": ["ink-scroll-view@0.3.7", "", { "peerDependencies": { "ink": "^5 || ^6 || ^7", "react": "^18 || ^19" } }, "sha512-lUBLxSbVry/+UtJhuvRu6wumP43+ScVp69J7e+hmYwz1kTkahWfkVwWOu7Mn1DMPb8AU8bnsmEsr6kvSJvnaRw=="], + + "ink-select-input": ["ink-select-input@6.2.0", "", { "dependencies": { "figures": "^6.1.0", "to-rotated": "^1.0.0" }, "peerDependencies": { "ink": ">=5.0.0", "react": ">=18.0.0" } }, "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ=="], + + "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], + + "ink-table": ["ink-table@3.1.0", "", { "dependencies": { "object-hash": "^2.0.3" }, "peerDependencies": { "ink": ">=3.0.0", "react": ">=16.8.0" } }, "sha512-qxVb4DIaEaJryvF9uZGydnmP9Hkmas3DCKVpEcBYC0E4eJd3qNgNe+PZKuzgCERFe9LfAS1TNWxCr9+AU4v3YA=="], + + "ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="], + + "inquirer": ["inquirer@12.11.1", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/prompts": "^7.10.1", "@inquirer/type": "^3.0.10", "mute-stream": "^2.0.0", "run-async": "^4.0.6", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw=="], + "ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="], "ip-address": ["ip-address@10.7.2", "", {}, "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-accessor-descriptor": ["is-accessor-descriptor@1.0.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-AIbwAcazqP3R65dGvqk1V+a+vE5Fg1yu/ZKMOiBWSUIXXiwQkYmXQcVa2O0nh0tSDKDFKxG2mY7dB1Sr4hEP1g=="], + + "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + + "is-data-descriptor": ["is-data-descriptor@1.0.1", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw=="], + + "is-descriptor": ["is-descriptor@1.0.4", "", { "dependencies": { "is-accessor-descriptor": "^1.0.2", "is-data-descriptor": "^1.0.1" } }, "sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-number": ["is-number@3.0.0", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "is-wayland": ["is-wayland@0.1.0", "", {}, "sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA=="], + + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], @@ -681,7 +897,7 @@ "js-base64": ["js-base64@3.9.4", "", {}, "sha512-PtOMXpEGuP0RRiRXsjzHzl44dMHxSu2CPvAhinupR1tBa88me+1DPqsobl7eupn5UukjLkln1ZnAOAOXOrYG0Q=="], - "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -691,6 +907,8 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], @@ -723,8 +941,12 @@ "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "macos-version": ["macos-version@6.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-O2S8voA+pMfCHhBn/TIYDXzJ1qNHpPDU32oFxglKnVdJABiYYITt45oLkV9yhwA3E2FDwn3tQqUFrTsr1p3sBQ=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "markdown-it": ["markdown-it@14.3.2", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q=="], @@ -737,6 +959,8 @@ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -745,6 +969,10 @@ "mimetext": ["mimetext@3.0.28", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@babel/runtime-corejs3": "^7.26.0", "js-base64": "^3.7.7", "mime-types": "^2.1.35" } }, "sha512-eQXpbNrtxLCjUtiVbR/qR09dbPgZ2o+KR1uA7QKqGhbn8QV7HIL16mXXsobBL4/8TqoYh1us31kfz+dNfCev9g=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "miniflare": ["miniflare@4.20260722.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.28.0", "workerd": "1.20260722.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-LW6ABMhCx/yIEFBLC/DO4yAhdm2T/G7jp7pr5T2kj895+CCIaHZqpMXdW9O6YE48LcYcCJChwWc8aEs1vpbTXw=="], "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], @@ -759,6 +987,8 @@ "multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="], + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], @@ -767,8 +997,12 @@ "node-releases": ["node-releases@2.0.57", "", {}, "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], @@ -779,6 +1013,12 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -787,6 +1027,8 @@ "partysocket": ["partysocket@1.3.0", "", { "dependencies": { "event-target-polyfill": "^0.0.4" }, "peerDependencies": { "react": ">=17" }, "optionalPeers": ["react"] }, "sha512-1zToNyolZFK/7nuAw/K2bZrNzFqaZyRoCEkS+9vG6WSC5ikrN6qWRe96q6ImU51uptz2r+dAwSkwhJVdQi4LiA=="], + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], @@ -831,24 +1073,38 @@ "postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="], + "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="], + "prettier": ["prettier@3.9.9", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg=="], "prettier-plugin-svelte": ["prettier-plugin-svelte@4.1.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^5.0.0" } }, "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA=="], + "pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proxy-addr": ["proxy-addr@2.0.8", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], + "query-string": ["query-string@9.5.1", "", { "dependencies": { "decode-uri-component": "^0.5.0", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-/zO3RwuRCMTIcEgq6YMv4OrtEE1XzBG7w5N6zc6ydYnkWYOWsnLI/5894hYEzfESfMOT4cHCsRTIdxsSl1KjGg=="], + "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], @@ -859,10 +1115,20 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + "rolldown": ["rolldown@1.2.11", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.11", "@rolldown/binding-android-arm64": "1.2.11", "@rolldown/binding-darwin-arm64": "1.2.11", "@rolldown/binding-darwin-x64": "1.2.11", "@rolldown/binding-freebsd-x64": "1.2.11", "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", "@rolldown/binding-linux-arm64-gnu": "1.2.11", "@rolldown/binding-linux-arm64-musl": "1.2.11", "@rolldown/binding-linux-ppc64-gnu": "1.2.11", "@rolldown/binding-linux-s390x-gnu": "1.2.11", "@rolldown/binding-linux-x64-gnu": "1.2.11", "@rolldown/binding-linux-x64-musl": "1.2.11", "@rolldown/binding-openharmony-arm64": "1.2.11", "@rolldown/binding-win32-arm64-msvc": "1.2.11", "@rolldown/binding-win32-x64-msvc": "1.2.11" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw=="], + "rou3": ["rou3@0.6.3", "", {}, "sha512-1HSG1ENTj7Kkm5muMnXuzzfdDOf7CFnbSYFA+H3Fp/rB9lOCxCPgy1jlZxTKyFoC5jJay8Mmc+VbPLYRjzYLrA=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "run-async": ["run-async@4.0.6", "", {}, "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ=="], + + "run-jxa": ["run-jxa@3.0.0", "", { "dependencies": { "execa": "^5.1.1", "macos-version": "^6.0.0", "subsume": "^4.0.0", "type-fest": "^2.0.0" } }, "sha512-4f2CrY7H+sXkKXJn/cE6qRA3z+NMVO7zvlZ/nUV0e62yWftpiLAfw5eV9ZdomzWd2TXWwEIiGjAT57+lWIzzvA=="], + "runed": ["runed@0.37.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0", "zod": "^4.1.0" }, "optionalPeers": ["@sveltejs/kit", "zod"] }, "sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA=="], "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], @@ -871,6 +1137,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -897,32 +1165,56 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "split-on-first": ["split-on-first@3.0.0", "", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], + + "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], + + "subsume": ["subsume@4.0.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0", "unique-string": "^3.0.0" } }, "sha512-BWnYJElmHbYZ/zKevy+TG+SsyoFCmRPDHJbR1MzLxkPOv1Jp/4hGhVUtP98s+wZBsBsHwCXvPTP0x287/WMjGg=="], + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "svelte": ["svelte@5.56.8", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg=="], "svelte-check": ["svelte-check@4.7.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.1", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-IW9ot9YqAoyv8FvyN+eb4ZTe8zgcKZrJLNYU6dzSKkGwEBsSPc4K7lmQ8bKn8W2YMXM6WDfZSSVOaGtekyUfOQ=="], + "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="], @@ -933,18 +1225,26 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-fest": ["type-fest@5.10.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA=="], + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "undici": ["undici@8.11.2", "", {}, "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ=="], "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "unique-string": ["unique-string@3.0.0", "", { "dependencies": { "crypto-random-string": "^4.0.0" } }, "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="], @@ -959,8 +1259,14 @@ "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="], + + "window-size": ["window-size@1.1.1", "", { "dependencies": { "define-property": "^1.0.0", "is-number": "^3.0.0" }, "bin": { "window-size": "cli.js" } }, "sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA=="], + "workerd": ["workerd@1.20260722.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260722.1", "@cloudflare/workerd-darwin-arm64": "1.20260722.1", "@cloudflare/workerd-linux-64": "1.20260722.1", "@cloudflare/workerd-linux-arm64": "1.20260722.1", "@cloudflare/workerd-windows-64": "1.20260722.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-NycKuc1x2onvsRfGGpM093vRlLFU2zHDAM0+APpccfg4+gZxDGCH27RmdDvkeBuoZyYqgLo3oAfF6re4mvC3vQ=="], "worktop": ["worktop@0.8.0-next.18", "", { "dependencies": { "mrmime": "^2.0.0", "regexparam": "^3.0.0" } }, "sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw=="], @@ -973,6 +1279,8 @@ "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -983,6 +1291,16 @@ "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + + "yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="], + + "yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], @@ -993,12 +1311,22 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "@babel/code-frame/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + "@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.4.0-beta.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0-beta.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w=="], "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@effect/sql-pg/pg-types": ["pg-types@4.1.0", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg=="], + "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "@mishieck/ink-titled-box/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + + "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], + "@sveltejs/vite-plugin-svelte/magic-string": ["magic-string@1.4.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g=="], "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1019,28 +1347,54 @@ "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "cfonts/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], + + "dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "ink-confirm-input/ink-text-input": ["ink-text-input@3.3.0", "", { "dependencies": { "chalk": "^3.0.0", "prop-types": "^15.5.10" }, "peerDependencies": { "ink": "^2.0.0", "react": "^16.5.2" } }, "sha512-gO4wrOf2ie3YuEARTIwGlw37lMjFn3Gk6CKIDrMlHb46WFMagZU7DplohjM24zynlqfnXA5UDEIfC2NBcvD8kg=="], + + "ink-text-input/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], "miniflare/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "parse5/entities": ["entities@8.1.0", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="], "postcss/nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "run-jxa/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "run-jxa/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "subsume/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "youch/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "@effect/sql-pg/pg-types/postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="], @@ -1051,6 +1405,12 @@ "@effect/sql-pg/pg-types/postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="], + "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], @@ -1077,8 +1437,30 @@ "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "ink-confirm-input/ink-text-input/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + + "run-jxa/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "run-jxa/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "run-jxa/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "run-jxa/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "run-jxa/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@inquirer/core/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ink-confirm-input/ink-text-input/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ink-confirm-input/ink-text-input/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], } } From c1dbc1fb3a8e5547f65df3a9def4c83aba4ee1df Mon Sep 17 00:00:00 2001 From: Ben Davis <45952064+bmdavis419@users.noreply.github.com> Date: Wed, 9 Sep 2026 07:36:14 -0700 Subject: [PATCH 2/7] FEATURE: Meter storage and AI usage into Autumn A usage-sync job reports the org's stored bytes as its storage balance and tracks the embedded chunks recorded since the last sync; it is sent after every upload, version commit, site commit, thumbnail, and purge, and reads the org_usage row when it runs so a burst of sends costs one report. Migration 0008 adds the monthly and pending AI counters. Before embedding, indexing asks BillingGates whether the plan allows the chunks; when it does not, the file finishes keyword-only with index_error 'AI quota exhausted' and is offered again a day later. BillingGates.canShare answers the public_sharing feature for the trust policy to combine with the org's trust. Every gate fails open; the local org_usage reservation stays the hard stop. Co-Authored-By: Claude Fable 5.1 --- apps/web/migrations-pg/0010_billing.sql | 18 ++++ apps/web/src/lib/server/edge.ts | 6 +- apps/web/src/lib/server/indexing-sql.ts | 13 ++- apps/web/src/lib/server/jobs/consumer.ts | 10 ++- apps/web/src/lib/server/layer.ts | 10 ++- .../src/lib/server/services/billing-gates.ts | 38 +++++++++ apps/web/src/lib/server/services/billing.ts | 47 +++++++++++ .../lib/server/services/files/internals.ts | 7 ++ .../src/lib/server/services/files/purge.ts | 3 +- .../lib/server/services/files/thumbnails.ts | 5 +- .../src/lib/server/services/files/upload.ts | 4 +- apps/web/src/lib/server/services/indexing.ts | 44 ++++++++++ .../src/lib/server/services/sites/sessions.ts | 1 + apps/web/src/lib/server/usage.ts | 84 +++++++++++++++++++ packages/shared/src/index.ts | 6 ++ 15 files changed, 287 insertions(+), 9 deletions(-) create mode 100644 apps/web/migrations-pg/0010_billing.sql create mode 100644 apps/web/src/lib/server/services/billing-gates.ts create mode 100644 apps/web/src/lib/server/services/billing.ts create mode 100644 apps/web/src/lib/server/usage.ts diff --git a/apps/web/migrations-pg/0010_billing.sql b/apps/web/migrations-pg/0010_billing.sql new file mode 100644 index 0000000..ec60496 --- /dev/null +++ b/apps/web/migrations-pg/0010_billing.sql @@ -0,0 +1,18 @@ +-- migrate:up +-- Billing counters beside the storage meter. ai_ops_month is this +-- calendar month's embedded chunk count for the billing page; it is +-- reset by the next increment after ai_ops_month_reset_at. ai_ops_pending +-- is what the usage-sync job has not yet tracked into Autumn; the job +-- subtracts what it sent, so a failed send is retried, never lost. +ALTER TABLE org_usage + ADD COLUMN ai_ops_month integer NOT NULL DEFAULT 0 + CHECK (ai_ops_month >= 0); +ALTER TABLE org_usage ADD COLUMN ai_ops_month_reset_at timestamptz; +ALTER TABLE org_usage + ADD COLUMN ai_ops_pending integer NOT NULL DEFAULT 0 + CHECK (ai_ops_pending >= 0); + +-- migrate:down +ALTER TABLE org_usage DROP COLUMN ai_ops_pending; +ALTER TABLE org_usage DROP COLUMN ai_ops_month_reset_at; +ALTER TABLE org_usage DROP COLUMN ai_ops_month; diff --git a/apps/web/src/lib/server/edge.ts b/apps/web/src/lib/server/edge.ts index a4d8cee..057e270 100644 --- a/apps/web/src/lib/server/edge.ts +++ b/apps/web/src/lib/server/edge.ts @@ -21,6 +21,8 @@ import { PgSql } from './pg'; import type { AppConfig } from './config'; import type { Auth } from './services/auth'; import type { AutumnClient } from './services/autumn'; +import type { Billing } from './services/billing'; +import type { BillingGates } from './services/billing-gates'; import type { AuthGuardStore } from './services/bindings'; import type { Blobs } from './services/blobs'; import type { CurrentOrg, CurrentUser } from './services/current-org'; @@ -66,7 +68,9 @@ export type AppServices = | JobQueue | Org | WorkOSClient - | AutumnClient; + | AutumnClient + | BillingGates + | Billing; export const isAppError = (failure: unknown): failure is AppError => failure instanceof InvalidRequest || diff --git a/apps/web/src/lib/server/indexing-sql.ts b/apps/web/src/lib/server/indexing-sql.ts index c40f620..1b5670d 100644 --- a/apps/web/src/lib/server/indexing-sql.ts +++ b/apps/web/src/lib/server/indexing-sql.ts @@ -106,12 +106,19 @@ export const storeExtractedText = ( ) .pipe(staleAsFalse); -export const finishKeywordOnly = (sql: PgClient.PgClient, lease: IndexLease) => +// Keyword search is complete and no embeddings were made. With a +// `reason` the row says why (the AI quota was exhausted) and when the +// reconciliation sweep may offer it to the embedder again. +export const finishKeywordOnly = ( + sql: PgClient.PgClient, + lease: IndexLease, + reason: { readonly error: string; readonly retryAt: string } | null = null +) => sql<{ id: string }>` UPDATE files SET index_state = 'disabled', indexed_version = NULL, - index_attempts = 0, index_error = NULL, - index_next_run_at = NULL, index_lease_token = NULL + index_attempts = 0, index_error = ${reason?.error ?? null}, + index_next_run_at = ${reason?.retryAt ?? null}, index_lease_token = NULL WHERE ${leaseUpdateFilter(sql, lease)} RETURNING id`.pipe(Effect.map((rows) => rows.length === 1)); diff --git a/apps/web/src/lib/server/jobs/consumer.ts b/apps/web/src/lib/server/jobs/consumer.ts index 7190697..32096ad 100644 --- a/apps/web/src/lib/server/jobs/consumer.ts +++ b/apps/web/src/lib/server/jobs/consumer.ts @@ -4,6 +4,7 @@ import type { AppServices } from '../edge'; import { StorageError } from '../errors'; import { retryDelaySeconds } from '../job-policy'; import { requestLayer } from '../layer'; +import { Billing } from '../services/billing'; import { Files } from '../services/files'; import { Indexing, type IndexOutcome } from '../services/indexing'; import { Scanner } from '../services/scanner'; @@ -63,6 +64,9 @@ export interface JobHandlers { readonly siteCleanup: ( job: JobOf<'site-cleanup'> ) => Effect.Effect; + readonly usageSync: ( + job: JobOf<'usage-sync'> + ) => Effect.Effect; } const log = (entry: Record) => @@ -80,6 +84,8 @@ export const dispatchJob = (handlers: JobHandlers) => (job: Job) => { return handlers.purge(job); case 'site-cleanup': return handlers.siteCleanup(job); + case 'usage-sync': + return handlers.usageSync(job); } }; @@ -93,6 +99,7 @@ export const liveJobHandlers = Effect.gen(function* () { const files = yield* Files; const sites = yield* Sites; const scanner = yield* Scanner; + const billing = yield* Billing; return { index: (job) => indexing.runOne(job).pipe(Effect.map(indexOutcome)), // The scanner records its own outcome (a verdict row, a re-sent @@ -100,7 +107,8 @@ export const liveJobHandlers = Effect.gen(function* () { scan: (job) => scanner.runOne(job).pipe(Effect.as('done')), purge: (job) => files.purgeOne(job.fileId).pipe(Effect.as('done')), siteCleanup: (job) => - sites.cleanupSession(job.sessionId).pipe(Effect.as('done')) + sites.cleanupSession(job.sessionId).pipe(Effect.as('done')), + usageSync: () => billing.syncUsage.pipe(Effect.as('done')) } satisfies JobHandlers; }); diff --git a/apps/web/src/lib/server/layer.ts b/apps/web/src/lib/server/layer.ts index 5d85cc2..a30d887 100644 --- a/apps/web/src/lib/server/layer.ts +++ b/apps/web/src/lib/server/layer.ts @@ -5,6 +5,8 @@ import type { ProgramTenant } from './identity'; import { PgSql } from './pg'; import { AuthLive } from './services/auth'; import { AutumnLive } from './services/autumn'; +import { BillingLive } from './services/billing'; +import { BillingGatesLive } from './services/billing-gates'; import { AuthGuardStore, Bucket, @@ -109,6 +111,10 @@ export const requestLayer = (env: Env, tenant: ProgramTenant | null) => { ); const workos = WorkOSLive.pipe(Layer.provide(bindings)); const autumn = AutumnLive.pipe(Layer.provide(bindings)); + const billingGates = BillingGatesLive.pipe(Layer.provide(autumn)); + const billing = BillingLive.pipe( + Layer.provide(Layer.merge(infrastructure, autumn)) + ); const auth = AuthLive.pipe( Layer.provide(Layer.mergeAll(infrastructure, workos, autumn)) ); @@ -132,7 +138,7 @@ export const requestLayer = (env: Env, tenant: ProgramTenant | null) => { Layer.provide(Layer.mergeAll(infrastructure, tags)) ); const indexing = IndexingLive.pipe( - Layer.provide(Layer.mergeAll(infrastructure, semantic)) + Layer.provide(Layer.mergeAll(infrastructure, semantic, billingGates)) ); const lifecycle = LifecycleLive.pipe( Layer.provide(Layer.mergeAll(infrastructure, auth, sites, files, indexing)) @@ -143,6 +149,8 @@ export const requestLayer = (env: Env, tenant: ProgramTenant | null) => { semantic, workos, autumn, + billingGates, + billing, auth, rateLimits, urlReputation, diff --git a/apps/web/src/lib/server/services/billing-gates.ts b/apps/web/src/lib/server/services/billing-gates.ts new file mode 100644 index 0000000..00e9e90 --- /dev/null +++ b/apps/web/src/lib/server/services/billing-gates.ts @@ -0,0 +1,38 @@ +import { Context, Effect, Layer } from 'effect'; +import { AutumnClient } from './autumn'; + +// Plan gates answered by Autumn. Every answer fails open (an unreachable +// or unconfigured Autumn allows), so a gate here is never the only stop: +// storage has the local reservation, and public sharing is combined with +// the org's trust in trust-policy (stack E), which consults canShare. +export interface BillingGatesShape { + // Whether the org's plan carries the public_sharing feature. + readonly canShare: (orgId: string) => Effect.Effect; + // Whether the org may embed `chunks` more chunks this cycle. + readonly canEmbed: (orgId: string, chunks: number) => Effect.Effect; +} + +export class BillingGates extends Context.Service< + BillingGates, + BillingGatesShape +>()('app/BillingGates') {} + +export const BillingGatesLive = Layer.effect( + BillingGates, + Effect.map(AutumnClient, (autumn) => + BillingGates.of({ + canShare: (orgId) => + autumn + .check({ customerId: orgId, featureId: 'public_sharing' }) + .pipe(Effect.map((result) => result.allowed)), + canEmbed: (orgId, chunks) => + autumn + .check({ + customerId: orgId, + featureId: 'ai_ops', + requiredBalance: Math.max(1, chunks) + }) + .pipe(Effect.map((result) => result.allowed)) + }) + ) +); diff --git a/apps/web/src/lib/server/services/billing.ts b/apps/web/src/lib/server/services/billing.ts new file mode 100644 index 0000000..5e2b55e --- /dev/null +++ b/apps/web/src/lib/server/services/billing.ts @@ -0,0 +1,47 @@ +import { Context, Effect, Layer } from 'effect'; +import type { StorageError } from '../errors'; +import { PgSql } from '../pg'; +import { readOrgUsage, settleAiOps } from '../usage'; +import { AutumnClient } from './autumn'; +import { CurrentOrg } from './current-org'; + +export interface BillingShape { + // The usage-sync job: pushes the current stored bytes to Autumn as the + // storage balance and tracks the AI operations recorded since the last + // sync. Reads the row at run time, so several sends coalesce into the + // same answer and a failed run is retried with nothing lost. + readonly syncUsage: Effect.Effect; +} + +export class Billing extends Context.Service()( + 'app/Billing' +) {} + +const makeBilling = Effect.gen(function* () { + const sql = yield* PgSql; + const autumn = yield* AutumnClient; + const org = yield* CurrentOrg; + + const syncUsage = Effect.gen(function* () { + const usage = yield* readOrgUsage(sql, org.id); + // The org was deleted after the job was sent: nothing to report. + if (usage === null) return; + yield* autumn.updateBalance({ + customerId: org.id, + featureId: 'storage_bytes', + usage: usage.storedBytes + }); + if (usage.aiOpsPending > 0) { + yield* autumn.track({ + customerId: org.id, + featureId: 'ai_ops', + value: usage.aiOpsPending + }); + yield* settleAiOps(sql, org.id, usage.aiOpsPending); + } + }).pipe(Effect.withSpan('Billing.syncUsage')); + + return Billing.of({ syncUsage }); +}); + +export const BillingLive = Layer.effect(Billing, makeBilling); diff --git a/apps/web/src/lib/server/services/files/internals.ts b/apps/web/src/lib/server/services/files/internals.ts index 7e69b52..06428af 100644 --- a/apps/web/src/lib/server/services/files/internals.ts +++ b/apps/web/src/lib/server/services/files/internals.ts @@ -113,6 +113,11 @@ export const createInternals = (deps: CoreDeps) => { // scanner publishes a held row itself (services/scanner.ts). const sendScanJob = (fileId: string, version: number) => jobs.trySend({ kind: 'scan', orgId: org.id, fileId, version }); + // After a transaction moved the storage counter: billing reads the row + // when the job runs, so a burst of sends costs one report. + const sendUsageSync = Effect.suspend(() => + jobs.trySend({ kind: 'usage-sync', orgId: org.id }) + ); const findDashboardFile = Effect.fn('Files.findDashboardFile')(function* ( id: string, @@ -235,6 +240,7 @@ export const createInternals = (deps: CoreDeps) => { ); yield* sendIndexJob(current.id, version); if (committed.scan) yield* sendScanJob(current.id, version); + yield* sendUsageSync; return committed.result; }); @@ -253,6 +259,7 @@ export const createInternals = (deps: CoreDeps) => { sendIndexJob, sendPurgeJob, sendScanJob, + sendUsageSync, findDashboardFile, commitStoredVersion }; diff --git a/apps/web/src/lib/server/services/files/purge.ts b/apps/web/src/lib/server/services/files/purge.ts index fcf170d..502072d 100644 --- a/apps/web/src/lib/server/services/files/purge.ts +++ b/apps/web/src/lib/server/services/files/purge.ts @@ -41,7 +41,7 @@ export const purgeDueAt = (row: { export const purgeOps = ( internals: FileInternals ): Pick => { - const { sql, blobs, org, sendPurgeJob } = internals; + const { sql, blobs, org, sendPurgeJob, sendUsageSync } = internals; const storage = (operation: string) => (cause: unknown) => new StorageError({ operation, cause }); @@ -161,6 +161,7 @@ export const purgeOps = ( yield* completePurge(sql, org.id, fileId); forgetTagListCache(org.id); + yield* sendUsageSync; }); // Reconciliation: rows whose purge was due long ago and that no diff --git a/apps/web/src/lib/server/services/files/thumbnails.ts b/apps/web/src/lib/server/services/files/thumbnails.ts index 27b681c..1d513d9 100644 --- a/apps/web/src/lib/server/services/files/thumbnails.ts +++ b/apps/web/src/lib/server/services/files/thumbnails.ts @@ -12,7 +12,7 @@ import type { FilesShape } from './types'; export const thumbnailOps = ( internals: FileInternals ): Pick => { - const { sql, blobs } = internals; + const { sql, blobs, jobs } = internals; const { reserveBytes, compensateStoredBlob } = internals; return { storeDashboardThumbnail: Effect.fn('Files.storeDashboardThumbnail')( @@ -116,6 +116,9 @@ export const thumbnailOps = ( r2Key: winner.thumbnail_r2_key } as const; } + // Thumbnails arrive on the content origin, so the org is the + // caller's, not the layer's. + yield* jobs.trySend({ kind: 'usage-sync', orgId }); return { _tag: 'Stored', blob: stored } as const; } ) diff --git a/apps/web/src/lib/server/services/files/upload.ts b/apps/web/src/lib/server/services/files/upload.ts index 095802f..35f0d1e 100644 --- a/apps/web/src/lib/server/services/files/upload.ts +++ b/apps/web/src/lib/server/services/files/upload.ts @@ -26,7 +26,8 @@ export const uploadOps = ( findDashboardFile, sendIndexJob, sendPurgeJob, - sendScanJob + sendScanJob, + sendUsageSync } = internals; return { upload: Effect.fn('Files.upload')(function* (input) { @@ -116,6 +117,7 @@ export const uploadOps = ( forgetTagListCache(org.id); yield* sendIndexJob(id, 1); if (visibility.public) yield* sendScanJob(id, 1); + yield* sendUsageSync; if (input.expiresAt !== null) yield* sendPurgeJob(id, input.expiresAt); return { diff --git a/apps/web/src/lib/server/services/indexing.ts b/apps/web/src/lib/server/services/indexing.ts index 7a48c05..aa1d075 100644 --- a/apps/web/src/lib/server/services/indexing.ts +++ b/apps/web/src/lib/server/services/indexing.ts @@ -19,12 +19,18 @@ import { isSearchableText, searchTextLimit } from '../search-text'; import { createTtlCache } from '../isolate-cache'; import { stuckBefore } from '../job-policy'; import { PgSql } from '../pg'; +import { recordAiOps } from '../usage'; +import { BillingGates } from './billing-gates'; import { Blobs } from './blobs'; import { CurrentOrg } from './current-org'; import { JobQueue } from './jobs'; import { Embedder, VectorIndex } from './semantic'; const INDEX_LEASE_MS = 5 * 60 * 1_000; +// A file refused embeddings for want of AI quota is offered again once a +// day; the monthly reset (or an upgrade) then lets it through. +const AI_QUOTA_RETRY_MS = 24 * 60 * 60 * 1_000; +export const AI_QUOTA_EXHAUSTED = 'AI quota exhausted'; const EMBEDDING_MODEL = '@cf/baai/bge-small-en-v1.5'; const SEMANTIC_STATUS_CACHE_TTL_MS = 10_000; // Per isolate, per org; the Postgres client is rebuilt per request so it @@ -93,6 +99,31 @@ const makeIndexing = Effect.gen(function* () { const embedder = yield* Embedder; const vectors = yield* VectorIndex; const jobs = yield* JobQueue; + const gates = yield* BillingGates; + + // Suspended: the layer is also built for tenant-less programs, where + // reading the org is a defect. + const sendUsageSync = Effect.suspend(() => + jobs.trySend({ kind: 'usage-sync', orgId: org.id }) + ); + + // The embeddings are committed; what they cost is best effort on top. + const meterAiOps = (fileId: string, chunks: number) => + recordAiOps(sql, org.id, chunks).pipe( + Effect.andThen(sendUsageSync), + Effect.catchCause((cause) => + Effect.sync(() => { + console.error( + JSON.stringify({ + message: 'AI usage could not be recorded', + fileId, + chunks, + cause: String(cause) + }) + ); + }) + ) + ); const storage = (operation: string) => (cause: unknown) => new StorageError({ operation, cause }); @@ -220,6 +251,18 @@ const makeIndexing = Effect.gen(function* () { } const chunks = chunkSearchText(initial.display_name, text); + const allowed = yield* gates.canEmbed(org.id, chunks.length); + if (!allowed) { + const finished = yield* finishKeywordOnly(sql, lease, { + error: AI_QUOTA_EXHAUSTED, + retryAt: new Date(now.getTime() + AI_QUOTA_RETRY_MS).toISOString() + }).pipe(Effect.mapError(storage('finish keyword-only indexing'))); + if (!finished) { + yield* stale('stale keyword-only indexing completion ignored'); + return 'skipped' as const; + } + return 'indexed' as const; + } const embeddings = yield* embedder.documents( chunks.map((chunk) => chunk.text) ); @@ -246,6 +289,7 @@ const makeIndexing = Effect.gen(function* () { yield* stale('stale semantic indexing completion rolled back'); return 'skipped' as const; } + yield* meterAiOps(fileId, chunks.length); return 'indexed' as const; }).pipe( Effect.catchCause((cause) => diff --git a/apps/web/src/lib/server/services/sites/sessions.ts b/apps/web/src/lib/server/services/sites/sessions.ts index 791b37e..7f07ca6 100644 --- a/apps/web/src/lib/server/services/sites/sessions.ts +++ b/apps/web/src/lib/server/services/sites/sessions.ts @@ -460,6 +460,7 @@ export const sessionOps = ( fileId: session.fileId, version: session.version }); + yield* jobs.trySend({ kind: 'usage-sync', orgId: org.id }); const cleanupPending = yield* drainDeletes(session.fileId).pipe( Effect.catchCause((cause) => Effect.sync(() => { diff --git a/apps/web/src/lib/server/usage.ts b/apps/web/src/lib/server/usage.ts new file mode 100644 index 0000000..0b36b06 --- /dev/null +++ b/apps/web/src/lib/server/usage.ts @@ -0,0 +1,84 @@ +import type { PgClient } from '@effect/sql-pg'; +import { Effect } from 'effect'; +import { StorageError } from './errors'; + +// The org_usage row as billing reads it. stored_bytes is moved by the +// storage quota; the AI counters are moved here. +export interface OrgUsage { + readonly storedBytes: number; + readonly fileCount: number; + // Chunks embedded this calendar month, already zero once the month + // the row counted has passed. + readonly aiOpsMonth: number; + readonly aiOpsPending: number; +} + +const storage = (operation: string) => (cause: unknown) => + new StorageError({ operation, cause }); + +export const readOrgUsage = Effect.fn('Usage.read')(function* ( + sql: PgClient.PgClient, + orgId: string +) { + const rows = yield* sql<{ + stored_bytes: number; + file_count: number; + ai_ops_month: number; + ai_ops_pending: number; + }>` + SELECT stored_bytes, file_count, + CASE + WHEN ai_ops_month_reset_at IS NULL OR ai_ops_month_reset_at <= now() + THEN 0 + ELSE ai_ops_month + END AS ai_ops_month, + ai_ops_pending + FROM org_usage + WHERE org_id = ${orgId}`.pipe(Effect.mapError(storage('read org usage'))); + const row = rows[0]; + if (!row) return null; + return { + storedBytes: row.stored_bytes, + fileCount: row.file_count, + aiOpsMonth: row.ai_ops_month, + aiOpsPending: row.ai_ops_pending + } satisfies OrgUsage; +}); + +// Counts embedded chunks: this month's total (restarted when the month +// the row counted is over) and the amount still owed to Autumn. +export const recordAiOps = Effect.fn('Usage.recordAiOps')(function* ( + sql: PgClient.PgClient, + orgId: string, + value: number +) { + if (value <= 0) return; + yield* sql` + UPDATE org_usage + SET ai_ops_month = CASE + WHEN ai_ops_month_reset_at IS NULL OR ai_ops_month_reset_at <= now() + THEN ${value}::integer + ELSE ai_ops_month + ${value}::integer + END, + ai_ops_month_reset_at = CASE + WHEN ai_ops_month_reset_at IS NULL OR ai_ops_month_reset_at <= now() + THEN date_trunc('month', now()) + interval '1 month' + ELSE ai_ops_month_reset_at + END, + ai_ops_pending = ai_ops_pending + ${value}::integer + WHERE org_id = ${orgId}`.pipe(Effect.mapError(storage('record AI usage'))); +}); + +// After Autumn accepted `value`, take it off the pending count. Anything +// recorded meanwhile stays owed for the next sync. +export const settleAiOps = Effect.fn('Usage.settleAiOps')(function* ( + sql: PgClient.PgClient, + orgId: string, + value: number +) { + if (value <= 0) return; + yield* sql` + UPDATE org_usage + SET ai_ops_pending = GREATEST(0, ai_ops_pending - ${value}::integer) + WHERE org_id = ${orgId}`.pipe(Effect.mapError(storage('settle AI usage'))); +}); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 3bf5f0e..af76fe6 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -423,6 +423,12 @@ export const JobSchema = Schema.Union([ kind: Schema.Literal('site-cleanup'), orgId: Schema.String, sessionId: Schema.String + }), + // Pushes the org's current usage counters to billing; several sends + // coalesce because the consumer reads the row when it runs. + Schema.Struct({ + kind: Schema.Literal('usage-sync'), + orgId: Schema.String }) ]); From df11b9a1de697cb8f9b6a722d97c52f84f5854f1 Mon Sep 17 00:00:00 2001 From: Ben Davis <45952064+bmdavis419@users.noreply.github.com> Date: Wed, 9 Sep 2026 07:40:46 -0700 Subject: [PATCH 3/7] FEATURE: Add the billing page /settings/billing shows the org's plan with a storage bar and an AI operations bar read from org_usage, an Upgrade button that follows the Autumn checkout URL from POST /api/billing/checkout, and a Manage billing button that follows the customer portal URL from POST /api/billing/portal. GET /api/billing serves the summary; the two POSTs require an owner with a write credential. Without Autumn configured the buttons are disabled and the summary still renders from local counters. Co-Authored-By: Claude Fable 5.1 --- apps/web/src/lib/dashboard/api.ts | 21 +++ apps/web/src/lib/dashboard/parse.ts | 26 +++ apps/web/src/lib/server/layer.ts | 2 +- apps/web/src/lib/server/request-auth.ts | 15 ++ apps/web/src/lib/server/services/billing.ts | 57 ++++++- apps/web/src/routes/api/billing/+server.ts | 16 ++ .../routes/api/billing/checkout/+server.ts | 19 +++ .../src/routes/api/billing/portal/+server.ts | 17 ++ apps/web/src/routes/settings/+page.svelte | 6 + .../src/routes/settings/billing/+page.svelte | 159 ++++++++++++++++++ packages/shared/src/index.ts | 20 +++ 11 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/routes/api/billing/+server.ts create mode 100644 apps/web/src/routes/api/billing/checkout/+server.ts create mode 100644 apps/web/src/routes/api/billing/portal/+server.ts create mode 100644 apps/web/src/routes/settings/billing/+page.svelte diff --git a/apps/web/src/lib/dashboard/api.ts b/apps/web/src/lib/dashboard/api.ts index cf642a9..cff3705 100644 --- a/apps/web/src/lib/dashboard/api.ts +++ b/apps/web/src/lib/dashboard/api.ts @@ -10,6 +10,8 @@ import type { import { parseApiKeyCreateResponse, parseApiKeyListResponse, + parseBillingLink, + parseBillingSummary, parseFileContentLink, parseFileDetailResponse, parseFileListResponse, @@ -90,6 +92,25 @@ export const changeOrgSlug = async (token: string, slug: string) => { return json(parseOrgSettings, response); }; +export const getBilling = async (token: string, signal?: AbortSignal) => { + const response = await request('/api/billing', token, { signal }); + return json(parseBillingSummary, response); +}; + +export const startCheckout = async (token: string) => { + const response = await request('/api/billing/checkout', token, { + method: 'POST' + }); + return (await json(parseBillingLink, response)).url; +}; + +export const openBillingPortal = async (token: string) => { + const response = await request('/api/billing/portal', token, { + method: 'POST' + }); + return (await json(parseBillingLink, response)).url; +}; + export const listApiKeys = async (token: string, signal?: AbortSignal) => { const response = await request('/api/auth/keys', token, { signal }); return (await json(parseApiKeyListResponse, response)).keys; diff --git a/apps/web/src/lib/dashboard/parse.ts b/apps/web/src/lib/dashboard/parse.ts index 55403ae..433d59e 100644 --- a/apps/web/src/lib/dashboard/parse.ts +++ b/apps/web/src/lib/dashboard/parse.ts @@ -8,6 +8,8 @@ // Struct decoders, they require every declared field but ignore extra keys. import type { ApiKey, + BillingLinkResponse, + BillingSummary, DashboardFile, FileContentLinkResponse, FileDetailResponse, @@ -271,6 +273,30 @@ export const parseOrgSettings = (value: unknown): OrgSettings => { }; }; +export const parseBillingSummary = (value: unknown): BillingSummary => { + const record = requireRecord(value, 'billing'); + const storage = requireRecord(record.storage, 'billing.storage'); + const aiOps = requireRecord(record.aiOps, 'billing.aiOps'); + return { + plan: text(record.plan, 'billing.plan'), + planName: text(record.planName, 'billing.planName'), + billingEnabled: flag(record.billingEnabled, 'billing.billingEnabled'), + storage: { + used: integer(storage.used, 'billing.storage.used'), + limit: integer(storage.limit, 'billing.storage.limit') + }, + aiOps: { + used: integer(aiOps.used, 'billing.aiOps.used'), + limit: integer(aiOps.limit, 'billing.aiOps.limit') + } + }; +}; + +export const parseBillingLink = (value: unknown): BillingLinkResponse => { + const record = requireRecord(value, 'billing'); + return { url: maybeString(record.url, 'billing.url') }; +}; + export const parseApiKeyCreateResponse = ( value: unknown ): ApiKeyCreateResponse => { diff --git a/apps/web/src/lib/server/layer.ts b/apps/web/src/lib/server/layer.ts index a30d887..6e59e47 100644 --- a/apps/web/src/lib/server/layer.ts +++ b/apps/web/src/lib/server/layer.ts @@ -113,7 +113,7 @@ export const requestLayer = (env: Env, tenant: ProgramTenant | null) => { const autumn = AutumnLive.pipe(Layer.provide(bindings)); const billingGates = BillingGatesLive.pipe(Layer.provide(autumn)); const billing = BillingLive.pipe( - Layer.provide(Layer.merge(infrastructure, autumn)) + Layer.provide(Layer.mergeAll(infrastructure, autumn)) ); const auth = AuthLive.pipe( Layer.provide(Layer.mergeAll(infrastructure, workos, autumn)) diff --git a/apps/web/src/lib/server/request-auth.ts b/apps/web/src/lib/server/request-auth.ts index c6da10a..07757cb 100644 --- a/apps/web/src/lib/server/request-auth.ts +++ b/apps/web/src/lib/server/request-auth.ts @@ -71,6 +71,21 @@ export const requireWrite = (event: AuthEvent) => ) ); +// For changes only an owner may make (billing, the content slug). +export const requireOwner = (event: AuthEvent, action: string) => + requireWrite(event).pipe( + Effect.flatMap((auth) => + auth.role === 'owner' + ? Effect.succeed(auth) + : Effect.fail( + new InvalidRequest({ + status: 403, + message: `Only an owner can ${action}` + }) + ) + ) + ); + export interface CredentialInput { readonly authorization: string | null; readonly sessionCookie: string | undefined; diff --git a/apps/web/src/lib/server/services/billing.ts b/apps/web/src/lib/server/services/billing.ts index 5e2b55e..b96b5a4 100644 --- a/apps/web/src/lib/server/services/billing.ts +++ b/apps/web/src/lib/server/services/billing.ts @@ -1,11 +1,22 @@ +import type { BillingSummary } from '@adrive/shared'; import { Context, Effect, Layer } from 'effect'; -import type { StorageError } from '../errors'; +import { AppConfig } from '../config'; +import { StorageError } from '../errors'; import { PgSql } from '../pg'; +import { PRO_PLAN_ID, planLimits } from '../plans'; import { readOrgUsage, settleAiOps } from '../usage'; import { AutumnClient } from './autumn'; import { CurrentOrg } from './current-org'; +const PLAN_NAMES: Record = { free: 'Free', pro: 'Pro' }; + export interface BillingShape { + readonly summary: Effect.Effect; + // Hosted checkout for the pro plan; null when the plan attached with + // no payment step, or billing is not configured. + readonly checkoutUrl: Effect.Effect; + // The customer portal (cards, invoices, cancellation). + readonly portalUrl: Effect.Effect; // The usage-sync job: pushes the current stored bytes to Autumn as the // storage balance and tracks the AI operations recorded since the last // sync. Reads the row at run time, so several sends coalesce into the @@ -21,6 +32,48 @@ const makeBilling = Effect.gen(function* () { const sql = yield* PgSql; const autumn = yield* AutumnClient; const org = yield* CurrentOrg; + const config = yield* AppConfig; + + const billingPage = `${config.dashboardOrigin}/settings/billing`; + + const summary = Effect.gen(function* () { + const rows = yield* sql<{ plan: string }>` + SELECT plan FROM orgs WHERE id = ${org.id}`.pipe( + Effect.mapError( + (cause) => new StorageError({ operation: 'read org plan', cause }) + ) + ); + const plan = rows[0]?.plan; + const usage = yield* readOrgUsage(sql, org.id); + if (plan === undefined || usage === null) { + return yield* new StorageError({ + operation: 'read billing summary', + cause: 'The organization has no plan or usage row' + }); + } + const limits = planLimits(plan); + return { + plan, + planName: PLAN_NAMES[plan] ?? plan, + billingEnabled: autumn.enabled, + storage: { used: usage.storedBytes, limit: limits.storedBytes }, + aiOps: { used: usage.aiOpsMonth, limit: limits.aiOpsPerMonth } + } satisfies BillingSummary; + }).pipe(Effect.withSpan('Billing.summary')); + + // Suspended: the layer is also built for tenant-less programs, where + // reading the org is a defect. + const checkoutUrl = Effect.suspend(() => + autumn.checkoutUrl({ + customerId: org.id, + planId: PRO_PLAN_ID, + successUrl: billingPage + }) + ).pipe(Effect.withSpan('Billing.checkoutUrl')); + + const portalUrl = Effect.suspend(() => + autumn.portalUrl({ customerId: org.id, returnUrl: billingPage }) + ).pipe(Effect.withSpan('Billing.portalUrl')); const syncUsage = Effect.gen(function* () { const usage = yield* readOrgUsage(sql, org.id); @@ -41,7 +94,7 @@ const makeBilling = Effect.gen(function* () { } }).pipe(Effect.withSpan('Billing.syncUsage')); - return Billing.of({ syncUsage }); + return Billing.of({ summary, checkoutUrl, portalUrl, syncUsage }); }); export const BillingLive = Layer.effect(Billing, makeBilling); diff --git a/apps/web/src/routes/api/billing/+server.ts b/apps/web/src/routes/api/billing/+server.ts new file mode 100644 index 0000000..360eb80 --- /dev/null +++ b/apps/web/src/routes/api/billing/+server.ts @@ -0,0 +1,16 @@ +import type { RequestHandler } from './$types'; +import { Effect } from 'effect'; +import { runEdge } from '$lib/server/edge'; +import { requireAuth } from '$lib/server/request-auth'; +import { Billing } from '$lib/server/services/billing'; + +export const GET: RequestHandler = (event) => + runEdge( + Effect.gen(function* () { + const billing = yield* Billing; + yield* requireAuth(event); + return Response.json(yield* billing.summary, { + headers: { 'Cache-Control': 'private, no-store' } + }); + }) + ); diff --git a/apps/web/src/routes/api/billing/checkout/+server.ts b/apps/web/src/routes/api/billing/checkout/+server.ts new file mode 100644 index 0000000..dbf7a85 --- /dev/null +++ b/apps/web/src/routes/api/billing/checkout/+server.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from './$types'; +import { Effect } from 'effect'; +import { runEdge } from '$lib/server/edge'; +import { requireOwner } from '$lib/server/request-auth'; +import { Billing } from '$lib/server/services/billing'; + +// Starts the pro upgrade. The browser follows the URL to the hosted +// checkout; a null URL means the plan attached without a payment step. +export const POST: RequestHandler = (event) => + runEdge( + Effect.gen(function* () { + const billing = yield* Billing; + yield* requireOwner(event, 'change the plan'); + return Response.json( + { url: yield* billing.checkoutUrl }, + { headers: { 'Cache-Control': 'private, no-store' } } + ); + }) + ); diff --git a/apps/web/src/routes/api/billing/portal/+server.ts b/apps/web/src/routes/api/billing/portal/+server.ts new file mode 100644 index 0000000..f0cc3e3 --- /dev/null +++ b/apps/web/src/routes/api/billing/portal/+server.ts @@ -0,0 +1,17 @@ +import type { RequestHandler } from './$types'; +import { Effect } from 'effect'; +import { runEdge } from '$lib/server/edge'; +import { requireOwner } from '$lib/server/request-auth'; +import { Billing } from '$lib/server/services/billing'; + +export const POST: RequestHandler = (event) => + runEdge( + Effect.gen(function* () { + const billing = yield* Billing; + yield* requireOwner(event, 'manage billing'); + return Response.json( + { url: yield* billing.portalUrl }, + { headers: { 'Cache-Control': 'private, no-store' } } + ); + }) + ); diff --git a/apps/web/src/routes/settings/+page.svelte b/apps/web/src/routes/settings/+page.svelte index 9268ac1..d4f30af 100644 --- a/apps/web/src/routes/settings/+page.svelte +++ b/apps/web/src/routes/settings/+page.svelte @@ -140,6 +140,12 @@ {/if} {/if} + + Plan and usage +
diff --git a/apps/web/src/routes/settings/billing/+page.svelte b/apps/web/src/routes/settings/billing/+page.svelte new file mode 100644 index 0000000..f2d6df0 --- /dev/null +++ b/apps/web/src/routes/settings/billing/+page.svelte @@ -0,0 +1,159 @@ + + + + Billing · adrive + + +
+ + + Settings + +

+ Billing +

+ + {#if !session.ready || (billing.loading && !billing.current)} +
+
+
+
+ {:else if !session.token} +

+ Sign in to see your plan. +

+ {:else if billing.error && !billing.current} + + {:else if billing.current} + {@const summary = billing.current} +
+

+ Current plan + {summary.planName} +

+
+ {#if summary.plan !== 'pro'} + + {/if} + +
+
+ +
+
+
+
Storage
+
+ {formatBytes(summary.storage.used)} of {formatBytes( + summary.storage.limit + )} +
+
+
+
+
+
+
+
+
AI operations this month
+
+ {count.format(summary.aiOps.used)} of {count.format( + summary.aiOps.limit + )} +
+
+
+
+
+
+
+ {/if} +
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index af76fe6..aa1247f 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -352,6 +352,26 @@ export const OrgUpdateSchema = Schema.Struct({ slug: Schema.String }); +// The org's plan and how much of it is used, as the billing page shows. +export const BillingSummarySchema = Schema.Struct({ + plan: Schema.String, + planName: Schema.String, + // Whether checkout and the portal are reachable (Autumn configured). + billingEnabled: Schema.Boolean, + storage: Schema.Struct({ used: Schema.Int, limit: Schema.Int }), + aiOps: Schema.Struct({ used: Schema.Int, limit: Schema.Int }) +}); + +export type BillingSummary = typeof BillingSummarySchema.Type; + +// A hosted checkout or portal page to send the browser to; null when no +// step is needed (the plan attached without payment) or billing is off. +export const BillingLinkResponseSchema = Schema.Struct({ + url: Schema.NullOr(Schema.String) +}); + +export type BillingLinkResponse = typeof BillingLinkResponseSchema.Type; + export const DeviceAuthorizationCreateSchema = Schema.Struct({ name: Schema.String }); From 45a39f1a95e9f3ede70fbf16b43396da38f01534 Mon Sep 17 00:00:00 2001 From: Ben Davis <45952064+bmdavis419@users.noreply.github.com> Date: Wed, 9 Sep 2026 07:43:50 -0700 Subject: [PATCH 4/7] FEATURE: Sync plan changes from the Autumn webhook POST /api/webhooks/autumn verifies the Svix headers (svix-id, svix-timestamp, svix-signature) against AUTUMN_WEBHOOK_SECRET with a WebCrypto HMAC-SHA256 and a five minute timestamp window, logs every event by type, and on a billing.updated plan list sets orgs.plan to pro when a pro subscription is in effect and back to free otherwise, so the quota and trust gates read the plan locally. Unknown customers and other event types are acknowledged. Co-Authored-By: Claude Fable 5.1 --- README.md | 3 +- apps/web/src/lib/server/billing-webhook.ts | 67 +++++++++++ apps/web/src/lib/server/svix.ts | 113 ++++++++++++++++++ .../src/routes/api/webhooks/autumn/+server.ts | 79 ++++++++++++ apps/web/wrangler.jsonc | 5 +- docs/release.md | 7 +- 6 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/lib/server/billing-webhook.ts create mode 100644 apps/web/src/lib/server/svix.ts create mode 100644 apps/web/src/routes/api/webhooks/autumn/+server.ts diff --git a/README.md b/README.md index 90d60f1..51db7f1 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,8 @@ create one D1 database and one private R2 bucket, replace the D1 database ID, apply the migration remotely, set the production dashboard origin and content domain, and set the secrets (`MAINTENANCE_SECRET`, `WORKOS_API_KEY`, `WORKOS_CLIENT_ID`, -`WORKOS_COOKIE_PASSWORD`, `WORKOS_WEBHOOK_SECRET`): +`WORKOS_COOKIE_PASSWORD`, `WORKOS_WEBHOOK_SECRET`, `AUTUMN_SECRET_KEY`, +`AUTUMN_WEBHOOK_SECRET`): ```bash cd apps/web && bun x wrangler secret put MAINTENANCE_SECRET diff --git a/apps/web/src/lib/server/billing-webhook.ts b/apps/web/src/lib/server/billing-webhook.ts new file mode 100644 index 0000000..abd1448 --- /dev/null +++ b/apps/web/src/lib/server/billing-webhook.ts @@ -0,0 +1,67 @@ +import type { PgClient } from '@effect/sql-pg'; +import { Effect } from 'effect'; +import { StorageError } from './errors'; +import { type Plan, isPlan } from './plans'; + +// What an Autumn webhook asks the app to do. Every event is logged by +// type; only a plan list (billing.updated's plan_changes) moves +// orgs.plan, which the trust and quota gates read without an API call. +export type BillingWebhookEvent = + | { readonly kind: 'plan'; readonly orgId: string; readonly plan: Plan } + | { readonly kind: 'ignored'; readonly type: string }; + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const text = (value: unknown) => (typeof value === 'string' ? value : ''); + +// Plans in effect after the change: activated or updated entries whose +// subscription is not expired. A scheduled plan is not yet in effect and +// an expired one no longer is. The best known plan wins; none means the +// org is back on free. +const PLAN_RANK: ReadonlyArray = ['pro', 'free']; + +const ENDED = new Set(['expired', 'canceled', 'scheduled']); + +const effectivePlans = (changes: ReadonlyArray) => + changes.flatMap((change) => { + if (!isRecord(change)) return []; + const action = text(change.action); + if (action === 'expired' || action === 'scheduled') return []; + const subject = isRecord(change.subscription) + ? change.subscription + : isRecord(change.purchase) + ? change.purchase + : null; + if (subject === null) return []; + if (ENDED.has(text(subject.status))) return []; + const planId = text(subject.plan_id); + return isPlan(planId) ? [planId] : []; + }); + +export const decodeBillingWebhook = (payload: unknown): BillingWebhookEvent => { + if (!isRecord(payload)) return { kind: 'ignored', type: '' }; + const type = text(payload.type); + const data = isRecord(payload.data) ? payload.data : null; + const orgId = data ? text(data.customer_id) : ''; + const changes = + data && Array.isArray(data.plan_changes) ? data.plan_changes : null; + // Entity-scoped plans belong to a sub-entity, not the org. + if (!orgId || changes === null || (data && text(data.entity_id))) { + return { kind: 'ignored', type }; + } + const effective = effectivePlans(changes); + const plan = PLAN_RANK.find((candidate) => effective.includes(candidate)); + return { kind: 'plan', orgId, plan: plan ?? 'free' }; +}; + +// Resolves to whether an org row was updated (a customer id that names no +// org is acknowledged and dropped). +export const setOrgPlan = (sql: PgClient.PgClient, orgId: string, plan: Plan) => + sql<{ id: string }>` + UPDATE orgs SET plan = ${plan} WHERE id = ${orgId} RETURNING id`.pipe( + Effect.map((rows) => rows.length === 1), + Effect.mapError( + (cause) => new StorageError({ operation: 'update org plan', cause }) + ) + ); diff --git a/apps/web/src/lib/server/svix.ts b/apps/web/src/lib/server/svix.ts new file mode 100644 index 0000000..ee6f317 --- /dev/null +++ b/apps/web/src/lib/server/svix.ts @@ -0,0 +1,113 @@ +// Svix webhook signatures (Autumn delivers through Svix), verified with +// WebCrypto so no library is needed. The signed content is +// `..`, the secret is `whsec_`, +// and the signature header carries one or more space-separated +// `v1,` entries (several during a secret rotation). + +export const SVIX_TOLERANCE_SECONDS = 5 * 60; + +export interface SvixHeaders { + readonly id: string | null; + readonly timestamp: string | null; + readonly signature: string | null; +} + +export type SvixVerification = + | { readonly ok: true } + | { + readonly ok: false; + readonly reason: + 'missing-headers' | 'stale-timestamp' | 'bad-signature' | 'bad-secret'; + }; + +const encoder = new TextEncoder(); + +type Bytes = Uint8Array; + +const decodeBase64 = (value: string): Bytes | null => { + try { + return Uint8Array.from(atob(value), (char) => char.charCodeAt(0)); + } catch { + return null; + } +}; + +const encodeBase64 = (bytes: ArrayBuffer) => + btoa(String.fromCharCode(...new Uint8Array(bytes))); + +const secretBytes = (secret: string) => + decodeBase64(secret.startsWith('whsec_') ? secret.slice(6) : secret); + +const constantTimeEqual = (left: Bytes, right: Bytes) => { + let difference = left.length ^ right.length; + for (let index = 0; index < left.length; index += 1) { + difference |= left[index]! ^ (right[index] ?? 0); + } + return difference === 0; +}; + +const hmac = async (key: Bytes, content: string): Promise => { + const cryptoKey = await crypto.subtle.importKey( + 'raw', + key, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + return new Uint8Array( + await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(content)) + ); +}; + +// The signature Svix would send for this message; tests use it to build +// deliveries and it doubles as the reference for verification. +export const signSvix = async ( + secret: string, + id: string, + timestamp: string, + body: string +) => { + const key = secretBytes(secret); + if (key === null || key.length === 0) return null; + const signature = await hmac(key, `${id}.${timestamp}.${body}`); + return `v1,${encodeBase64(signature.buffer)}`; +}; + +export const verifySvix = async ( + secret: string, + headers: SvixHeaders, + body: string, + nowSeconds = Math.floor(Date.now() / 1000) +): Promise => { + const key = secretBytes(secret); + if (key === null || key.length === 0) { + return { ok: false, reason: 'bad-secret' }; + } + if (!headers.id || !headers.timestamp || !headers.signature) { + return { ok: false, reason: 'missing-headers' }; + } + const sent = Number(headers.timestamp); + if ( + !Number.isFinite(sent) || + Math.abs(nowSeconds - sent) > SVIX_TOLERANCE_SECONDS + ) { + return { ok: false, reason: 'stale-timestamp' }; + } + const expected = await hmac( + key, + `${headers.id}.${headers.timestamp}.${body}` + ); + const presented = headers.signature + .split(' ') + .map((entry) => entry.trim()) + .filter((entry) => entry.startsWith('v1,')) + .map((entry) => decodeBase64(entry.slice(3))) + .filter((bytes): bytes is Bytes => bytes !== null); + // Every candidate is compared so the timing does not reveal which one + // (if any) matched. + let matched = false; + for (const candidate of presented) { + if (constantTimeEqual(expected, candidate)) matched = true; + } + return matched ? { ok: true } : { ok: false, reason: 'bad-signature' }; +}; diff --git a/apps/web/src/routes/api/webhooks/autumn/+server.ts b/apps/web/src/routes/api/webhooks/autumn/+server.ts new file mode 100644 index 0000000..71ed339 --- /dev/null +++ b/apps/web/src/routes/api/webhooks/autumn/+server.ts @@ -0,0 +1,79 @@ +import type { RequestHandler } from './$types'; +import { Effect } from 'effect'; +import { decodeBillingWebhook, setOrgPlan } from '$lib/server/billing-webhook'; +import { AppConfig } from '$lib/server/config'; +import { runEdge } from '$lib/server/edge'; +import { InvalidRequest, Unauthorized } from '$lib/server/errors'; +import { PgSql } from '$lib/server/pg'; +import { verifySvix } from '$lib/server/svix'; + +const MAX_PAYLOAD_BYTES = 256 * 1024; + +const log = (entry: Record) => + Effect.sync(() => { + console.log(JSON.stringify(entry)); + }); + +// Autumn's Svix deliveries. Every event is logged by type; a plan list +// moves orgs.plan so the quota and trust gates read the plan locally. +export const POST: RequestHandler = ({ request }) => + runEdge( + Effect.gen(function* () { + const config = yield* AppConfig; + const sql = yield* PgSql; + if (!config.autumn.webhookSecret) { + return yield* new Unauthorized({ + message: 'The Autumn webhook is not configured' + }); + } + const payload = yield* Effect.tryPromise({ + try: () => request.text(), + catch: () => + new InvalidRequest({ status: 400, message: 'Webhook is invalid' }) + }); + if (payload.length > MAX_PAYLOAD_BYTES) { + return yield* new InvalidRequest({ + status: 413, + message: 'Webhook payload is too large' + }); + } + const verified = yield* Effect.promise(() => + verifySvix( + config.autumn.webhookSecret, + { + id: request.headers.get('svix-id'), + timestamp: request.headers.get('svix-timestamp'), + signature: request.headers.get('svix-signature') + }, + payload + ) + ); + if (!verified.ok) { + return yield* new Unauthorized({ + message: `Webhook signature is invalid (${verified.reason})` + }); + } + const body = yield* Effect.try({ + try: (): unknown => JSON.parse(payload), + catch: () => + new InvalidRequest({ status: 400, message: 'Webhook is invalid' }) + }); + const event = decodeBillingWebhook(body); + if (event.kind === 'plan') { + const updated = yield* setOrgPlan(sql, event.orgId, event.plan); + yield* log({ + message: updated + ? 'org plan updated from Autumn' + : 'Autumn plan change for an unknown org', + orgId: event.orgId, + plan: event.plan + }); + } else { + yield* log({ message: 'Autumn webhook ignored', type: event.type }); + } + return Response.json( + { ok: true as const }, + { headers: { 'Cache-Control': 'private, no-store' } } + ); + }) + ); diff --git a/apps/web/wrangler.jsonc b/apps/web/wrangler.jsonc index cecd9a3..f3a30c2 100644 --- a/apps/web/wrangler.jsonc +++ b/apps/web/wrangler.jsonc @@ -122,8 +122,9 @@ // davis7.space zone, or use a dedicated content-domain zone apex. // 5. `wrangler secret put` MAINTENANCE_SECRET, WORKOS_API_KEY, // WORKOS_CLIENT_ID, WORKOS_COOKIE_PASSWORD, WORKOS_WEBHOOK_SECRET, - // ADMIN_USER_IDS; optionally URLSCAN_API_KEY + CF_ACCOUNT_ID and - // CF_API_TOKEN + CF_ZONE_ID (docs/abuse.md) (all `--env production`) + // ADMIN_USER_IDS, AUTUMN_SECRET_KEY, AUTUMN_WEBHOOK_SECRET; optionally + // URLSCAN_API_KEY + CF_ACCOUNT_ID and CF_API_TOKEN + CF_ZONE_ID + // (docs/abuse.md) (all `--env production`) // 6. `DATABASE_URL=postgres://... bun db:pg:migrate` (see docs/release.md) "env": { "production": { diff --git a/docs/release.md b/docs/release.md index e40b0f6..dd0c51d 100644 --- a/docs/release.md +++ b/docs/release.md @@ -79,7 +79,12 @@ the repository root. `WORKOS_COOKIE_PASSWORD` (32+ characters), `WORKOS_WEBHOOK_SECRET`. Point the WorkOS redirect URI at `/auth/callback` and the webhook at `/api/webhooks/workos` (events `user.deleted`, - `organization_membership.deleted`). + `organization_membership.deleted`). For billing, set `AUTUMN_SECRET_KEY` + and `AUTUMN_WEBHOOK_SECRET` (the Svix signing secret of an Autumn + webhook endpoint at `/api/webhooks/autumn` subscribed + to `billing.updated`), and deploy the plans with + `bun --filter @adrive/web billing:push`. Without the key every plan gate + fails open and nothing is metered. 8. Activate the zones for the hosted target's dashboard and content domains in Cloudflare. The `custom_domain` routes create DNS records for their exact hostnames; they do not create the tenant wildcard. Create a proxied From 6c84fe80c9232fafc27d52e931b97aa66be5f374 Mon Sep 17 00:00:00 2001 From: Ben Davis <45952064+bmdavis419@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:47:17 -0700 Subject: [PATCH 5/7] Enforce billing quotas and reconcile authoritative usage and plans --- apps/web/.dev.vars.example | 4 +- apps/web/autumn.config.ts | 3 +- apps/web/migrations-pg/0010_billing.sql | 11 +- .../web/migrations-pg/0011_usage_delivery.sql | 23 +++ apps/web/src/lib/dashboard/api.ts | 55 ++++-- apps/web/src/lib/dashboard/parse.ts | 1 + apps/web/src/lib/server/billing-webhook.ts | 76 ++++---- apps/web/src/lib/server/config.ts | 16 +- apps/web/src/lib/server/plans.ts | 3 +- apps/web/src/lib/server/services/autumn.ts | 174 +++++++++++++++--- .../src/lib/server/services/billing-gates.ts | 12 +- apps/web/src/lib/server/services/billing.ts | 77 +++++--- apps/web/src/lib/server/services/indexing.ts | 80 ++++---- apps/web/src/lib/server/services/lifecycle.ts | 10 +- apps/web/src/lib/server/usage.ts | 150 ++++++++++++--- apps/web/src/routes/api/billing/+server.ts | 14 +- .../src/routes/api/webhooks/autumn/+server.ts | 39 ++-- .../src/routes/settings/billing/+page.svelte | 93 +++++++--- docs/billing.md | 78 ++++++++ docs/release.md | 6 +- packages/shared/src/index.ts | 2 + 21 files changed, 683 insertions(+), 244 deletions(-) create mode 100644 apps/web/migrations-pg/0011_usage_delivery.sql create mode 100644 docs/billing.md diff --git a/apps/web/.dev.vars.example b/apps/web/.dev.vars.example index d2c8711..c304e6e 100644 --- a/apps/web/.dev.vars.example +++ b/apps/web/.dev.vars.example @@ -16,8 +16,8 @@ WORKOS_COOKIE_PASSWORD="replace-with-at-least-32-characters-of-random-text" WORKOS_WEBHOOK_SECRET="replace" # Autumn billing. Leave AUTUMN_SECRET_KEY empty to run without billing: -# every plan gate allows and nothing is metered. `fake:` selects the -# in-memory fake used by tests. The webhook secret is the Svix signing +# external billing is disabled; local quotas still apply. `fake:` selects +# the in-memory fake in development only. The webhook secret is the Svix signing # secret of the /api/webhooks/autumn endpoint. AUTUMN_SECRET_KEY="" AUTUMN_WEBHOOK_SECRET="" diff --git a/apps/web/autumn.config.ts b/apps/web/autumn.config.ts index 4cbfba8..5cae585 100644 --- a/apps/web/autumn.config.ts +++ b/apps/web/autumn.config.ts @@ -36,7 +36,8 @@ export const free = plan({ featureId: aiOps.id, included: 500, reset: { interval: 'month' } - }) + }), + item({ featureId: publicSharing.id }) ] }); diff --git a/apps/web/migrations-pg/0010_billing.sql b/apps/web/migrations-pg/0010_billing.sql index ec60496..7d105be 100644 --- a/apps/web/migrations-pg/0010_billing.sql +++ b/apps/web/migrations-pg/0010_billing.sql @@ -1,18 +1,13 @@ -- migrate:up -- Billing counters beside the storage meter. ai_ops_month is this --- calendar month's embedded chunk count for the billing page; it is --- reset by the next increment after ai_ops_month_reset_at. ai_ops_pending --- is what the usage-sync job has not yet tracked into Autumn; the job --- subtracts what it sent, so a failed send is retried, never lost. +-- UTC calendar month's embedded chunk count for the billing page; it +-- resets on the next increment after ai_ops_month_reset_at. Sync sends +-- the absolute current total, so a failed acknowledgement is safe to retry. ALTER TABLE org_usage ADD COLUMN ai_ops_month integer NOT NULL DEFAULT 0 CHECK (ai_ops_month >= 0); ALTER TABLE org_usage ADD COLUMN ai_ops_month_reset_at timestamptz; -ALTER TABLE org_usage - ADD COLUMN ai_ops_pending integer NOT NULL DEFAULT 0 - CHECK (ai_ops_pending >= 0); -- migrate:down -ALTER TABLE org_usage DROP COLUMN ai_ops_pending; ALTER TABLE org_usage DROP COLUMN ai_ops_month_reset_at; ALTER TABLE org_usage DROP COLUMN ai_ops_month; diff --git a/apps/web/migrations-pg/0011_usage_delivery.sql b/apps/web/migrations-pg/0011_usage_delivery.sql new file mode 100644 index 0000000..a9c3a60 --- /dev/null +++ b/apps/web/migrations-pg/0011_usage_delivery.sql @@ -0,0 +1,23 @@ +-- migrate:up +-- A periodic absolute snapshot repairs lost enqueues and acknowledgements. +ALTER TABLE org_usage + ADD COLUMN usage_sync_next_run_at timestamptz NOT NULL DEFAULT now(); + +-- Reserve quota before calling the embedder. Only a successful semantic +-- commit consumes the reservation and records billable usage. Abandoned +-- reservations expire with the indexing lease. +CREATE TABLE ai_usage_reservations ( + token text PRIMARY KEY, + org_id text NOT NULL REFERENCES orgs(id) ON DELETE CASCADE, + value integer NOT NULL CHECK (value > 0), + expires_at timestamptz NOT NULL +); +CREATE INDEX ai_usage_reservations_org ON ai_usage_reservations (org_id, expires_at); +ALTER TABLE ai_usage_reservations ENABLE ROW LEVEL SECURITY; +ALTER TABLE ai_usage_reservations FORCE ROW LEVEL SECURITY; +CREATE POLICY org_isolation ON ai_usage_reservations USING (app_org_visible(org_id)); + +-- migrate:down +DROP TABLE ai_usage_reservations; +ALTER TABLE org_usage + DROP COLUMN usage_sync_next_run_at; diff --git a/apps/web/src/lib/dashboard/api.ts b/apps/web/src/lib/dashboard/api.ts index cff3705..6e732a3 100644 --- a/apps/web/src/lib/dashboard/api.ts +++ b/apps/web/src/lib/dashboard/api.ts @@ -92,24 +92,53 @@ export const changeOrgSlug = async (token: string, slug: string) => { return json(parseOrgSettings, response); }; -export const getBilling = async (token: string, signal?: AbortSignal) => { - const response = await request('/api/billing', token, { signal }); - return json(parseBillingSummary, response); +// Billing requests need a deadline through body decoding as well as fetch. +// Clear our timer when the request settles or its caller cancels it. +const withBillingTimeout = async ( + signal: AbortSignal | undefined, + run: (signal: AbortSignal) => Promise +) => { + const timeout = new AbortController(); + const timer = setTimeout(() => { + timeout.abort(new Error('Billing request timed out. Please try again.')); + }, 15_000); + try { + return await run( + signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal + ); + } catch (cause) { + if (timeout.signal.aborted) throw timeout.signal.reason; + throw cause; + } finally { + clearTimeout(timer); + } }; -export const startCheckout = async (token: string) => { - const response = await request('/api/billing/checkout', token, { - method: 'POST' +export const getBilling = (token: string, signal?: AbortSignal) => + withBillingTimeout(signal, async (requestSignal) => { + const response = await request('/api/billing', token, { + signal: requestSignal + }); + return json(parseBillingSummary, response); }); - return (await json(parseBillingLink, response)).url; -}; -export const openBillingPortal = async (token: string) => { - const response = await request('/api/billing/portal', token, { - method: 'POST' +export const startCheckout = (token: string, signal?: AbortSignal) => + withBillingTimeout(signal, async (requestSignal) => { + const response = await request('/api/billing/checkout', token, { + method: 'POST', + signal: requestSignal + }); + return (await json(parseBillingLink, response)).url; + }); + +export const openBillingPortal = (token: string, signal?: AbortSignal) => + withBillingTimeout(signal, async (requestSignal) => { + const response = await request('/api/billing/portal', token, { + method: 'POST', + signal: requestSignal + }); + return (await json(parseBillingLink, response)).url; }); - return (await json(parseBillingLink, response)).url; -}; export const listApiKeys = async (token: string, signal?: AbortSignal) => { const response = await request('/api/auth/keys', token, { signal }); diff --git a/apps/web/src/lib/dashboard/parse.ts b/apps/web/src/lib/dashboard/parse.ts index 433d59e..12ddd98 100644 --- a/apps/web/src/lib/dashboard/parse.ts +++ b/apps/web/src/lib/dashboard/parse.ts @@ -281,6 +281,7 @@ export const parseBillingSummary = (value: unknown): BillingSummary => { plan: text(record.plan, 'billing.plan'), planName: text(record.planName, 'billing.planName'), billingEnabled: flag(record.billingEnabled, 'billing.billingEnabled'), + canManageBilling: flag(record.canManageBilling, 'billing.canManageBilling'), storage: { used: integer(storage.used, 'billing.storage.used'), limit: integer(storage.limit, 'billing.storage.limit') diff --git a/apps/web/src/lib/server/billing-webhook.ts b/apps/web/src/lib/server/billing-webhook.ts index abd1448..44ed9e7 100644 --- a/apps/web/src/lib/server/billing-webhook.ts +++ b/apps/web/src/lib/server/billing-webhook.ts @@ -1,13 +1,12 @@ import type { PgClient } from '@effect/sql-pg'; import { Effect } from 'effect'; import { StorageError } from './errors'; -import { type Plan, isPlan } from './plans'; +import type { AutumnClientShape } from './services/autumn'; -// What an Autumn webhook asks the app to do. Every event is logged by -// type; only a plan list (billing.updated's plan_changes) moves -// orgs.plan, which the trust and quota gates read without an API call. +// plan_changes contains only affected plans, not the complete subscription +// state. A verified customer-level event asks us to reconcile from Autumn. export type BillingWebhookEvent = - | { readonly kind: 'plan'; readonly orgId: string; readonly plan: Plan } + | { readonly kind: 'reconcile'; readonly orgId: string } | { readonly kind: 'ignored'; readonly type: string }; const isRecord = (value: unknown): value is Record => @@ -15,53 +14,40 @@ const isRecord = (value: unknown): value is Record => const text = (value: unknown) => (typeof value === 'string' ? value : ''); -// Plans in effect after the change: activated or updated entries whose -// subscription is not expired. A scheduled plan is not yet in effect and -// an expired one no longer is. The best known plan wins; none means the -// org is back on free. -const PLAN_RANK: ReadonlyArray = ['pro', 'free']; - -const ENDED = new Set(['expired', 'canceled', 'scheduled']); - -const effectivePlans = (changes: ReadonlyArray) => - changes.flatMap((change) => { - if (!isRecord(change)) return []; - const action = text(change.action); - if (action === 'expired' || action === 'scheduled') return []; - const subject = isRecord(change.subscription) - ? change.subscription - : isRecord(change.purchase) - ? change.purchase - : null; - if (subject === null) return []; - if (ENDED.has(text(subject.status))) return []; - const planId = text(subject.plan_id); - return isPlan(planId) ? [planId] : []; - }); - export const decodeBillingWebhook = (payload: unknown): BillingWebhookEvent => { if (!isRecord(payload)) return { kind: 'ignored', type: '' }; const type = text(payload.type); const data = isRecord(payload.data) ? payload.data : null; const orgId = data ? text(data.customer_id) : ''; - const changes = - data && Array.isArray(data.plan_changes) ? data.plan_changes : null; - // Entity-scoped plans belong to a sub-entity, not the org. - if (!orgId || changes === null || (data && text(data.entity_id))) { + if (type !== 'billing.updated' || !orgId || !data || text(data.entity_id)) { return { kind: 'ignored', type }; } - const effective = effectivePlans(changes); - const plan = PLAN_RANK.find((candidate) => effective.includes(candidate)); - return { kind: 'plan', orgId, plan: plan ?? 'free' }; + return { kind: 'reconcile', orgId }; }; -// Resolves to whether an org row was updated (a customer id that names no -// org is acknowledged and dropped). -export const setOrgPlan = (sql: PgClient.PgClient, orgId: string, plan: Plan) => - sql<{ id: string }>` - UPDATE orgs SET plan = ${plan} WHERE id = ${orgId} RETURNING id`.pipe( - Effect.map((rows) => rows.length === 1), - Effect.mapError( - (cause) => new StorageError({ operation: 'update org plan', cause }) +// Lock before the provider read, so concurrent/redelivered signals cannot +// finish an older read after a newer one and restore a stale local plan. +// Unknown customers are acknowledged without creating an Autumn customer. +export const reconcileOrgPlan = ( + sql: PgClient.PgClient, + autumn: AutumnClientShape, + orgId: string +) => + sql + .withTransaction( + Effect.gen(function* () { + const rows = yield* sql<{ id: string }>` + SELECT id FROM orgs WHERE id = ${orgId} FOR UPDATE`; + if (rows.length === 0) return null; + const plan = yield* autumn.getPlan({ customerId: orgId }); + yield* sql`UPDATE orgs SET plan = ${plan} WHERE id = ${orgId}`; + return plan; + }) ) - ); + .pipe( + Effect.mapError((cause) => + cause instanceof StorageError + ? cause + : new StorageError({ operation: 'reconcile org plan', cause }) + ) + ); diff --git a/apps/web/src/lib/server/config.ts b/apps/web/src/lib/server/config.ts index ef61737..43645e0 100644 --- a/apps/web/src/lib/server/config.ts +++ b/apps/web/src/lib/server/config.ts @@ -28,7 +28,7 @@ export interface CloudflareZoneConfig { // Autumn billing. secretKey is null when AUTUMN_SECRET_KEY is unset, in // which case every gate fails open; a `fake:` key picks the in-memory -// fake. See services/autumn.ts. +// fake only in development. See services/autumn.ts. export interface AutumnConfig { readonly secretKey: string | null; readonly webhookSecret: string; @@ -132,10 +132,16 @@ const adminUserIdsFromEnv = (env: Env) => .map((id) => id.trim()) .filter((id) => id.length > 0) ); -const autumnFromEnv = (env: Env): AutumnConfig => ({ - secretKey: optionalString(env.AUTUMN_SECRET_KEY) || null, - webhookSecret: optionalString(env.AUTUMN_WEBHOOK_SECRET) -}); +const autumnFromEnv = (env: Env): AutumnConfig => { + const secretKey = optionalString(env.AUTUMN_SECRET_KEY).trim() || null; + if (secretKey?.startsWith('fake:') && !dev) { + throw new Error('Fake Autumn billing is only available in development'); + } + return { + secretKey, + webhookSecret: optionalString(env.AUTUMN_WEBHOOK_SECRET) + }; +}; export const configFromEnv = (env: Env) => { const origins = normalizeOrigins({ diff --git a/apps/web/src/lib/server/plans.ts b/apps/web/src/lib/server/plans.ts index 65aa4fd..80b13e0 100644 --- a/apps/web/src/lib/server/plans.ts +++ b/apps/web/src/lib/server/plans.ts @@ -12,7 +12,8 @@ export type Plan = keyof typeof PLAN_LIMITS; export const PRO_PLAN_ID: Plan = 'pro'; -export const isPlan = (value: string): value is Plan => value in PLAN_LIMITS; +export const isPlan = (value: string): value is Plan => + Object.hasOwn(PLAN_LIMITS, value); // Unknown plans fall back to free so a typo in the column can only make // an org smaller, never unlimited. diff --git a/apps/web/src/lib/server/services/autumn.ts b/apps/web/src/lib/server/services/autumn.ts index 169bc3a..fd61f2b 100644 --- a/apps/web/src/lib/server/services/autumn.ts +++ b/apps/web/src/lib/server/services/autumn.ts @@ -1,13 +1,15 @@ -import { Autumn } from 'autumn-js'; +import { dev } from '$app/environment'; +import { Autumn, HTTPClient } from 'autumn-js'; import { Context, Effect, Layer } from 'effect'; import { AppConfig, type AutumnConfig } from '../config'; import { StorageError } from '../errors'; +import type { Plan } from '../plans'; // The slice of Autumn the app depends on. The customer id is always the // org id. The SDK is confined to autumnLive; without AUTUMN_SECRET_KEY the // null client fails open (every check allows, every write is a no-op) so -// the app runs unmetered rather than not at all, and a `fake:` key picks -// the in-memory fake for tests. +// local quotas still apply, and a `fake:` key picks the in-memory fake +// only in development. export type FeatureId = 'storage_bytes' | 'ai_ops' | 'public_sharing'; @@ -25,25 +27,23 @@ export interface AutumnClientShape { readonly name: string; readonly email: string; }) => Effect.Effect; - // Never fails: an unreachable Autumn answers `allowed` (its own - // fail-open default), so the local counters stay the hard stop. + // Permission checks alone fail open; local counters stay the hard stop. readonly check: (input: { readonly customerId: string; readonly featureId: FeatureId; readonly requiredBalance?: number; }) => Effect.Effect; - readonly track: (input: { - readonly customerId: string; - readonly featureId: FeatureId; - readonly value: number; - }) => Effect.Effect; readonly updateBalance: (input: { readonly customerId: string; readonly featureId: FeatureId; readonly usage: number; + readonly interval?: 'month'; + readonly nextResetAt?: number; }) => Effect.Effect; - // The hosted checkout URL, or null when no payment step was needed and - // the plan is already attached. + readonly getPlan: (input: { + readonly customerId: string; + }) => Effect.Effect; + // Null means billing is disabled. Live checkout always requires review. readonly checkoutUrl: (input: { readonly customerId: string; readonly planId: string; @@ -68,11 +68,81 @@ const log = (entry: Record) => console.error(JSON.stringify(entry)); }); +const REQUEST_TIMEOUT_MS = 5_000; +const MAX_RESPONSE_BYTES = 2 * 1024 * 1024; + +// Bound both the HTTP request and its body. SDK timeouts otherwise stop at +// response headers, leaving a stalled body able to hold a database lock. +const boundedFetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const controller = new AbortController(); + const parentSignal = + init?.signal ?? (input instanceof Request ? input.signal : null); + const signal = parentSignal + ? AbortSignal.any([controller.signal, parentSignal]) + : controller.signal; + let reader: ReadableStreamDefaultReader | undefined; + let timer: ReturnType | undefined; + const deadline = new Promise((_, reject) => { + timer = setTimeout(() => { + controller.abort(); + reject(new Error('Autumn request timed out')); + }, REQUEST_TIMEOUT_MS); + }); + try { + return await Promise.race([ + deadline, + (async () => { + const response = await fetch(input, { ...init, signal }); + if (signal.aborted) { + void response.body?.cancel().catch(() => {}); + throw new Error('Autumn request aborted'); + } + if (response.body === null) return response; + reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let size = 0; + while (true) { + const chunk = await reader.read(); + if (chunk.done) break; + size += chunk.value.byteLength; + if (size > MAX_RESPONSE_BYTES) + throw new Error('Autumn response is too large'); + chunks.push(chunk.value); + } + const body = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + body.set(chunk, offset); + offset += chunk.byteLength; + } + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers: response.headers + }); + })() + ]); + } finally { + clearTimeout(timer); + controller.abort(); + void reader?.cancel().catch(() => {}); + } +}; + // One SDK instance per isolate; it holds nothing but the key and a fetch. let cachedSdk: { readonly key: string; readonly sdk: Autumn } | undefined; const sdkFor = (secretKey: string) => { if (cachedSdk?.key !== secretKey) { - cachedSdk = { key: secretKey, sdk: new Autumn({ secretKey }) }; + cachedSdk = { + key: secretKey, + sdk: new Autumn({ + secretKey, + failOpen: false, + timeoutMs: REQUEST_TIMEOUT_MS, + retryConfig: { strategy: 'none' }, + httpClient: new HTTPClient({ fetcher: boundedFetch }) + }) + }; } return cachedSdk.sdk; }; @@ -101,27 +171,62 @@ const autumnLive = (secretKey: string): AutumnClientShape => { }).pipe(Effect.as({ allowed: true, remaining: null })) ) ), - track: (input) => - Effect.tryPromise({ - try: () => autumn.track(input), - catch: failure('track Autumn usage') - }).pipe(Effect.asVoid), updateBalance: (input) => Effect.tryPromise({ try: () => autumn.balances.update(input), catch: failure('update Autumn balance') - }).pipe(Effect.asVoid), + }).pipe( + Effect.flatMap((result) => + result.success + ? Effect.void + : Effect.fail( + new StorageError({ + operation: 'update Autumn balance', + cause: 'Autumn did not acknowledge the balance update' + }) + ) + ) + ), + getPlan: (input) => + Effect.tryPromise({ + try: () => autumn.customers.get(input), + catch: failure('read Autumn subscriptions') + }).pipe( + Effect.map((customer): Plan => + customer.subscriptions.some( + (subscription) => + subscription.planId === 'pro' && + (subscription.scope === undefined || + subscription.scope === 'customer') && + (subscription.status === 'active' || + subscription.status === 'past_due') + ) + ? 'pro' + : 'free' + ) + ), checkoutUrl: (input) => Effect.tryPromise({ try: () => autumn.billing.attach({ customerId: input.customerId, planId: input.planId, - redirectMode: 'if_required', + redirectMode: 'always', successUrl: input.successUrl }), catch: failure('start Autumn checkout') - }).pipe(Effect.map((result) => result.paymentUrl)), + }).pipe( + Effect.flatMap((result) => + result.paymentUrl + ? Effect.succeed(result.paymentUrl) + : Effect.fail( + new StorageError({ + operation: 'start Autumn checkout', + cause: 'Autumn did not return a checkout URL' + }) + ) + ) + ), portalUrl: (input) => Effect.tryPromise({ try: () => autumn.billing.openCustomerPortal(input), @@ -148,8 +253,14 @@ export const autumnNull: AutumnClientShape = { enabled: false, ensureCustomer: () => warnOnce, check: () => warnOnce.pipe(Effect.as({ allowed: true, remaining: null })), - track: () => warnOnce, updateBalance: () => warnOnce, + getPlan: () => + Effect.fail( + new StorageError({ + operation: 'read Autumn subscriptions', + cause: 'Autumn billing is not configured' + }) + ), checkoutUrl: () => warnOnce.pipe(Effect.as(null)), portalUrl: () => warnOnce.pipe(Effect.as(null)) }; @@ -165,6 +276,7 @@ export interface AutumnFakeCall { export const autumnFakeCalls: Array = []; export const autumnFakeAllowed = new Map(); +export const autumnFakePlans = new Map(); const record = ( method: AutumnFakeCall['method'], @@ -184,8 +296,11 @@ export const autumnFake: AutumnClientShape = { remaining: null }) ), - track: (input) => record('track', input), updateBalance: (input) => record('updateBalance', input), + getPlan: (input) => + record('getPlan', input).pipe( + Effect.map(() => autumnFakePlans.get(input.customerId) ?? 'free') + ), checkoutUrl: (input) => record('checkoutUrl', input).pipe( Effect.as( @@ -198,15 +313,22 @@ export const autumnFake: AutumnClientShape = { ) }; -export const clientFor = (config: AutumnConfig): AutumnClientShape => { +export const clientFor = ( + config: AutumnConfig, + development = false +): AutumnClientShape => { if (config.secretKey === null) return autumnNull; - if (config.secretKey.startsWith(FAKE_AUTUMN_PREFIX)) return autumnFake; + if (config.secretKey.startsWith(FAKE_AUTUMN_PREFIX)) { + if (!development) + throw new Error('Fake Autumn billing is only available in development'); + return autumnFake; + } return autumnLive(config.secretKey); }; export const AutumnLive = Layer.effect( AutumnClient, - Effect.map(AppConfig, (config) => clientFor(config.autumn)) + Effect.map(AppConfig, (config) => clientFor(config.autumn, dev)) ); export const AutumnNull = Layer.succeed(AutumnClient, autumnNull); diff --git a/apps/web/src/lib/server/services/billing-gates.ts b/apps/web/src/lib/server/services/billing-gates.ts index 00e9e90..6a6c6f8 100644 --- a/apps/web/src/lib/server/services/billing-gates.ts +++ b/apps/web/src/lib/server/services/billing-gates.ts @@ -1,13 +1,9 @@ import { Context, Effect, Layer } from 'effect'; import { AutumnClient } from './autumn'; -// Plan gates answered by Autumn. Every answer fails open (an unreachable -// or unconfigured Autumn allows), so a gate here is never the only stop: -// storage has the local reservation, and public sharing is combined with -// the org's trust in trust-policy (stack E), which consults canShare. +// Autumn's advisory gate fails open during an outage. Indexing reserves +// local monthly quota before embedding, which remains the hard stop. export interface BillingGatesShape { - // Whether the org's plan carries the public_sharing feature. - readonly canShare: (orgId: string) => Effect.Effect; // Whether the org may embed `chunks` more chunks this cycle. readonly canEmbed: (orgId: string, chunks: number) => Effect.Effect; } @@ -21,10 +17,6 @@ export const BillingGatesLive = Layer.effect( BillingGates, Effect.map(AutumnClient, (autumn) => BillingGates.of({ - canShare: (orgId) => - autumn - .check({ customerId: orgId, featureId: 'public_sharing' }) - .pipe(Effect.map((result) => result.allowed)), canEmbed: (orgId, chunks) => autumn .check({ diff --git a/apps/web/src/lib/server/services/billing.ts b/apps/web/src/lib/server/services/billing.ts index b96b5a4..c140ce9 100644 --- a/apps/web/src/lib/server/services/billing.ts +++ b/apps/web/src/lib/server/services/billing.ts @@ -2,25 +2,27 @@ import type { BillingSummary } from '@adrive/shared'; import { Context, Effect, Layer } from 'effect'; import { AppConfig } from '../config'; import { StorageError } from '../errors'; +import { STUCK_JOB_MS } from '../job-policy'; import { PgSql } from '../pg'; import { PRO_PLAN_ID, planLimits } from '../plans'; -import { readOrgUsage, settleAiOps } from '../usage'; +import { readOrgUsage } from '../usage'; import { AutumnClient } from './autumn'; import { CurrentOrg } from './current-org'; const PLAN_NAMES: Record = { free: 'Free', pro: 'Pro' }; export interface BillingShape { - readonly summary: Effect.Effect; - // Hosted checkout for the pro plan; null when the plan attached with - // no payment step, or billing is not configured. + readonly summary: Effect.Effect< + Omit, + StorageError + >; + // Hosted checkout for the pro plan; null when billing is not configured. readonly checkoutUrl: Effect.Effect; // The customer portal (cards, invoices, cancellation). readonly portalUrl: Effect.Effect; // The usage-sync job: pushes the current stored bytes to Autumn as the - // storage balance and tracks the AI operations recorded since the last - // sync. Reads the row at run time, so several sends coalesce into the - // same answer and a failed run is retried with nothing lost. + // storage balance and the current UTC month's successful AI operations. + // Absolute snapshots are safe to retry after a lost acknowledgement. readonly syncUsage: Effect.Effect; } @@ -58,7 +60,7 @@ const makeBilling = Effect.gen(function* () { billingEnabled: autumn.enabled, storage: { used: usage.storedBytes, limit: limits.storedBytes }, aiOps: { used: usage.aiOpsMonth, limit: limits.aiOpsPerMonth } - } satisfies BillingSummary; + } satisfies Omit; }).pipe(Effect.withSpan('Billing.summary')); // Suspended: the layer is also built for tenant-less programs, where @@ -76,22 +78,49 @@ const makeBilling = Effect.gen(function* () { ).pipe(Effect.withSpan('Billing.portalUrl')); const syncUsage = Effect.gen(function* () { - const usage = yield* readOrgUsage(sql, org.id); - // The org was deleted after the job was sent: nothing to report. - if (usage === null) return; - yield* autumn.updateBalance({ - customerId: org.id, - featureId: 'storage_bytes', - usage: usage.storedBytes - }); - if (usage.aiOpsPending > 0) { - yield* autumn.track({ - customerId: org.id, - featureId: 'ai_ops', - value: usage.aiOpsPending - }); - yield* settleAiOps(sql, org.id, usage.aiOpsPending); - } + // Retain obligations while billing is unconfigured; a no-op provider + // must never acknowledge and discard real usage. + if (!autumn.enabled) return; + yield* sql + .withTransaction( + Effect.gen(function* () { + // Serialize the bounded provider calls with usage mutations, so + // older storage snapshots cannot overwrite newer ones out of order. + yield* sql`SELECT org_id FROM org_usage WHERE org_id = ${org.id} FOR UPDATE`; + const rows = yield* sql<{ + stored_bytes: number; + ai_ops_month: number; + ai_ops_reset_at: number; + }>`SELECT stored_bytes, + CASE WHEN ai_ops_month_reset_at IS NULL OR ai_ops_month_reset_at <= now() + THEN 0 ELSE ai_ops_month END AS ai_ops_month, + EXTRACT(epoch FROM ((date_trunc('month', now() AT TIME ZONE 'UTC') + interval '1 month') AT TIME ZONE 'UTC')) * 1000 AS ai_ops_reset_at + FROM org_usage WHERE org_id = ${org.id}`; + const usage = rows.at(0); + if (!usage) return; + yield* autumn.updateBalance({ + customerId: org.id, + featureId: 'storage_bytes', + usage: usage.stored_bytes + }); + yield* autumn.updateBalance({ + customerId: org.id, + featureId: 'ai_ops', + usage: usage.ai_ops_month, + interval: 'month', + nextResetAt: usage.ai_ops_reset_at + }); + yield* sql`UPDATE org_usage + SET usage_sync_next_run_at = clock_timestamp() + ${STUCK_JOB_MS} * interval '1 millisecond' + WHERE org_id = ${org.id}`; + }) + ) + .pipe( + Effect.mapError( + (cause) => + new StorageError({ operation: 'sync billing usage', cause }) + ) + ); }).pipe(Effect.withSpan('Billing.syncUsage')); return Billing.of({ summary, checkoutUrl, portalUrl, syncUsage }); diff --git a/apps/web/src/lib/server/services/indexing.ts b/apps/web/src/lib/server/services/indexing.ts index aa1d075..de12e21 100644 --- a/apps/web/src/lib/server/services/indexing.ts +++ b/apps/web/src/lib/server/services/indexing.ts @@ -19,7 +19,7 @@ import { isSearchableText, searchTextLimit } from '../search-text'; import { createTtlCache } from '../isolate-cache'; import { stuckBefore } from '../job-policy'; import { PgSql } from '../pg'; -import { recordAiOps } from '../usage'; +import { commitAiOps, releaseAiOps, reserveAiOps } from '../usage'; import { BillingGates } from './billing-gates'; import { Blobs } from './blobs'; import { CurrentOrg } from './current-org'; @@ -107,24 +107,6 @@ const makeIndexing = Effect.gen(function* () { jobs.trySend({ kind: 'usage-sync', orgId: org.id }) ); - // The embeddings are committed; what they cost is best effort on top. - const meterAiOps = (fileId: string, chunks: number) => - recordAiOps(sql, org.id, chunks).pipe( - Effect.andThen(sendUsageSync), - Effect.catchCause((cause) => - Effect.sync(() => { - console.error( - JSON.stringify({ - message: 'AI usage could not be recorded', - fileId, - chunks, - cause: String(cause) - }) - ); - }) - ) - ); - const storage = (operation: string) => (cause: unknown) => new StorageError({ operation, cause }); @@ -251,7 +233,15 @@ const makeIndexing = Effect.gen(function* () { } const chunks = chunkSearchText(initial.display_name, text); - const allowed = yield* gates.canEmbed(org.id, chunks.length); + const allowed = + (yield* gates.canEmbed(org.id, chunks.length)) && + (yield* reserveAiOps( + sql, + org.id, + lease.token, + chunks.length, + leaseUntil + )); if (!allowed) { const finished = yield* finishKeywordOnly(sql, lease, { error: AI_QUOTA_EXHAUSTED, @@ -273,25 +263,49 @@ const makeIndexing = Effect.gen(function* () { }); } - const committed = yield* semanticCommit( - sql, - lease, - chunks.map((chunk, index) => ({ - fileId, - version: lease.version, - ordinal: chunk.ordinal, - charStart: chunk.charStart, - charEnd: chunk.charEnd, - values: embeddings[index] ?? [] - })) - ).pipe(Effect.mapError(storage('commit semantic index state'))); + const committed = yield* sql + .withTransaction( + Effect.gen(function* () { + const committed = yield* semanticCommit( + sql, + lease, + chunks.map((chunk, index) => ({ + fileId, + version: lease.version, + ordinal: chunk.ordinal, + charStart: chunk.charStart, + charEnd: chunk.charEnd, + values: embeddings[index] ?? [] + })) + ); + if (committed && chunks.length > 0) + yield* commitAiOps(sql, org.id, lease.token); + return committed; + }) + ) + .pipe(Effect.mapError(storage('commit semantic index state'))); if (!committed) { yield* stale('stale semantic indexing completion rolled back'); return 'skipped' as const; } - yield* meterAiOps(fileId, chunks.length); + yield* sendUsageSync; return 'indexed' as const; }).pipe( + Effect.ensuring( + releaseAiOps(sql, org.id, lease.token).pipe( + Effect.catchCause((cause) => + Effect.sync(() => { + console.error( + JSON.stringify({ + message: 'AI reservation release failed; lease will expire', + fileId, + cause: String(cause) + }) + ); + }) + ) + ) + ), Effect.catchCause((cause) => markFailure(lease, Cause.pretty(cause)).pipe( Effect.catchCause((recordCause) => diff --git a/apps/web/src/lib/server/services/lifecycle.ts b/apps/web/src/lib/server/services/lifecycle.ts index 7d1ac27..eaffcb5 100644 --- a/apps/web/src/lib/server/services/lifecycle.ts +++ b/apps/web/src/lib/server/services/lifecycle.ts @@ -2,6 +2,7 @@ import { Context, Effect, Layer } from 'effect'; import { PgSql } from '../pg'; import { recoverScanJobs } from '../scan-jobs'; import { promoteEstablished } from '../trust'; +import { recoverUsageSync } from '../usage'; import { Auth } from './auth'; import { Files } from './files'; import { Indexing } from './indexing'; @@ -14,6 +15,7 @@ export interface LifecycleSummary { readonly sites: number; readonly indexing: number; readonly scans: number; + readonly usage: number; readonly files: number; } @@ -41,6 +43,7 @@ export interface LifecycleTasks { readonly sites: Effect.Effect; readonly indexing: Effect.Effect; readonly scans: Effect.Effect; + readonly usage: Effect.Effect; readonly files: Effect.Effect; } @@ -74,8 +77,9 @@ export const runLifecycleTasks = (tasks: LifecycleTasks) => const sites = yield* recover('sites', tasks.sites, 0); const indexing = yield* recover('indexing', tasks.indexing, 0); const scans = yield* recover('scans', tasks.scans, 0); + const usage = yield* recover('usage', tasks.usage, 0); const files = yield* recover('files', tasks.files, 0); - return { authentication, sites, indexing, scans, files }; + return { authentication, sites, indexing, scans, usage, files }; }); export const summarize = ( @@ -88,9 +92,10 @@ export const summarize = ( sites: total.sites + summary.sites, indexing: total.indexing + summary.indexing, scans: total.scans + summary.scans, + usage: total.usage + summary.usage, files: total.files + summary.files }), - { authentication, sites: 0, indexing: 0, scans: 0, files: 0 } + { authentication, sites: 0, indexing: 0, scans: 0, usage: 0, files: 0 } ); const makeLifecycle = Effect.gen(function* () { @@ -118,6 +123,7 @@ const makeLifecycle = Effect.gen(function* () { scans: Effect.suspend(() => recoverScanJobs(sql, jobs, currentOrg.id, ORG_SWEEP_LIMIT) ), + usage: Effect.suspend(() => recoverUsageSync(sql, jobs, currentOrg.id)), files: files.sweepPurges(ORG_SWEEP_LIMIT) }).pipe(Effect.withSpan('Lifecycle.org')); diff --git a/apps/web/src/lib/server/usage.ts b/apps/web/src/lib/server/usage.ts index 0b36b06..bd5d628 100644 --- a/apps/web/src/lib/server/usage.ts +++ b/apps/web/src/lib/server/usage.ts @@ -1,6 +1,9 @@ import type { PgClient } from '@effect/sql-pg'; import { Effect } from 'effect'; import { StorageError } from './errors'; +import { STUCK_JOB_MS } from './job-policy'; +import { planLimits } from './plans'; +import type { JobQueue } from './services/jobs'; // The org_usage row as billing reads it. stored_bytes is moved by the // storage quota; the AI counters are moved here. @@ -10,7 +13,6 @@ export interface OrgUsage { // Chunks embedded this calendar month, already zero once the month // the row counted has passed. readonly aiOpsMonth: number; - readonly aiOpsPending: number; } const storage = (operation: string) => (cause: unknown) => @@ -24,15 +26,13 @@ export const readOrgUsage = Effect.fn('Usage.read')(function* ( stored_bytes: number; file_count: number; ai_ops_month: number; - ai_ops_pending: number; }>` SELECT stored_bytes, file_count, CASE WHEN ai_ops_month_reset_at IS NULL OR ai_ops_month_reset_at <= now() THEN 0 ELSE ai_ops_month - END AS ai_ops_month, - ai_ops_pending + END AS ai_ops_month FROM org_usage WHERE org_id = ${orgId}`.pipe(Effect.mapError(storage('read org usage'))); const row = rows[0]; @@ -40,20 +40,18 @@ export const readOrgUsage = Effect.fn('Usage.read')(function* ( return { storedBytes: row.stored_bytes, fileCount: row.file_count, - aiOpsMonth: row.ai_ops_month, - aiOpsPending: row.ai_ops_pending + aiOpsMonth: row.ai_ops_month } satisfies OrgUsage; }); -// Counts embedded chunks: this month's total (restarted when the month -// the row counted is over) and the amount still owed to Autumn. +// Counts successfully indexed chunks in the current UTC calendar month. export const recordAiOps = Effect.fn('Usage.recordAiOps')(function* ( sql: PgClient.PgClient, orgId: string, value: number ) { if (value <= 0) return; - yield* sql` + const rows = yield* sql<{ org_id: string }>` UPDATE org_usage SET ai_ops_month = CASE WHEN ai_ops_month_reset_at IS NULL OR ai_ops_month_reset_at <= now() @@ -62,23 +60,131 @@ export const recordAiOps = Effect.fn('Usage.recordAiOps')(function* ( END, ai_ops_month_reset_at = CASE WHEN ai_ops_month_reset_at IS NULL OR ai_ops_month_reset_at <= now() - THEN date_trunc('month', now()) + interval '1 month' + THEN (date_trunc('month', now() AT TIME ZONE 'UTC') + interval '1 month') AT TIME ZONE 'UTC' ELSE ai_ops_month_reset_at - END, - ai_ops_pending = ai_ops_pending + ${value}::integer - WHERE org_id = ${orgId}`.pipe(Effect.mapError(storage('record AI usage'))); + END + WHERE org_id = ${orgId} + RETURNING org_id`.pipe(Effect.mapError(storage('record AI usage'))); + if (rows.length !== 1) { + return yield* new StorageError({ + operation: 'record AI usage', + cause: 'The organization has no usage row' + }); + } }); -// After Autumn accepted `value`, take it off the pending count. Anything -// recorded meanwhile stays owed for the next sync. -export const settleAiOps = Effect.fn('Usage.settleAiOps')(function* ( +// Pin the plan, then the usage row, before taking a fresh snapshot. The +// separate read sees counters committed while either lock was waiting. +const holdUsage = (sql: PgClient.PgClient, orgId: string) => + Effect.gen(function* () { + yield* sql`SELECT id FROM orgs WHERE id = ${orgId} FOR SHARE`; + yield* sql`SELECT org_id FROM org_usage WHERE org_id = ${orgId} FOR UPDATE`; + const rows = yield* sql<{ plan: string; used: number }>` + SELECT o.plan, + CASE WHEN u.ai_ops_month_reset_at IS NULL OR u.ai_ops_month_reset_at <= now() + THEN 0 ELSE u.ai_ops_month END AS used + FROM org_usage u JOIN orgs o ON o.id = u.org_id + WHERE u.org_id = ${orgId}`; + const row = rows.at(0); + if (!row) + return yield* new StorageError({ + operation: 'lock AI quota', + cause: 'The organization has no usage row' + }); + return row; + }).pipe(Effect.mapError(storage('lock AI quota'))); + +export const reserveAiOps = Effect.fn('Usage.reserveAiOps')(function* ( sql: PgClient.PgClient, orgId: string, - value: number + token: string, + value: number, + expiresAt: string ) { - if (value <= 0) return; - yield* sql` - UPDATE org_usage - SET ai_ops_pending = GREATEST(0, ai_ops_pending - ${value}::integer) - WHERE org_id = ${orgId}`.pipe(Effect.mapError(storage('settle AI usage'))); + if (value <= 0) return true; + return yield* sql + .withTransaction( + Effect.gen(function* () { + const usage = yield* holdUsage(sql, orgId); + yield* sql`DELETE FROM ai_usage_reservations + WHERE org_id = ${orgId} AND expires_at <= clock_timestamp()`; + const existing = yield* sql<{ value: number }>` + SELECT value FROM ai_usage_reservations + WHERE org_id = ${orgId} AND token = ${token}`; + if (existing.length > 0) return existing[0]?.value === value; + const reserved = yield* sql<{ value: number }>` + SELECT COALESCE(sum(value), 0)::integer AS value + FROM ai_usage_reservations WHERE org_id = ${orgId}`; + if ( + usage.used + (reserved[0]?.value ?? 0) + value > + planLimits(usage.plan).aiOpsPerMonth + ) { + return false; + } + const inserted = yield* sql<{ token: string }>` + INSERT INTO ai_usage_reservations (token, org_id, value, expires_at) + SELECT ${token}, ${orgId}, ${value}, ${expiresAt}::timestamptz + WHERE ${expiresAt}::timestamptz > clock_timestamp() + RETURNING token`; + if (inserted.length !== 1) + return yield* new StorageError({ + operation: 'reserve AI quota', + cause: 'The indexing lease expired before embeddings could start' + }); + return true; + }) + ) + .pipe(Effect.mapError(storage('reserve AI quota'))); +}); + +// Called in the same transaction as semanticCommit. Losing or expiring +// the reservation rolls back both the ready state and its metering. +export const commitAiOps = Effect.fn('Usage.commitAiOps')(function* ( + sql: PgClient.PgClient, + orgId: string, + token: string +) { + yield* holdUsage(sql, orgId); + const rows = yield* sql<{ value: number }>` + DELETE FROM ai_usage_reservations + WHERE org_id = ${orgId} AND token = ${token} AND expires_at > clock_timestamp() + RETURNING value`.pipe(Effect.mapError(storage('commit AI reservation'))); + const row = rows.at(0); + if (!row) + return yield* new StorageError({ + operation: 'commit AI reservation', + cause: 'The AI reservation expired before indexing completed' + }); + yield* recordAiOps(sql, orgId, row.value); }); + +export const releaseAiOps = ( + sql: PgClient.PgClient, + orgId: string, + token: string +) => + sql`DELETE FROM ai_usage_reservations WHERE org_id = ${orgId} AND token = ${token}`.pipe( + Effect.asVoid, + Effect.mapError(storage('release AI reservation')) + ); + +// Periodic absolute storage snapshots also repair a lost upload/purge +// enqueue. The durable due time retries a lost recovery send next sweep. +export const recoverUsageSync = ( + sql: PgClient.PgClient, + jobs: JobQueue['Service'], + orgId: string +) => + Effect.gen(function* () { + const rows = yield* sql<{ org_id: string }>` + UPDATE org_usage u + SET usage_sync_next_run_at = clock_timestamp() + ${STUCK_JOB_MS} * interval '1 millisecond' + WHERE u.org_id IN ( + SELECT due.org_id FROM org_usage due + WHERE due.org_id = ${orgId} AND due.usage_sync_next_run_at <= now() + FOR UPDATE SKIP LOCKED + ) + RETURNING org_id`.pipe(Effect.mapError(storage('recover usage sync'))); + if (rows.length > 0) yield* jobs.trySend({ kind: 'usage-sync', orgId }); + return rows.length; + }); diff --git a/apps/web/src/routes/api/billing/+server.ts b/apps/web/src/routes/api/billing/+server.ts index 360eb80..68b5abc 100644 --- a/apps/web/src/routes/api/billing/+server.ts +++ b/apps/web/src/routes/api/billing/+server.ts @@ -8,9 +8,15 @@ export const GET: RequestHandler = (event) => runEdge( Effect.gen(function* () { const billing = yield* Billing; - yield* requireAuth(event); - return Response.json(yield* billing.summary, { - headers: { 'Cache-Control': 'private, no-store' } - }); + const auth = yield* requireAuth(event); + return Response.json( + { + ...(yield* billing.summary), + canManageBilling: auth.role === 'owner' && auth.scope === 'read-write' + }, + { + headers: { 'Cache-Control': 'private, no-store' } + } + ); }) ); diff --git a/apps/web/src/routes/api/webhooks/autumn/+server.ts b/apps/web/src/routes/api/webhooks/autumn/+server.ts index 71ed339..78b686a 100644 --- a/apps/web/src/routes/api/webhooks/autumn/+server.ts +++ b/apps/web/src/routes/api/webhooks/autumn/+server.ts @@ -1,11 +1,16 @@ import type { RequestHandler } from './$types'; import { Effect } from 'effect'; -import { decodeBillingWebhook, setOrgPlan } from '$lib/server/billing-webhook'; +import { + decodeBillingWebhook, + reconcileOrgPlan +} from '$lib/server/billing-webhook'; import { AppConfig } from '$lib/server/config'; import { runEdge } from '$lib/server/edge'; import { InvalidRequest, Unauthorized } from '$lib/server/errors'; import { PgSql } from '$lib/server/pg'; +import { readBoundedText } from '$lib/server/request-json'; import { verifySvix } from '$lib/server/svix'; +import { AutumnClient } from '$lib/server/services/autumn'; const MAX_PAYLOAD_BYTES = 256 * 1024; @@ -14,8 +19,8 @@ const log = (entry: Record) => console.log(JSON.stringify(entry)); }); -// Autumn's Svix deliveries. Every event is logged by type; a plan list -// moves orgs.plan so the quota and trust gates read the plan locally. +// Autumn's verified Svix deliveries reconcile authoritative subscriptions +// into orgs.plan, which quota and trust gates read locally. export const POST: RequestHandler = ({ request }) => runEdge( Effect.gen(function* () { @@ -26,17 +31,11 @@ export const POST: RequestHandler = ({ request }) => message: 'The Autumn webhook is not configured' }); } - const payload = yield* Effect.tryPromise({ - try: () => request.text(), - catch: () => - new InvalidRequest({ status: 400, message: 'Webhook is invalid' }) + const payload = yield* readBoundedText(request, { + maxBytes: MAX_PAYLOAD_BYTES, + invalidLengthMessage: 'Webhook payload is too large', + invalidTextMessage: 'Webhook is invalid' }); - if (payload.length > MAX_PAYLOAD_BYTES) { - return yield* new InvalidRequest({ - status: 413, - message: 'Webhook payload is too large' - }); - } const verified = yield* Effect.promise(() => verifySvix( config.autumn.webhookSecret, @@ -59,14 +58,16 @@ export const POST: RequestHandler = ({ request }) => new InvalidRequest({ status: 400, message: 'Webhook is invalid' }) }); const event = decodeBillingWebhook(body); - if (event.kind === 'plan') { - const updated = yield* setOrgPlan(sql, event.orgId, event.plan); + if (event.kind === 'reconcile') { + const autumn = yield* AutumnClient; + const plan = yield* reconcileOrgPlan(sql, autumn, event.orgId); yield* log({ - message: updated - ? 'org plan updated from Autumn' - : 'Autumn plan change for an unknown org', + message: + plan !== null + ? 'org plan updated from Autumn' + : 'Autumn plan change for an unknown org', orgId: event.orgId, - plan: event.plan + plan }); } else { yield* log({ message: 'Autumn webhook ignored', type: event.type }); diff --git a/apps/web/src/routes/settings/billing/+page.svelte b/apps/web/src/routes/settings/billing/+page.svelte index f2d6df0..e1286e0 100644 --- a/apps/web/src/routes/settings/billing/+page.svelte +++ b/apps/web/src/routes/settings/billing/+page.svelte @@ -10,6 +10,7 @@ import Button from '$lib/components/ui/Button.svelte'; import Icon from '$lib/components/ui/Icon.svelte'; import { resource } from 'runed'; + import { onDestroy } from 'svelte'; const session = getDashboardSession(); const toasts = getToasts(); @@ -19,6 +20,8 @@ ready && token ? getBilling(token, signal) : Promise.resolve(null) ); let busy = $state<'checkout' | 'portal' | null>(null); + let actionController: AbortController | undefined; + onDestroy(() => actionController?.abort()); const percent = (used: number, limit: number) => limit <= 0 ? 0 : Math.min(100, Math.round((used / limit) * 100)); @@ -27,23 +30,37 @@ const follow = async ( kind: 'checkout' | 'portal', - start: (token: string) => Promise, + start: (token: string, signal?: AbortSignal) => Promise, fallback: string ) => { - if (busy || !session.token) return; + if ( + busy || + !session.token || + !billing.current?.canManageBilling || + !billing.current.billingEnabled + ) + return; + const token = session.token; + const controller = new AbortController(); + actionController = controller; busy = kind; try { - const url = await start(session.token); + const url = await start(token, controller.signal); + if (controller.signal.aborted || session.token !== token) return; if (url) { window.location.assign(url); return; } toasts.info(fallback); - void billing.refetch(); } catch (cause) { - toasts.error(cause, 'Billing is unavailable right now'); + if (!controller.signal.aborted && session.token === token) { + toasts.error(cause, 'Billing is unavailable right now'); + } } finally { - busy = null; + if (actionController === controller) { + actionController = undefined; + busy = null; + } } }; @@ -84,38 +101,60 @@
{:else if billing.current} {@const summary = billing.current} -
+

Current plan {summary.planName}

-
- {#if summary.plan !== 'pro'} + {#if summary.canManageBilling} +
+ {#if summary.plan !== 'pro'} + + {/if} - {/if} - -
+
+ {:else} +

Only owners can manage billing.

+ {/if}
diff --git a/docs/billing.md b/docs/billing.md new file mode 100644 index 0000000..260b4e4 --- /dev/null +++ b/docs/billing.md @@ -0,0 +1,78 @@ +# Billing provider contract + +Autumn customers use the aDrive organization ID. `apps/web/autumn.config.ts` +defines Free (2 GiB, 500 AI operations) and Pro ($8/month, 100 GiB, 10,000 AI +operations). Public sharing exists on both plans; verified identity and the +application's trust gates determine eligibility. + +AI allowances reset at the start of each UTC calendar month. Stripe's +subscription billing date is separate from that allowance window. Postgres +is authoritative for stored bytes and AI usage; local reservations enforce +AI limits before embedding work starts. + +## Usage synchronization + +The application sends absolute usage with `balances.update`, including +`interval: 'month'` and the next UTC month's timestamp in `nextResetAt` for AI +operations. Repeated delivery of the same usage does not consume another +allowance. Writes are serialized per organization; durable reconciliation +work and the scheduled sweep repair interrupted deliveries and resubmit +current counters. An old request that completes late at the provider can +briefly leave its copy stale; the next reconciliation restores current +usage. The local counters continue to enforce quotas. + +Do not switch this to additive `track` calls with automatic acknowledgement +of duplicate HTTP 409 responses. Autumn's request idempotency key is claimed +before the operation completes, and the documented default key lifetime is +24 hours. A duplicate acknowledgement therefore does not prove completed +metering, and retries after expiry can add usage twice. + +The SDK has `failOpen: false`. Only permission checks explicitly fail open +on provider failure. Customer creation, balance synchronization, checkout, +portal creation, and subscription reads return errors on failure. Every HTTP +request, including reading its response body, has a five-second deadline; +responses larger than 2 MiB are rejected. SDK retries are disabled so the +application owns recovery and a provider outage cannot hold a transaction +open indefinitely. + +## Subscription changes and checkout + +The signed `billing.updated` webhook is a reconciliation signal. +`plan_changes` lists only the plans affected by an event, so it cannot be +used as a complete subscription snapshot. The handler locks the organization +row before fetching the current customer subscriptions and updating the +local plan. Unknown customers and entity-scoped events are acknowledged +without modifying an organization. Provider failure retains the existing +plan and returns an error so Svix retries. + +Active customer-level Pro subscriptions retain Pro access, including trials, +cancellation at period end, and the provider's default past-due grace state. +Scheduled, expired, and entity-scoped subscriptions do not grant Pro to the +organization. Webhook redelivery fetches the latest state each time. + +Checkout uses `redirectMode: 'always'` so an owner reviews the hosted payment +flow even when a payment method is already saved. A missing URL from a +configured provider is an error; it is not evidence that a plan changed. + +## Configuration and validation + +Set both `AUTUMN_SECRET_KEY` and `AUTUMN_WEBHOOK_SECRET` for live billing. +Without an API key, external billing is disabled and local quotas remain in +force. A configured webhook cannot infer a plan without an API key. Keys +starting with `fake:` are accepted only in development, including when the +client is constructed directly. + +Provider tests exercise the real installed SDK against intercepted HTTP +responses, including failures, stalled bodies, cancellation, calendar usage +payloads, subscription states, and checkout behavior. Postgres tests cover +serialized webhook reconciliation; route tests cover signed delivery and +redelivery. These do not establish live Autumn, Stripe, or Svix behavior; +verify those integrations in their sandbox before production cutover. + +References: + +- [Autumn Update Balance API](https://docs.useautumn.com/api-reference/balances/updateBalance) +- [Autumn payment flow](https://docs.useautumn.com/documentation/customers/payment-flow) +- [Autumn billing reliability and idempotency](https://docs.useautumn.com/documentation/customers/edge-cases) +- [Autumn webhooks](https://docs.useautumn.com/documentation/webhooks) +- [Provider request idempotency implementation](https://github.com/useautumn/autumn/blob/main/server/src/internal/misc/idempotency/withIdempotencyKey.ts) diff --git a/docs/release.md b/docs/release.md index dd0c51d..977901d 100644 --- a/docs/release.md +++ b/docs/release.md @@ -83,8 +83,10 @@ the repository root. and `AUTUMN_WEBHOOK_SECRET` (the Svix signing secret of an Autumn webhook endpoint at `/api/webhooks/autumn` subscribed to `billing.updated`), and deploy the plans with - `bun --filter @adrive/web billing:push`. Without the key every plan gate - fails open and nothing is metered. + `bun --filter @adrive/web billing:push`. Without the key external billing + is disabled; local quotas still apply. Fake billing keys are development + only. See [the billing contract](billing.md) for UTC allowance resets, + durable usage reconciliation, and sandbox validation before cutover. 8. Activate the zones for the hosted target's dashboard and content domains in Cloudflare. The `custom_domain` routes create DNS records for their exact hostnames; they do not create the tenant wildcard. Create a proxied diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index aa1247f..7c8f03a 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -358,6 +358,8 @@ export const BillingSummarySchema = Schema.Struct({ planName: Schema.String, // Whether checkout and the portal are reachable (Autumn configured). billingEnabled: Schema.Boolean, + // Whether the authenticated caller can open checkout or the billing portal. + canManageBilling: Schema.Boolean, storage: Schema.Struct({ used: Schema.Int, limit: Schema.Int }), aiOps: Schema.Struct({ used: Schema.Int, limit: Schema.Int }) }); From 55ba3b17a256e682129e1afb598a6e5688328249 Mon Sep 17 00:00:00 2001 From: Ben Davis <45952064+bmdavis419@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:59:45 -0700 Subject: [PATCH 6/7] Require webhook verification whenever live billing is enabled --- apps/web/src/lib/server/config.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/web/src/lib/server/config.ts b/apps/web/src/lib/server/config.ts index 43645e0..f610b05 100644 --- a/apps/web/src/lib/server/config.ts +++ b/apps/web/src/lib/server/config.ts @@ -134,13 +134,16 @@ const adminUserIdsFromEnv = (env: Env) => ); const autumnFromEnv = (env: Env): AutumnConfig => { const secretKey = optionalString(env.AUTUMN_SECRET_KEY).trim() || null; + const webhookSecret = optionalString(env.AUTUMN_WEBHOOK_SECRET).trim(); if (secretKey?.startsWith('fake:') && !dev) { throw new Error('Fake Autumn billing is only available in development'); } - return { - secretKey, - webhookSecret: optionalString(env.AUTUMN_WEBHOOK_SECRET) - }; + if (secretKey && !secretKey.startsWith('fake:') && !webhookSecret) { + throw new Error( + 'AUTUMN_WEBHOOK_SECRET is required alongside AUTUMN_SECRET_KEY' + ); + } + return { secretKey, webhookSecret }; }; export const configFromEnv = (env: Env) => { From 75908c4e1ed553cf7ad60e85d3a458972d970d24 Mon Sep 17 00:00:00 2001 From: Ben Davis <45952064+bmdavis419@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:35:59 -0700 Subject: [PATCH 7/7] Use organization owner details when creating billing customers --- apps/web/src/lib/server/services/auth.ts | 30 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/web/src/lib/server/services/auth.ts b/apps/web/src/lib/server/services/auth.ts index 37436e9..1796798 100644 --- a/apps/web/src/lib/server/services/auth.ts +++ b/apps/web/src/lib/server/services/auth.ts @@ -480,18 +480,27 @@ const makeAuth = Effect.gen(function* () { ) ) ); - // The Autumn customer is the org; the free plan auto-attaches. - // A failure here is logged, not fatal: getOrCreate is retried - // on a later sign-in after the local transaction has committed. - yield* autumn - .ensureCustomer({ - customerId: orgId, - name: personalOrgFor(exchanged.user.email).name, - email: exchanged.user.email - }) - .pipe( + // Customer creation happens after the local sign-in commits. Use + // the stored org and a deterministic known owner, even when a member + // is the first to sign in after billing is enabled. Without an owner, + // defer creation until a later sign-in can supply that contact. + if (autumn.enabled) { + yield* Effect.gen(function* () { + const contacts = yield* sql<{ name: string; email: string }>` + SELECT o.name, u.email + FROM orgs o + JOIN memberships m ON m.org_id = o.id AND m.role = 'owner' + JOIN users u ON u.id = m.user_id + WHERE o.id = ${orgId} AND u.email <> '' + ORDER BY u.created_at, u.id + LIMIT 1`; + const contact = contacts[0]; + if (!contact) return; + yield* autumn.ensureCustomer({ customerId: orgId, ...contact }); + }).pipe( Effect.catchCause((cause) => Effect.sync(() => { + // A provider or lookup failure is retried on a later sign-in. console.error( JSON.stringify({ message: 'Autumn customer could not be created', @@ -502,6 +511,7 @@ const makeAuth = Effect.gen(function* () { }) ) ); + } // Pin the org on the session so every later request carries it. const pinned = exchanged.organizationId === orgId