diff --git a/.env.example b/.env.example index 646eadd..891c301 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,12 @@ PYMTHOUSE_ALLOW_INSECURE_HTTP= # PYMTHOUSE_SIGNER_URL=https://signer.pymthouse.com # Non-prod mint requires PYMTHOUSE_PUBLIC_CLIENT_ID=app_98575870d7ae33589a3f0660 +# Neon Postgres for MCP asset recents (media URL + gateway_request_id). +# Apply schema with `pnpm db:migrate` — the app does not run DDL at request time. +# Not OpenMeter. Do not commit the password. +# DATABASE_URL=postgresql://USER:PASSWORD@HOST/neondb?sslmode=require +# MCP_ASSETS_DATABASE_URL= + # Invite-only Console HTML (/home, /usage, /keys, /calls, /settings). # Empty = no gate. Comma-separated emails, matched case-insensitively. # CONSOLE_EMAIL_ALLOWLIST=alice@livepeer.org,bob@studio.com diff --git a/db/client.ts b/db/client.ts new file mode 100644 index 0000000..3c754b5 --- /dev/null +++ b/db/client.ts @@ -0,0 +1,45 @@ +import { neon, type NeonQueryFunction } from "@neondatabase/serverless"; +import { drizzle, type NeonHttpDatabase } from "drizzle-orm/neon-http"; +import * as schema from "./schema"; + +export type AssetDb = NeonHttpDatabase; + +let sqlClient: NeonQueryFunction | null = null; +let dbClient: AssetDb | null = null; + +export function databaseUrl(): string { + const raw = + process.env.MCP_ASSETS_DATABASE_URL?.trim() || + process.env.DATABASE_URL?.trim(); + if (!raw) { + throw new Error( + "DATABASE_URL (or MCP_ASSETS_DATABASE_URL) is required for MCP assets" + ); + } + const url = new URL(raw); + url.searchParams.delete("channel_binding"); + if (!url.searchParams.has("sslmode")) { + url.searchParams.set("sslmode", "require"); + } + return url.toString(); +} + +export function assetStoreConfigured(): boolean { + return Boolean( + process.env.MCP_ASSETS_DATABASE_URL?.trim() || + process.env.DATABASE_URL?.trim() + ); +} + +export function getAssetDb(): AssetDb { + if (!dbClient) { + sqlClient = neon(databaseUrl()); + dbClient = drizzle(sqlClient, { schema }); + } + return dbClient; +} + +export function resetAssetDbForTests(): void { + sqlClient = null; + dbClient = null; +} diff --git a/db/schema.ts b/db/schema.ts new file mode 100644 index 0000000..045a01e --- /dev/null +++ b/db/schema.ts @@ -0,0 +1,42 @@ +import { + index, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core"; + +/** MCP-produced media URLs, scoped per console/MCP principal (`eu_…`). */ +export const mcpAssets = pgTable( + "mcp_assets", + { + id: text("id").primaryKey(), + principalId: text("principal_id").notNull(), + url: text("url").notNull(), + capability: text("capability").notNull(), + gatewayRequestId: text("gateway_request_id").notNull(), + providerRequestId: text("provider_request_id"), + createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }) + .notNull() + .defaultNow(), + }, + (table) => [ + uniqueIndex("mcp_assets_principal_job_url_idx").on( + table.principalId, + table.gatewayRequestId, + table.url + ), + index("mcp_assets_principal_created_idx").on( + table.principalId, + table.createdAt + ), + index("mcp_assets_principal_capability_idx").on( + table.principalId, + table.capability + ), + index("mcp_assets_principal_gateway_idx").on( + table.principalId, + table.gatewayRequestId + ), + ] +); diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..0c79266 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./db/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: + process.env.MCP_ASSETS_DATABASE_URL?.trim() || + process.env.DATABASE_URL?.trim() || + "postgresql://unused:unused@localhost/unused", + }, +}); diff --git a/drizzle/0000_narrow_solo.sql b/drizzle/0000_narrow_solo.sql new file mode 100644 index 0000000..013dd4d --- /dev/null +++ b/drizzle/0000_narrow_solo.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS "mcp_assets" ( + "id" text PRIMARY KEY NOT NULL, + "principal_id" text NOT NULL, + "url" text NOT NULL, + "capability" text NOT NULL, + "gateway_request_id" text NOT NULL, + "provider_request_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "mcp_assets_principal_job_url_idx" ON "mcp_assets" USING btree ("principal_id","gateway_request_id","url");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "mcp_assets_principal_created_idx" ON "mcp_assets" USING btree ("principal_id","created_at");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "mcp_assets_principal_capability_idx" ON "mcp_assets" USING btree ("principal_id","capability");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "mcp_assets_principal_gateway_idx" ON "mcp_assets" USING btree ("principal_id","gateway_request_id"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..af61365 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,166 @@ +{ + "id": "b194958b-6d47-422b-a5ab-b0eec9a5d704", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.mcp_assets": { + "name": "mcp_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gateway_request_id": { + "name": "gateway_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_request_id": { + "name": "provider_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_assets_principal_job_url_idx": { + "name": "mcp_assets_principal_job_url_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_assets_principal_created_idx": { + "name": "mcp_assets_principal_created_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_assets_principal_capability_idx": { + "name": "mcp_assets_principal_capability_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_assets_principal_gateway_idx": { + "name": "mcp_assets_principal_gateway_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..b988ece --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1788576402765, + "tag": "0000_narrow_solo", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/lib/mcp/mcp-server.ts b/lib/mcp/mcp-server.ts index 6388fc6..a4f0809 100644 --- a/lib/mcp/mcp-server.ts +++ b/lib/mcp/mcp-server.ts @@ -13,7 +13,16 @@ import { } from "./run-capability"; import { fetchMcpUsage } from "./pymthouse-spend"; import { assertSpendable } from "./pymthouse-usage"; -import { forgetAssets, listAssets, rememberAsset } from "./store"; +import { + ASSET_STORE_UNAVAILABLE, + FORGET_IDS_OR_ALL_REQUIRED, + forgetAssets, + listAssets, + logAssetStoreError, + publicAssetStoreError, + rememberAsset, + serializeAsset, +} from "./store"; import { principalId } from "./log"; function text(data: unknown, isError = false) { @@ -162,29 +171,81 @@ export function buildRawMcpServer(principal: McpPrincipal): McpServer { server.registerTool( "get_recent_assets", { - description: "Assets produced in this isolate for the current principal.", - inputSchema: {}, + description: + "Recent media URLs for this principal, persisted in Postgres. Join to usage history with gateway_request_id. Newest first, default 20, max 50.", + inputSchema: { + limit: z.number().int().min(1).max(50).optional(), + capability: z.string().min(1).optional(), + gateway_request_id: z.string().min(1).optional(), + }, }, - async () => text({ assets: listAssets(pid) }) + async ({ limit, capability, gateway_request_id }) => { + try { + const assets = await listAssets(pid, { + limit, + capability, + gatewayRequestId: gateway_request_id, + }); + return text({ + assets: assets.map(serializeAsset), + count: assets.length, + }); + } catch (err) { + logAssetStoreError(err); + return text(publicAssetStoreError(), true); + } + } ); server.registerTool( "search_assets", { - description: "Search recent assets by capability or URL substring.", + description: + "Search this principal's persisted assets by capability, URL, or gateway_request_id substring.", inputSchema: { query: z.string().min(1) }, }, - async ({ query }) => text({ assets: listAssets(pid, query) }) + async ({ query }) => { + try { + const assets = await listAssets(pid, { query }); + return text({ + assets: assets.map(serializeAsset), + count: assets.length, + }); + } catch (err) { + logAssetStoreError(err); + return text(publicAssetStoreError(), true); + } + } ); server.registerTool( "forget_assets", { description: - "Drop remembered assets for this principal (this isolate only).", - inputSchema: { ids: z.array(z.string()).optional() }, + "Delete persisted assets for this principal. Pass ids, or all: true to drop every asset they own.", + inputSchema: { + ids: z.array(z.string()).optional(), + all: z.boolean().optional(), + }, }, - async ({ ids }) => text({ forgotten: forgetAssets(pid, ids) }) + async ({ ids, all }) => { + try { + return text({ forgotten: await forgetAssets(pid, { ids, all }) }); + } catch (err) { + if (err instanceof Error && err.name === FORGET_IDS_OR_ALL_REQUIRED) { + return text( + { + error: FORGET_IDS_OR_ALL_REQUIRED, + message: + "Pass ids, or all: true to delete every asset for this principal.", + }, + true + ); + } + logAssetStoreError(err); + return text(publicAssetStoreError(), true); + } + } ); server.registerTool( @@ -283,14 +344,21 @@ export function buildRawMcpServer(principal: McpPrincipal): McpServer { const urlRaw = result.url ?? result.imageUrl ?? result.videoUrl ?? result.audioUrl; const url = urlRaw && !isQueueControlUrl(urlRaw) ? urlRaw : null; + let persistError: string | null = null; if (url) { - rememberAsset(pid, { - id: newId("asset"), - url, - capability, - createdAt: new Date().toISOString(), - gatewayRequestId: result.gatewayRequestId, - }); + try { + await rememberAsset(pid, { + id: newId("asset"), + url, + capability, + createdAt: new Date().toISOString(), + gatewayRequestId: result.gatewayRequestId, + providerRequestId: result.providerRequestId, + }); + } catch (err) { + logAssetStoreError(err); + persistError = ASSET_STORE_UNAVAILABLE; + } } return text({ capability, @@ -302,6 +370,7 @@ export function buildRawMcpServer(principal: McpPrincipal): McpServer { orchestrator: result.orchestrator, elapsed_ms: result.elapsedMs, gateway_request_id: result.gatewayRequestId, + ...(persistError ? { persist_error: persistError } : {}), ...(url ? {} : { data: result.data }), }); } catch (err) { diff --git a/lib/mcp/store.test.ts b/lib/mcp/store.test.ts new file mode 100644 index 0000000..b015bd8 --- /dev/null +++ b/lib/mcp/store.test.ts @@ -0,0 +1,238 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + ASSET_STORE_UNAVAILABLE, + FORGET_IDS_OR_ALL_REQUIRED, + chunkIds, + forgetAssets, + likeSubstring, + listAssets, + listAssetsForGatewayRequestIds, + mapAssetRow, + publicAssetStoreError, + rememberAsset, + resetAssetStoreForTests, + serializeAsset, +} from "./store"; + +function neonConfigured(): boolean { + return Boolean( + process.env.DATABASE_URL?.trim() || + process.env.MCP_ASSETS_DATABASE_URL?.trim() + ); +} + +test("mapAssetRow exposes job ids for ticket joins", () => { + const asset = mapAssetRow({ + id: "asset_1", + url: "https://v3b.fal.media/files/x.jpg", + capability: "livepeer-example/fal-flux-schnell", + gatewayRequestId: "job_abc", + providerRequestId: "req-fal", + createdAt: "2026-09-05T01:00:00.000Z", + }); + assert.equal(asset.gatewayRequestId, "job_abc"); + assert.equal(asset.providerRequestId, "req-fal"); + assert.deepEqual(serializeAsset(asset), { + id: "asset_1", + url: "https://v3b.fal.media/files/x.jpg", + capability: "livepeer-example/fal-flux-schnell", + created_at: "2026-09-05T01:00:00.000Z", + gateway_request_id: "job_abc", + provider_request_id: "req-fal", + }); +}); + +test("likeSubstring escapes ILIKE metacharacters", () => { + assert.equal(likeSubstring("50%_off\\x"), "50\\%\\_off\\\\x"); +}); + +test("chunkIds keeps leftovers instead of truncating to 50", () => { + const ids = Array.from({ length: 130 }, (_, i) => `job_${i}`); + const chunks = chunkIds(ids, 100); + assert.equal(chunks.length, 2); + assert.equal(chunks[0]?.length, 100); + assert.equal(chunks[1]?.length, 30); + assert.equal(chunks.flat().length, 130); +}); + +test("publicAssetStoreError does not include connection details", () => { + const payload = publicAssetStoreError(); + assert.equal(payload.error, ASSET_STORE_UNAVAILABLE); + assert.equal(payload.message.includes("postgresql://"), false); + assert.equal(JSON.stringify(payload).includes("password"), false); +}); + +test("forgetAssets without ids or all throws a stable code", async (t) => { + if (!neonConfigured()) { + t.skip("DATABASE_URL not set"); + return; + } + await assert.rejects( + () => forgetAssets(`eu_test_${Date.now()}`), + (err: unknown) => + err instanceof Error && err.name === FORGET_IDS_OR_ALL_REQUIRED + ); +}); + +test("remember / list / forget persist against Neon when DATABASE_URL is set", async (t) => { + if (!neonConfigured()) { + t.skip("DATABASE_URL not set"); + return; + } + const principalId = `eu_test_${Date.now()}`; + const jobId = `job_test_${Date.now()}`; + const remembered = await rememberAsset(principalId, { + id: `asset_test_${Date.now()}`, + url: `https://example.test/${jobId}.jpg`, + capability: "livepeer-example/fal-flux-schnell", + createdAt: new Date().toISOString(), + gatewayRequestId: jobId, + providerRequestId: "req-test", + }); + assert.equal(remembered.gatewayRequestId, jobId); + + const listed = await listAssets(principalId, { gatewayRequestId: jobId }); + assert.equal(listed.length, 1); + assert.equal(listed[0]?.url, remembered.url); + + const forgotten = await forgetAssets(principalId, { ids: [remembered.id] }); + assert.equal(forgotten, 1); + assert.deepEqual(await listAssets(principalId, { gatewayRequestId: jobId }), []); +}); + +test("listAssetsForGatewayRequestIds returns newest URL per job", async (t) => { + if (!neonConfigured()) { + t.skip("DATABASE_URL not set"); + return; + } + const stamp = Date.now(); + const principalId = `eu_join_${stamp}`; + const jobA = `job_a_${stamp}`; + const jobB = `job_b_${stamp}`; + await rememberAsset(principalId, { + id: `asset_a1_${stamp}`, + url: `https://example.test/a1/${jobA}.jpg`, + capability: "livepeer-example/fal-flux-schnell", + createdAt: new Date().toISOString(), + gatewayRequestId: jobA, + }); + await rememberAsset(principalId, { + id: `asset_b_${stamp}`, + url: `https://example.test/b/${jobB}.jpg`, + capability: "livepeer-example/fal-flux-schnell", + createdAt: new Date().toISOString(), + gatewayRequestId: jobB, + }); + const listed = await listAssetsForGatewayRequestIds(principalId, [ + jobA, + jobB, + "job_missing", + ]); + const urls = new Map(listed.map((asset) => [asset.gatewayRequestId, asset.url])); + assert.equal(urls.get(jobA)?.includes(jobA), true); + assert.equal(urls.get(jobB)?.includes(jobB), true); + await forgetAssets(principalId, { all: true }); +}); + +test("conflict on the same job URL does not steal another principal", async (t) => { + if (!neonConfigured()) { + t.skip("DATABASE_URL not set"); + return; + } + const stamp = Date.now(); + const jobId = `job_conflict_${stamp}`; + const url = `https://example.test/conflict/${jobId}.jpg`; + const owner = `eu_owner_${stamp}`; + const other = `eu_other_${stamp}`; + const ownerAsset = await rememberAsset(owner, { + id: `asset_owner_${stamp}`, + url, + capability: "livepeer-example/fal-flux-schnell", + createdAt: new Date().toISOString(), + gatewayRequestId: jobId, + providerRequestId: "req-owner", + }); + const otherAsset = await rememberAsset(other, { + id: `asset_other_${stamp}`, + url, + capability: "livepeer-example/fal-flux-schnell", + createdAt: new Date().toISOString(), + gatewayRequestId: jobId, + providerRequestId: "req-other", + }); + assert.notEqual(ownerAsset.id, otherAsset.id); + + const ownerListed = await listAssets(owner, { gatewayRequestId: jobId }); + const otherListed = await listAssets(other, { gatewayRequestId: jobId }); + assert.equal(ownerListed.length, 1); + assert.equal(ownerListed[0]?.id, ownerAsset.id); + assert.equal(otherListed.length, 1); + assert.equal(otherListed[0]?.id, otherAsset.id); + + assert.equal(await forgetAssets(owner, { ids: [otherAsset.id] }), 0); + assert.equal((await listAssets(other, { gatewayRequestId: jobId })).length, 1); + + assert.equal(await forgetAssets(owner, { all: true }), 1); + assert.equal((await listAssets(owner, { gatewayRequestId: jobId })).length, 0); + assert.equal((await listAssets(other, { gatewayRequestId: jobId })).length, 1); + await forgetAssets(other, { all: true }); +}); + +test("ILIKE search treats percent and underscore as literals", async (t) => { + if (!neonConfigured()) { + t.skip("DATABASE_URL not set"); + return; + } + const stamp = Date.now(); + const principalId = `eulike${stamp}`; + const jobId = `joblike${stamp}`; + await rememberAsset(principalId, { + id: `assetlike${stamp}`, + url: `https://example.test/plain/${jobId}.jpg`, + capability: "livepeer-example/fal-flux-schnell", + createdAt: new Date().toISOString(), + gatewayRequestId: jobId, + }); + assert.equal((await listAssets(principalId, { query: "%" })).length, 0); + assert.equal((await listAssets(principalId, { query: "_" })).length, 0); + await forgetAssets(principalId, { all: true }); +}); + +test("remember fails closed when the database URL is unreachable", async (t) => { + if (!neonConfigured()) { + t.skip("DATABASE_URL not set"); + return; + } + const original = process.env.DATABASE_URL; + const originalMcp = process.env.MCP_ASSETS_DATABASE_URL; + process.env.MCP_ASSETS_DATABASE_URL = ""; + process.env.DATABASE_URL = + "postgresql://neondb_owner:wrong@no-such-host.invalid/neondb?sslmode=require"; + resetAssetStoreForTests(); + await assert.rejects(() => + rememberAsset("eu_retry", { + id: "asset_retry", + url: "https://example.test/retry.jpg", + capability: "x", + createdAt: new Date().toISOString(), + gatewayRequestId: "job_retry", + }) + ); + if (original) process.env.DATABASE_URL = original; + else delete process.env.DATABASE_URL; + if (originalMcp) process.env.MCP_ASSETS_DATABASE_URL = originalMcp; + else delete process.env.MCP_ASSETS_DATABASE_URL; + resetAssetStoreForTests(); + const principalId = `eu_retry_${Date.now()}`; + const remembered = await rememberAsset(principalId, { + id: `asset_retry_${Date.now()}`, + url: `https://example.test/retry/${Date.now()}.jpg`, + capability: "livepeer-example/fal-flux-schnell", + createdAt: new Date().toISOString(), + gatewayRequestId: `job_retry_${Date.now()}`, + }); + assert.ok(remembered.id); + await forgetAssets(principalId, { all: true }); +}); diff --git a/lib/mcp/store.ts b/lib/mcp/store.ts index b664f6d..1b4c4a3 100644 --- a/lib/mcp/store.ts +++ b/lib/mcp/store.ts @@ -1,3 +1,11 @@ +import { and, desc, eq, gte, inArray, lte, or, sql } from "drizzle-orm"; +import { mcpAssets } from "@/db/schema"; +import { + assetStoreConfigured, + getAssetDb, + resetAssetDbForTests, +} from "@/db/client"; + export type Asset = { id: string; url: string; @@ -5,34 +13,256 @@ export type Asset = { createdAt: string; /** Joins this asset to its ticket rows in PymtHouse metering. */ gatewayRequestId: string; + providerRequestId?: string | null; +}; + +export type ListAssetsInput = { + query?: string; + capability?: string; + gatewayRequestId?: string; + limit?: number; + createdFrom?: Date; + createdTo?: Date; +}; + +export type ForgetAssetsInput = { + ids?: string[]; + all?: boolean; }; -const assets = new Map(); +export const ASSET_STORE_UNAVAILABLE = "asset_store_unavailable"; +export const FORGET_IDS_OR_ALL_REQUIRED = "ids_or_all_required"; + +const DEFAULT_LIMIT = 20; +const MAX_LIMIT = 50; +/** Neon `ANY` / `IN` chunks — never drop leftover IDs. */ +export const GATEWAY_ID_QUERY_CHUNK = 100; + +export { assetStoreConfigured, resetAssetDbForTests as resetAssetStoreForTests }; + +export function chunkIds( + ids: string[], + size = GATEWAY_ID_QUERY_CHUNK +): string[][] { + const unique = [ + ...new Set(ids.map((id) => id.trim()).filter((id) => id.length > 0)), + ]; + const chunks: string[][] = []; + for (let i = 0; i < unique.length; i += size) { + chunks.push(unique.slice(i, i + size)); + } + return chunks; +} + +function clampLimit(limit?: number): number { + if (limit == null || !Number.isFinite(limit)) return DEFAULT_LIMIT; + return Math.min(MAX_LIMIT, Math.max(1, Math.trunc(limit))); +} + +function asIso(value: Date | string): string { + if (value instanceof Date) return value.toISOString(); + const parsed = new Date(value); + return Number.isNaN(parsed.getTime()) ? String(value) : parsed.toISOString(); +} + +/** Escape `%`, `_`, and `\` so ILIKE is a literal substring match. */ +export function likeSubstring(query: string): string { + return query.replace(/[\\%_]/g, (ch) => `\\${ch}`); +} -export function rememberAsset(principalId: string, asset: Asset): void { - const list = assets.get(principalId) ?? []; - list.unshift(asset); - assets.set(principalId, list.slice(0, 50)); +export function publicAssetStoreError(): { + error: string; + message: string; +} { + return { + error: ASSET_STORE_UNAVAILABLE, + message: "Could not access persisted assets.", + }; } -export function listAssets(principalId: string, query?: string): Asset[] { - const list = assets.get(principalId) ?? []; - if (!query?.trim()) return list; - const q = query.toLowerCase(); - return list.filter( - (a) => a.capability.toLowerCase().includes(q) || a.url.toLowerCase().includes(q) +export function logAssetStoreError(err: unknown): void { + const rec = + err && typeof err === "object" + ? (err as { name?: unknown; code?: unknown }) + : null; + console.error( + JSON.stringify({ + msg: "mcp.assets", + name: rec && typeof rec.name === "string" ? rec.name : "Error", + code: rec && typeof rec.code === "string" ? rec.code : undefined, + }) ); } -export function forgetAssets(principalId: string, ids?: string[]): number { - if (!ids?.length) { - const n = (assets.get(principalId) ?? []).length; - assets.delete(principalId); - return n; +export function mapAssetRow(row: { + id: string; + url: string; + capability: string; + gatewayRequestId: string; + providerRequestId: string | null; + createdAt: Date | string; +}): Asset { + return { + id: row.id, + url: row.url, + capability: row.capability, + createdAt: asIso(row.createdAt), + gatewayRequestId: row.gatewayRequestId, + providerRequestId: row.providerRequestId, + }; +} + +export function serializeAsset(asset: Asset) { + return { + id: asset.id, + url: asset.url, + capability: asset.capability, + created_at: asset.createdAt, + gateway_request_id: asset.gatewayRequestId, + provider_request_id: asset.providerRequestId ?? null, + }; +} + +export async function rememberAsset( + principalId: string, + asset: Asset +): Promise { + const db = getAssetDb(); + const rows = await db + .insert(mcpAssets) + .values({ + id: asset.id, + principalId, + url: asset.url, + capability: asset.capability, + gatewayRequestId: asset.gatewayRequestId, + providerRequestId: asset.providerRequestId ?? null, + }) + .onConflictDoUpdate({ + target: [ + mcpAssets.principalId, + mcpAssets.gatewayRequestId, + mcpAssets.url, + ], + set: { + capability: sql`excluded.capability`, + providerRequestId: sql`coalesce(excluded.provider_request_id, ${mcpAssets.providerRequestId})`, + }, + }) + .returning(); + const row = rows[0]; + if (!row) { + throw new Error("mcp_assets insert returned no row"); + } + return mapAssetRow(row); +} + +export async function listAssets( + principalId: string, + input: ListAssetsInput = {} +): Promise { + const db = getAssetDb(); + const query = input.query?.trim() ? likeSubstring(input.query.trim()) : null; + const capability = input.capability?.trim() || null; + const gatewayRequestId = input.gatewayRequestId?.trim() || null; + const limit = clampLimit(input.limit); + const filters = [eq(mcpAssets.principalId, principalId)]; + if (capability) filters.push(eq(mcpAssets.capability, capability)); + if (gatewayRequestId) { + filters.push(eq(mcpAssets.gatewayRequestId, gatewayRequestId)); + } + if (input.createdFrom) { + filters.push(gte(mcpAssets.createdAt, input.createdFrom)); + } + if (input.createdTo) { + filters.push(lte(mcpAssets.createdAt, input.createdTo)); + } + if (query) { + const pattern = `%${query}%`; + filters.push( + or( + sql`${mcpAssets.capability} ILIKE ${pattern} ESCAPE '\\'`, + sql`${mcpAssets.url} ILIKE ${pattern} ESCAPE '\\'`, + sql`${mcpAssets.gatewayRequestId} ILIKE ${pattern} ESCAPE '\\'` + )! + ); + } + const rows = await db + .select() + .from(mcpAssets) + .where(and(...filters)) + .orderBy(desc(mcpAssets.createdAt)) + .limit(limit); + return rows.map(mapAssetRow); +} + +export async function listAssetsForGatewayRequestIds( + principalId: string, + gatewayRequestIds: string[] +): Promise { + const chunks = chunkIds(gatewayRequestIds); + if (chunks.length === 0) return []; + const db = getAssetDb(); + const batches = await Promise.all( + chunks.map((ids) => + db + .select() + .from(mcpAssets) + .where( + and( + eq(mcpAssets.principalId, principalId), + inArray(mcpAssets.gatewayRequestId, ids) + ) + ) + .orderBy(desc(mcpAssets.createdAt)) + ) + ); + return batches.flat().map(mapAssetRow); +} + +export async function listAssetsInCreatedRange( + principalId: string, + createdFrom: Date, + createdTo: Date +): Promise { + const db = getAssetDb(); + const rows = await db + .select() + .from(mcpAssets) + .where( + and( + eq(mcpAssets.principalId, principalId), + gte(mcpAssets.createdAt, createdFrom), + lte(mcpAssets.createdAt, createdTo) + ) + ) + .orderBy(desc(mcpAssets.createdAt)); + return rows.map(mapAssetRow); +} + +export async function forgetAssets( + principalId: string, + input: ForgetAssetsInput = {} +): Promise { + const db = getAssetDb(); + const ids = input.ids?.filter((id) => id.trim()) ?? []; + if (input.all === true) { + const rows = await db + .delete(mcpAssets) + .where(eq(mcpAssets.principalId, principalId)) + .returning({ id: mcpAssets.id }); + return rows.length; + } + if (ids.length === 0) { + const err = new Error(FORGET_IDS_OR_ALL_REQUIRED); + err.name = FORGET_IDS_OR_ALL_REQUIRED; + throw err; } - const set = new Set(ids); - const list = (assets.get(principalId) ?? []).filter((a) => !set.has(a.id)); - const removed = (assets.get(principalId) ?? []).length - list.length; - assets.set(principalId, list); - return removed; + const rows = await db + .delete(mcpAssets) + .where( + and(eq(mcpAssets.principalId, principalId), inArray(mcpAssets.id, ids)) + ) + .returning({ id: mcpAssets.id }); + return rows.length; } diff --git a/package.json b/package.json index 7b133cd..346f209 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,17 @@ "lint": "eslint . --max-warnings 0", "typecheck": "tsc --noEmit --incremental false", "format": "prettier . --write", - "format:check": "prettier . --check" + "format:check": "prettier . --check", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate" }, "dependencies": { "@auth0/nextjs-auth0": "^4.27.0", "@modelcontextprotocol/sdk": "^1.30.0", + "@neondatabase/serverless": "^1.1.0", "@pymthouse/builder-sdk": "^0.6.5", - "@pymthouse/gateway-web": "0.3.2", + "@pymthouse/gateway-web": "0.3.3", + "drizzle-orm": "^0.45.2", "framer-motion": "^11.15.0", "geist": "^1.7.0", "jose": "^6.2.10", @@ -37,6 +41,7 @@ "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "drizzle-kit": "^0.31.10", "eslint": "^9", "eslint-config-next": "^15.1.0", "eslint-config-prettier": "^10.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f594c46..67b8b33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,18 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.30.0 version: 1.30.0(zod@3.25.76) + '@neondatabase/serverless': + specifier: ^1.1.0 + version: 1.1.0 '@pymthouse/builder-sdk': specifier: ^0.6.5 version: 0.6.5 '@pymthouse/gateway-web': - specifier: 0.3.2 - version: 0.3.2 + specifier: 0.3.3 + version: 0.3.3 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@neondatabase/serverless@1.1.0) framer-motion: specifier: ^11.15.0 version: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -75,6 +81,9 @@ importers: '@types/react-dom': specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.14) + drizzle-kit: + specifier: ^0.31.10 + version: 0.31.10 eslint: specifier: ^9 version: 9.39.4(jiti@2.6.1) @@ -110,6 +119,9 @@ packages: react: ^18.0.0 || ~19.0.1 || ~19.1.2 || ^19.2.1 react-dom: ^18.0.0 || ~19.0.1 || ~19.1.2 || ^19.2.1 + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@edge-runtime/cookies@5.0.2': resolution: {integrity: sha512-Sd8LcWpZk/SWEeKGE8LT6gMm5MGfX/wm+GPnh1eBEtCpya3vYqn37wYknwAHw92ONoyyREl1hJwxV/Qx2DWNOg==} engines: {node: '>=16'} @@ -123,6 +135,458 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -365,6 +829,10 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@neondatabase/serverless@1.1.0': + resolution: {integrity: sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q==} + engines: {node: '>=19.0.0'} + '@next/env@15.5.14': resolution: {integrity: sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==} @@ -446,8 +914,8 @@ packages: resolution: {integrity: sha512-oWQC3y7vTqOKG+fEaZRc7TtbIMJaLZZoS6kCqBzF5M4489wGS31LlE2gwhYDEuVNAeyoJwaK/Uu+mfEe8TwGFw==} engines: {node: '>=20'} - '@pymthouse/gateway-web@0.3.2': - resolution: {integrity: sha512-7G66ezZ8ntOFf7S6L9RWK+qSUrEjB5MF74UchcKqgwMYs8RcQJm1wxEoQF1R5ntr7BzSSkjnlqt5khHZflAqQA==} + '@pymthouse/gateway-web@0.3.3': + resolution: {integrity: sha512-eZ9AHvgAMxZmPC2Hki0n2y3Sl8FO8oCOQD7BsR+DxhZsdovl3abU+3KNA0+rToXDKurdgpozeQKh98ekY4zqsQ==} engines: {node: '>=20'} '@reduxjs/toolkit@2.11.2': @@ -895,6 +1363,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1076,6 +1547,102 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + drizzle-kit@0.31.10: + resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} + hasBin: true + + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1129,6 +1696,21 @@ packages: es-toolkit@1.45.1: resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -1375,6 +1957,11 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -2204,6 +2791,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -2306,6 +2900,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2427,6 +3026,8 @@ snapshots: react-dom: 19.2.4(react@19.2.4) swr: 2.5.1(react@19.2.4) + '@drizzle-team/brocli@0.10.2': {} + '@edge-runtime/cookies@5.0.2': {} '@emnapi/core@1.9.2': @@ -2445,6 +3046,238 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.7 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -2651,6 +3484,8 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@neondatabase/serverless@1.1.0': {} + '@next/env@15.5.14': {} '@next/eslint-plugin-next@15.5.14': @@ -2701,7 +3536,7 @@ snapshots: dependencies: oauth4webapi: 3.8.7 - '@pymthouse/gateway-web@0.3.2': + '@pymthouse/gateway-web@0.3.3': dependencies: undici: 7.29.0 @@ -3144,6 +3979,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-from@1.1.2: {} + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -3299,6 +4136,17 @@ snapshots: dependencies: esutils: 2.0.3 + drizzle-kit@0.31.10: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.23.13 + + drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0): + optionalDependencies: + '@neondatabase/serverless': 1.1.0 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3420,6 +4268,89 @@ snapshots: es-toolkit@1.45.1: {} + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} @@ -3758,6 +4689,9 @@ snapshots: fresh@2.0.0: {} + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -4632,6 +5566,13 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + stable-hash@0.0.5: {} statuses@2.0.2: {} @@ -4742,6 +5683,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1