diff --git a/typescript/packages/extensions/drizzle.config.ts b/typescript/packages/extensions/drizzle.config.ts new file mode 100644 index 0000000000..8adaa407ae --- /dev/null +++ b/typescript/packages/extensions/drizzle.config.ts @@ -0,0 +1,19 @@ +/** + * Drizzle Kit config for bazaar Postgres schema introspection / generation. + * + * The schema is shipped along with hand-written SQL migrations in + * `src/bazaar/facilitator-service/postgres/migrations`. Use `pnpm db:generate` + * to materialize new diffs from `schema.ts` (you may need to merge the output + * into `0001_init.sql` if you're adjusting the generated tsvector column). + */ + +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/bazaar/facilitator-service/postgres/schema.ts", + out: "./src/bazaar/facilitator-service/postgres/migrations", + dialect: "postgresql", + dbCredentials: { + url: process.env.BAZAAR_DATABASE_URL ?? "postgres://localhost:5432/postgres", + }, +}); diff --git a/typescript/packages/extensions/package.json b/typescript/packages/extensions/package.json index 91c898c6f8..9e5964463a 100644 --- a/typescript/packages/extensions/package.json +++ b/typescript/packages/extensions/package.json @@ -6,14 +6,17 @@ "types": "./dist/cjs/index.d.ts", "scripts": { "start": "tsx --env-file=.env index.ts", - "build": "tsup", + "build": "tsup && node -e \"const{cpSync}=require('fs');cpSync('src/bazaar/facilitator-service/postgres/migrations','dist/esm/bazaar/postgres/migrations',{recursive:true});cpSync('src/bazaar/facilitator-service/postgres/migrations','dist/cjs/bazaar/postgres/migrations',{recursive:true})\"", "test": "vitest run", + "test:db": "RUN_DB_TESTS=1 vitest run", "test:watch": "vitest", "watch": "tsc --watch", "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", "lint": "eslint . --ext .ts --fix", - "lint:check": "eslint . --ext .ts" + "lint:check": "eslint . --ext .ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate" }, "keywords": [ "x402", @@ -27,13 +30,17 @@ "description": "x402 Payment Protocol Extensions", "devDependencies": { "@eslint/js": "^9.24.0", + "@testcontainers/postgresql": "^10.16.0", "@types/node": "^22.13.4", + "@types/pg": "^8.11.10", "@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/parser": "^8.29.1", + "drizzle-kit": "^0.30.0", "eslint": "^9.24.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.6.9", "eslint-plugin-prettier": "^5.2.6", + "pg": "^8.13.1", "prettier": "3.5.2", "tsup": "^8.4.0", "tsx": "^4.19.2", @@ -47,12 +54,25 @@ "@scure/base": "^1.2.6", "@x402/core": "workspace:~", "ajv": "^8.17.1", + "drizzle-orm": "^0.36.4", "jose": "^5.9.6", "@signinwithethereum/siwe": "^4.1.0", "tweetnacl": "^1.0.3", "viem": "^2.48.11", "zod": "^3.24.2" }, + "peerDependencies": { + "pg": "^8.13.0", + "@neondatabase/serverless": "^0.10.0" + }, + "peerDependenciesMeta": { + "pg": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + } + }, "exports": { ".": { "import": { @@ -74,6 +94,16 @@ "default": "./dist/cjs/bazaar/index.js" } }, + "./bazaar/postgres": { + "import": { + "types": "./dist/esm/bazaar/postgres/index.d.mts", + "default": "./dist/esm/bazaar/postgres/index.mjs" + }, + "require": { + "types": "./dist/cjs/bazaar/postgres/index.d.ts", + "default": "./dist/cjs/bazaar/postgres/index.js" + } + }, "./sign-in-with-x": { "import": { "types": "./dist/esm/sign-in-with-x/index.d.mts", diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/catalog.ts b/typescript/packages/extensions/src/bazaar/facilitator-service/catalog.ts new file mode 100644 index 0000000000..f9f881ef7e --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/catalog.ts @@ -0,0 +1,63 @@ +/** + * BazaarCatalog: storage interface for cataloging discovered x402 resources. + * + * Facilitators receive `bazaar` discovery extensions inside payment payloads. + * `installBazaarFacilitator` extracts each discovery via `extractDiscoveryInfo` + * and forwards the result here. The interface is intentionally narrow so it can + * be implemented against an in-memory Map, Postgres, Redis, or anything else. + * + * The list/search response shapes match the spec's facilitator discovery API + * (`GET /discovery/resources`, `GET /discovery/search`); see + * `specs/extensions/bazaar.md` "Optional Discovery Endpoints". + */ + +import type { PaymentRequirements } from "@x402/core/types"; +import type { DiscoveredResource } from "../facilitator"; +import type { + ListDiscoveryResourcesParams, + SearchDiscoveryResourcesParams, + DiscoveryResource, + DiscoveryResourcesResponse, + SearchDiscoveryResourcesResponse, +} from "../facilitatorClient"; + +export type { + ListDiscoveryResourcesParams, + SearchDiscoveryResourcesParams, + DiscoveryResource, + DiscoveryResourcesResponse, + SearchDiscoveryResourcesResponse, +}; + +/** + * Input to `BazaarCatalog.upsert`. The catalog is responsible for merging this + * single observation into any existing row for the same canonical resource. + * + * Note: at verify time the facilitator only sees the **one** PaymentRequirements + * the client selected — not the full accepts[] from the original 402. Catalog + * implementations must merge `paymentRequirements` into the row's accepts list + * (deduped by `(scheme, network)`) so the catalog converges on the full set + * across many payments. + */ +export interface CatalogUpsertInput { + /** Extracted discovery resource as produced by `extractDiscoveryInfo`. */ + discovered: DiscoveredResource; + /** The single PaymentRequirements selected for this payment. */ + paymentRequirements: PaymentRequirements; + /** Optional extra extensions from the payment payload, stored alongside the row. */ + extensions?: Record; +} + +export interface BazaarCatalog { + /** + * Insert or update a row for the canonical resource. Must merge accepts + * (deduped by scheme+network) and update lastSeenAt on upsert. Idempotent. + */ + upsert(input: CatalogUpsertInput): Promise; + + /** List cataloged resources with optional filters and offset pagination. */ + list(params?: ListDiscoveryResourcesParams): Promise; + + /** Search cataloged resources by natural-language query. Cursor pagination is optional. */ + search(params: SearchDiscoveryResourcesParams): Promise; +} diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/index.ts b/typescript/packages/extensions/src/bazaar/facilitator-service/index.ts new file mode 100644 index 0000000000..134511322b --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/index.ts @@ -0,0 +1,30 @@ +/** + * Bazaar Facilitator Service — server-side helpers for cataloging x402 discoveries. + * + * The SDK exports here let a facilitator host the discovery endpoints required + * by the bazaar spec. The flow is: + * + * 1. Pick or implement a `BazaarCatalog` (see `InMemoryBazaarCatalog` for a + * reference / test implementation, or `./postgres` for a Postgres-backed one). + * 2. Call `installBazaarFacilitator(facilitator, catalog)` to register the + * `bazaar` extension and the post-verify cataloging hook. + * 3. Wire `catalog.list(...)` / `catalog.search(...)` behind your HTTP routes + * at `GET /discovery/resources` and `GET /discovery/search`. + */ + +export type { + BazaarCatalog, + CatalogUpsertInput, + DiscoveryResource, + DiscoveryResourcesResponse, + SearchDiscoveryResourcesResponse, + ListDiscoveryResourcesParams, + SearchDiscoveryResourcesParams, +} from "./catalog"; + +export { + installBazaarFacilitator, + type InstallBazaarFacilitatorOptions, +} from "./installFacilitator"; + +export { InMemoryBazaarCatalog } from "./memoryCatalog"; diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/installFacilitator.ts b/typescript/packages/extensions/src/bazaar/facilitator-service/installFacilitator.ts new file mode 100644 index 0000000000..3741d601c6 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/installFacilitator.ts @@ -0,0 +1,76 @@ +/** + * One-call wiring of the `bazaar` extension into an x402Facilitator. + * + * Registers the `bazaar` marker (so `/supported` advertises it) and installs an + * `onAfterVerify` hook that extracts the discovery info from each verified + * payment and forwards it to the supplied `BazaarCatalog`. + * + * The hook is best-effort: any catalog error is logged but never propagated, + * because cataloging is purely observational and must not break payment + * verification. + */ + +import type { x402Facilitator } from "@x402/core/facilitator"; +import { BAZAAR } from "../types"; +import { extractDiscoveryInfo } from "../facilitator"; +import type { BazaarCatalog } from "./catalog"; + +export interface InstallBazaarFacilitatorOptions { + /** + * Called when the catalog upsert throws. Defaults to `console.warn`. + * Provide your own logger to integrate with the facilitator's existing + * observability stack. + */ + onError?: (error: unknown) => void; +} + +/** + * Registers the bazaar extension marker and installs the cataloging hook. + * + * @param facilitator - The x402Facilitator instance to wire into. + * @param catalog - The BazaarCatalog implementation that will receive discoveries. + * @param options - Optional overrides (custom error handler). + * @returns The same facilitator for chaining. + * + * @example + * ```typescript + * import { x402Facilitator } from "@x402/core/facilitator"; + * import { installBazaarFacilitator, InMemoryBazaarCatalog } from "@x402/extensions"; + * + * const facilitator = new x402Facilitator(); + * // ... register schemes ... + * installBazaarFacilitator(facilitator, new InMemoryBazaarCatalog()); + * ``` + */ +export function installBazaarFacilitator( + facilitator: x402Facilitator, + catalog: BazaarCatalog, + options: InstallBazaarFacilitatorOptions = {}, +): x402Facilitator { + const onError = + options.onError ?? + ((err: unknown) => { + console.warn(`[bazaar] catalog upsert failed: ${err instanceof Error ? err.message : err}`); + }); + + return facilitator.registerExtension(BAZAAR).onAfterVerify(async context => { + let discovered; + try { + discovered = extractDiscoveryInfo(context.paymentPayload, context.requirements); + } catch (err) { + onError(err); + return; + } + if (!discovered) return; + + try { + await catalog.upsert({ + discovered, + paymentRequirements: context.requirements, + extensions: context.paymentPayload.extensions as Record | undefined, + }); + } catch (err) { + onError(err); + } + }); +} diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/memoryCatalog.ts b/typescript/packages/extensions/src/bazaar/facilitator-service/memoryCatalog.ts new file mode 100644 index 0000000000..dc1b0f42af --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/memoryCatalog.ts @@ -0,0 +1,279 @@ +/** + * In-memory `BazaarCatalog` implementation. + * + * Suitable for tests, examples, and local development. Production deployments + * should swap in a persistent implementation (see `./postgres`). + */ + +import type { PaymentRequirements } from "@x402/core/types"; +import type { DiscoveryInfo } from "../types"; +import type { DiscoveredHTTPResource } from "../http/types"; +import type { DiscoveredMCPResource } from "../mcp/types"; +import type { + BazaarCatalog, + CatalogUpsertInput, + DiscoveryResource, + DiscoveryResourcesResponse, + ListDiscoveryResourcesParams, + SearchDiscoveryResourcesParams, + SearchDiscoveryResourcesResponse, +} from "./catalog"; + +interface StoredRow { + /** Composite key: `${resourceUrl}::${method ?? "tool=" + toolName}`. Used internally for the Map. */ + key: string; + resourceUrl: string; + type: "http" | "mcp"; + method?: string; + toolName?: string; + routeTemplate?: string; + description?: string; + mimeType?: string; + serviceName?: string; + tags?: string[]; + iconUrl?: string; + x402Version: number; + discoveryInfo: DiscoveryInfo; + accepts: PaymentRequirements[]; + extensions?: Record; + firstSeenAt: Date; + lastSeenAt: Date; +} + +/** + * Discriminates MCP resources from HTTP resources by inspecting `input.type`. + * + * @param discovered - The extracted discovery resource to test. + * @returns True if `discovered` is an MCP resource (narrows the type accordingly). + */ +function isMCP( + discovered: DiscoveredHTTPResource | DiscoveredMCPResource, +): discovered is DiscoveredMCPResource { + return discovered.discoveryInfo.input.type === "mcp"; +} + +/** + * Computes the catalog key used to dedupe rows. HTTP rows are keyed by + * `(resourceUrl, method)`; MCP rows by `(resourceUrl, toolName)`. + * + * @param discovered - The extracted discovery resource. + * @returns The composite key string. + */ +function buildKey(discovered: DiscoveredHTTPResource | DiscoveredMCPResource): string { + if (isMCP(discovered)) { + return `${discovered.resourceUrl}::tool=${discovered.toolName}`; + } + return `${discovered.resourceUrl}::${discovered.method ?? "GET"}`; +} + +/** + * Merges `incoming` into `existing`, deduping by (scheme, network). + * Mutates and returns `existing` for caller convenience. + * + * @param existing - The accumulated accepts array on the stored row. + * @param incoming - The single PaymentRequirements observed in this payment. + * @returns The mutated `existing` array. + */ +function mergeAccepts( + existing: PaymentRequirements[], + incoming: PaymentRequirements, +): PaymentRequirements[] { + const existingIdx = existing.findIndex( + a => a.scheme === incoming.scheme && a.network === incoming.network, + ); + if (existingIdx === -1) { + existing.push(incoming); + } else { + existing[existingIdx] = incoming; + } + return existing; +} + +/** + * Projects an internal `StoredRow` into the wire-format `DiscoveryResource`. + * + * @param row - The stored row. + * @returns The wire-format discovery resource. + */ +function toDiscoveryResource(row: StoredRow): DiscoveryResource { + return { + resource: row.resourceUrl, + type: row.type, + x402Version: row.x402Version, + accepts: row.accepts, + lastUpdated: row.lastSeenAt.toISOString(), + extensions: row.extensions, + }; +} + +/** + * Trivial substring scoring: serviceName (3) > description (2) > tags (1). + * Intentionally simple — the real ranking lives in the Postgres tsvector path. + * + * @param row - The stored row to score. + * @param query - The user-supplied search query. + * @returns A non-negative integer score; 0 indicates no match. + */ +function searchScore(row: StoredRow, query: string): number { + const q = query.toLowerCase(); + if (!q) return 0; + let score = 0; + if (row.serviceName && row.serviceName.toLowerCase().includes(q)) score += 3; + if (row.description && row.description.toLowerCase().includes(q)) score += 2; + if (row.tags?.some(t => t.toLowerCase().includes(q))) score += 1; + return score; +} + +/** + * Map-backed `BazaarCatalog` for tests, examples, and local development. + * Not durable: state is lost when the process exits. + */ +export class InMemoryBazaarCatalog implements BazaarCatalog { + private readonly rows = new Map(); + + /** + * Visible for testing — current row count. + * + * @returns The number of distinct cataloged resources. + */ + get size(): number { + return this.rows.size; + } + + /** Visible for testing. */ + clear(): void { + this.rows.clear(); + } + + /** + * Insert or update the row for `input.discovered`. Merges `paymentRequirements` + * into the row's accepts list (deduped by scheme+network) and updates lastSeenAt. + * + * @param input - The upsert payload assembled by `installBazaarFacilitator`. + * @returns A promise that resolves once the in-memory map is updated. + */ + async upsert(input: CatalogUpsertInput): Promise { + const { discovered, paymentRequirements, extensions } = input; + const key = buildKey(discovered); + const now = new Date(); + + const existing = this.rows.get(key); + if (existing) { + mergeAccepts(existing.accepts, paymentRequirements); + existing.lastSeenAt = now; + // Refresh metadata from the latest payment so renamed services / updated descriptions propagate. + existing.description = discovered.description ?? existing.description; + existing.mimeType = discovered.mimeType ?? existing.mimeType; + existing.serviceName = discovered.serviceName ?? existing.serviceName; + existing.tags = discovered.tags ?? existing.tags; + existing.iconUrl = discovered.iconUrl ?? existing.iconUrl; + existing.discoveryInfo = discovered.discoveryInfo; + if (extensions) { + existing.extensions = { ...(existing.extensions ?? {}), ...extensions }; + } + return; + } + + const row: StoredRow = { + key, + resourceUrl: discovered.resourceUrl, + type: discovered.discoveryInfo.input.type === "mcp" ? "mcp" : "http", + method: isMCP(discovered) ? undefined : discovered.method, + toolName: isMCP(discovered) ? discovered.toolName : undefined, + routeTemplate: isMCP(discovered) ? undefined : discovered.routeTemplate, + description: discovered.description, + mimeType: discovered.mimeType, + serviceName: discovered.serviceName, + tags: discovered.tags, + iconUrl: discovered.iconUrl, + x402Version: discovered.x402Version, + discoveryInfo: discovered.discoveryInfo, + accepts: [paymentRequirements], + extensions: extensions ? { ...extensions } : undefined, + firstSeenAt: now, + lastSeenAt: now, + }; + this.rows.set(key, row); + } + + /** + * Returns rows matching the optional filters, in firstSeenAt order, with offset + * pagination. `limit` is clamped to `[1, 500]`; `offset` is clamped to `>= 0`. + * + * @param params - Optional list filters and pagination. + * @returns The discovery resources response with `pagination.total`. + */ + async list(params: ListDiscoveryResourcesParams = {}): Promise { + const filtered = this.filterRows(params); + const total = filtered.length; + const offset = Math.max(0, params.offset ?? 0); + const limit = Math.min(Math.max(1, params.limit ?? 50), 500); + const page = filtered.slice(offset, offset + limit); + + return { + x402Version: 2, + items: page.map(toDiscoveryResource), + pagination: { limit, offset, total }, + }; + } + + /** + * Substring-matches the query against serviceName/description/tags and returns + * scored hits. Cursor pagination is engaged when `limit` is supplied. + * + * @param params - Search parameters (`query` is required). + * @returns The matching resources, possibly with `pagination.cursor` set. + */ + async search(params: SearchDiscoveryResourcesParams): Promise { + const filtered = this.filterRows(params); + const scored = filtered + .map(row => ({ row, score: searchScore(row, params.query) })) + .filter(s => s.score > 0) + .sort( + (a, b) => b.score - a.score || a.row.firstSeenAt.getTime() - b.row.firstSeenAt.getTime(), + ); + + // Cursor pagination: cursor is the stringified index into the scored list. + const limit = params.limit !== undefined ? Math.min(Math.max(1, params.limit), 500) : undefined; + const startIdx = params.cursor ? Math.max(0, parseInt(params.cursor, 10) || 0) : 0; + const endIdx = limit !== undefined ? startIdx + limit : scored.length; + const page = scored.slice(startIdx, endIdx); + const nextCursor = endIdx < scored.length ? String(endIdx) : null; + + return { + x402Version: 2, + resources: page.map(s => toDiscoveryResource(s.row)), + pagination: limit !== undefined ? { limit, cursor: nextCursor } : null, + }; + } + + /** + * Applies the shared filter predicates used by both `list` and `search`. + * + * @param params - The list or search parameters with optional filter fields. + * @returns The matching rows in firstSeenAt order. + */ + private filterRows( + params: ListDiscoveryResourcesParams | SearchDiscoveryResourcesParams, + ): StoredRow[] { + const all = Array.from(this.rows.values()).sort( + (a, b) => a.firstSeenAt.getTime() - b.firstSeenAt.getTime(), + ); + return all.filter(row => { + if (params.type !== undefined && row.type !== params.type) return false; + if (params.payTo !== undefined && !row.accepts.some(a => a.payTo === params.payTo)) { + return false; + } + if (params.scheme !== undefined && !row.accepts.some(a => a.scheme === params.scheme)) { + return false; + } + if (params.network !== undefined && !row.accepts.some(a => a.network === params.network)) { + return false; + } + if (params.extensions !== undefined) { + if (!row.extensions || !(params.extensions in row.extensions)) return false; + } + return true; + }); + } +} diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/factory.ts b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/factory.ts new file mode 100644 index 0000000000..df374dd3db --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/factory.ts @@ -0,0 +1,65 @@ +/** + * Convenience factory: `pg` + Drizzle + `PostgresBazaarCatalog` in one call. + * + * Pulls `pg` and `drizzle-orm/node-postgres` via dynamic import so the dependency + * stays optional — callers that wire their own Drizzle db (e.g. Neon HTTP driver + * for Vercel serverless) can skip this helper entirely and instantiate + * `PostgresBazaarCatalog` directly. + */ + +import type { AnyDrizzlePgDatabase } from "./postgresCatalog"; +import { PostgresBazaarCatalog } from "./postgresCatalog"; + +export interface CreatePostgresBazaarCatalogOptions { + /** Postgres connection string (e.g. `postgres://user:pass@host:5432/db`). */ + connectionString: string; + /** Whether to run pending migrations on startup. Defaults to `false`. */ + migrate?: boolean; +} + +export interface PostgresBazaarCatalogHandle { + catalog: PostgresBazaarCatalog; + db: AnyDrizzlePgDatabase; + /** Closes the underlying pg pool. Call this on shutdown. */ + close: () => Promise; +} + +/** + * Creates a `PostgresBazaarCatalog` backed by a `pg` connection pool. + * + * Throws a helpful error if `pg` is not installed — it's an optional peer dep + * so consumers that use Neon HTTP / `postgres-js` don't have to install it. + * + * @param options - Connection options. + * @returns A handle with the catalog, the Drizzle db, and a `close()` to drain the pool. + */ +export async function createPostgresBazaarCatalog( + options: CreatePostgresBazaarCatalogOptions, +): Promise { + let pgModule: typeof import("pg"); + try { + pgModule = await import("pg"); + } catch { + throw new Error( + "createPostgresBazaarCatalog requires the optional peer dependency 'pg'. " + + "Install it (npm install pg) or instantiate PostgresBazaarCatalog directly with your own Drizzle db.", + ); + } + const { drizzle } = await import("drizzle-orm/node-postgres"); + + const pool = new pgModule.Pool({ connectionString: options.connectionString }); + const db = drizzle(pool) as AnyDrizzlePgDatabase; + + if (options.migrate) { + const { migrateBazaar } = await import("./migrate"); + await migrateBazaar(db); + } + + return { + catalog: new PostgresBazaarCatalog(db), + db, + close: async () => { + await pool.end(); + }, + }; +} diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/index.ts b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/index.ts new file mode 100644 index 0000000000..7d39f55e9d --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/index.ts @@ -0,0 +1,25 @@ +/** + * Postgres-backed Bazaar catalog. + * + * Import via the subpath: `@x402/extensions/bazaar/postgres`. Keeping it as a + * separate entry point means consumers that use the in-memory catalog (or roll + * their own backend) don't pull in Drizzle / pg. + */ + +export { PostgresBazaarCatalog, type AnyDrizzlePgDatabase } from "./postgresCatalog"; + +export { + createPostgresBazaarCatalog, + type CreatePostgresBazaarCatalogOptions, + type PostgresBazaarCatalogHandle, +} from "./factory"; + +export { migrateBazaar } from "./migrate"; + +export { + discoveredResources, + searchVectorMatch, + searchVectorRank, + type DiscoveredResourcesRow, + type NewDiscoveredResourcesRow, +} from "./schema"; diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/migrate.ts b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/migrate.ts new file mode 100644 index 0000000000..63ee6b5908 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/migrate.ts @@ -0,0 +1,93 @@ +/** + * Bazaar Postgres migrations runner. + * + * Production deployments should run migrations as part of a deploy step, not on + * the hot path. This module exposes a single function that applies the SQL + * migrations bundled with the package. + * + * The migrations live in `migrations/*.sql` and are applied lexicographically. + * A `bazaar_schema_migrations` table tracks which files have been applied so + * re-running is safe. + */ + +import { readdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { sql } from "drizzle-orm"; +import type { AnyDrizzlePgDatabase } from "./postgresCatalog"; + +const MIGRATIONS_TABLE = "bazaar_schema_migrations"; + +/** + * Resolves the migrations directory relative to this source file. In built + * packages the postbuild step copies `migrations/` alongside the compiled + * output; the second candidate falls back to the source tree for tests and + * workspace consumers that run directly from `src/`. + * + * @returns Absolute path to the migrations directory. + */ +function migrationsDir(): string { + const here = dirname(fileURLToPath(import.meta.url)); + const candidates = [ + join(here, "migrations"), + join( + here, + "..", + "..", + "..", + "..", + "src", + "bazaar", + "facilitator-service", + "postgres", + "migrations", + ), + ]; + for (const candidate of candidates) { + try { + readdirSync(candidate); + return candidate; + } catch { + // try next + } + } + throw new Error(`bazaar: unable to locate migrations directory (tried ${candidates.join(", ")})`); +} + +/** + * Applies all pending bazaar SQL migrations idempotently. Safe to run on every + * deploy — already-applied migrations are skipped. + * + * @param db - Drizzle Postgres database (any compatible driver). + */ +export async function migrateBazaar(db: AnyDrizzlePgDatabase): Promise { + await db.execute( + sql`CREATE TABLE IF NOT EXISTS ${sql.raw(MIGRATIONS_TABLE)} ( + name text PRIMARY KEY, + applied_at timestamptz NOT NULL DEFAULT NOW() + )`, + ); + + const applied = await db.execute<{ name: string }>( + sql`SELECT name FROM ${sql.raw(MIGRATIONS_TABLE)}`, + ); + // Drizzle's execute() result shape varies by driver — normalize. + const appliedRows = + (applied as unknown as { rows: { name: string }[] }).rows ?? + (applied as unknown as { name: string }[]); + const appliedSet = new Set(appliedRows.map(r => r.name)); + + const dir = migrationsDir(); + const files = readdirSync(dir) + .filter(name => name.endsWith(".sql")) + .sort(); + + for (const file of files) { + if (appliedSet.has(file)) continue; + const sqlText = readFileSync(join(dir, file), "utf8"); + await db.transaction(async tx => { + await tx.execute(sql.raw(sqlText)); + await tx.execute(sql`INSERT INTO ${sql.raw(MIGRATIONS_TABLE)} (name) VALUES (${file})`); + }); + } +} diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/migrations/0001_init.sql b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/migrations/0001_init.sql new file mode 100644 index 0000000000..f9dd237072 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/migrations/0001_init.sql @@ -0,0 +1,78 @@ +-- Bazaar discovered_resources: initial schema. +-- +-- Hand-written rather than drizzle-kit generated because we need: +-- 1. A maintained tsvector column for full-text search +-- 2. A unique index with NULLS NOT DISTINCT semantics (Postgres 15+) so that +-- (resource_url, NULL method, NULL tool_name) deduplicates correctly. +-- +-- We use a BEFORE INSERT/UPDATE trigger to populate `search_vector` rather +-- than a STORED generated column. The trigger version sidesteps Postgres' +-- immutability check on generated expressions that involve `to_tsvector` +-- under certain configurations, and is the canonical PG pattern for FTS. + +CREATE TABLE IF NOT EXISTS discovered_resources ( + id bigserial PRIMARY KEY, + resource_url text NOT NULL, + type text NOT NULL CHECK (type IN ('http', 'mcp')), + method text, + tool_name text, + route_template text, + description text, + mime_type text, + service_name text, + tags text[], + icon_url text, + x402_version integer NOT NULL, + discovery_info jsonb NOT NULL, + accepts jsonb NOT NULL DEFAULT '[]'::jsonb, + extensions jsonb, + first_seen_at timestamptz NOT NULL DEFAULT NOW(), + last_seen_at timestamptz NOT NULL DEFAULT NOW(), + -- Populated by `discovered_resources_search_vector_update`. Weighted: + -- serviceName (A) > description (B) > tags (C). Mirrors the in-memory + -- catalog's searchScore() so behavior is consistent across backends. + search_vector tsvector +); + +CREATE OR REPLACE FUNCTION discovered_resources_search_vector_update() +RETURNS trigger AS $$ +BEGIN + NEW.search_vector := + setweight(to_tsvector('english', COALESCE(NEW.service_name, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B') || + setweight(to_tsvector('english', array_to_string(COALESCE(NEW.tags, '{}'), ' ')), 'C'); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS discovered_resources_search_vector_trg ON discovered_resources; +CREATE TRIGGER discovered_resources_search_vector_trg + BEFORE INSERT OR UPDATE OF service_name, description, tags + ON discovered_resources + FOR EACH ROW + EXECUTE FUNCTION discovered_resources_search_vector_update(); + +-- HTTP rows are keyed by (resource_url, method); MCP rows by (resource_url, tool_name). +-- NULLS NOT DISTINCT (Postgres 15+) makes (url, NULL method, NULL tool_name) collide as expected +-- for legacy entries where the resource server extension never enriched a method. +CREATE UNIQUE INDEX IF NOT EXISTS discovered_resources_catalog_key + ON discovered_resources (resource_url, method, tool_name) + NULLS NOT DISTINCT; + +CREATE INDEX IF NOT EXISTS discovered_resources_type_idx + ON discovered_resources (type); + +CREATE INDEX IF NOT EXISTS discovered_resources_last_seen_idx + ON discovered_resources (last_seen_at); + +CREATE INDEX IF NOT EXISTS discovered_resources_resource_url_idx + ON discovered_resources (resource_url); + +-- GIN index for the tsvector — what makes `websearch_to_tsquery` fast at scale. +CREATE INDEX IF NOT EXISTS discovered_resources_search_idx + ON discovered_resources USING GIN (search_vector); + +-- jsonb_path_ops GIN over `accepts` lets filters by payTo/scheme/network use the index +-- via @> containment queries. +CREATE INDEX IF NOT EXISTS discovered_resources_accepts_idx + ON discovered_resources USING GIN (accepts jsonb_path_ops); diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/postgresCatalog.ts b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/postgresCatalog.ts new file mode 100644 index 0000000000..b965a37d31 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/postgresCatalog.ts @@ -0,0 +1,330 @@ +/** + * Postgres-backed `BazaarCatalog` implementation. + * + * Uses Drizzle ORM over any compatible Postgres driver. The catalog only + * depends on the Drizzle interface, so the caller decides which driver to + * use (`pg` / `@neondatabase/serverless` / `postgres`). See `factory.ts` + * for the convenience constructor that wires up `pg` for you. + */ + +import { and, asc, desc, eq, sql } from "drizzle-orm"; +import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core"; +import type { PaymentRequirements } from "@x402/core/types"; +import type { DiscoveryInfo } from "../../types"; +import type { DiscoveredHTTPResource } from "../../http/types"; +import type { DiscoveredMCPResource } from "../../mcp/types"; +import type { + BazaarCatalog, + CatalogUpsertInput, + DiscoveryResource, + DiscoveryResourcesResponse, + ListDiscoveryResourcesParams, + SearchDiscoveryResourcesParams, + SearchDiscoveryResourcesResponse, +} from "../catalog"; +import { + discoveredResources, + searchVectorMatch, + searchVectorRank, + type DiscoveredResourcesRow, +} from "./schema"; + +// Drizzle's PgDatabase generic varies by driver. We accept the loosest form so +// callers can pass pg/Neon/postgres-js drizzles interchangeably. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyDrizzlePgDatabase = PgDatabase; + +const MAX_LIMIT = 500; +const DEFAULT_LIMIT = 50; + +/** + * Clamps `n` into `[1, MAX_LIMIT]` with `fallback` for null/undefined inputs. + * + * @param n - Raw user-supplied value. + * @param fallback - Default when `n` is null/undefined. + * @returns The clamped integer. + */ +function clampLimit(n: number | undefined, fallback: number): number { + if (n === undefined || n === null) return fallback; + return Math.min(Math.max(1, Math.floor(n)), MAX_LIMIT); +} + +/** + * Determines whether the discovered resource is MCP based on `input.type`. + * + * @param discovered - Extracted resource. + * @returns True if the resource is an MCP tool. + */ +function isMCP( + discovered: DiscoveredHTTPResource | DiscoveredMCPResource, +): discovered is DiscoveredMCPResource { + return discovered.discoveryInfo.input.type === "mcp"; +} + +/** + * Projects a Drizzle row into the wire-format `DiscoveryResource`. + * + * @param row - Row returned by Drizzle. + * @returns The shape returned by `list` and `search`. + */ +function rowToDiscoveryResource(row: DiscoveredResourcesRow): DiscoveryResource { + return { + resource: row.resourceUrl, + type: row.type, + x402Version: row.x402Version, + accepts: row.accepts as PaymentRequirements[], + lastUpdated: row.lastSeenAt.toISOString(), + extensions: (row.extensions ?? undefined) as Record | undefined, + }; +} + +/** + * Postgres-backed catalog. Pass a Drizzle `pg-core` database; the schema is + * embedded so consumers don't have to import it explicitly. + */ +export class PostgresBazaarCatalog implements BazaarCatalog { + /** + * Wraps a Drizzle Postgres database in the `BazaarCatalog` interface. + * + * @param db - A Drizzle Postgres database instance from any compatible driver. + */ + constructor(private readonly db: AnyDrizzlePgDatabase) {} + + /** + * Upsert a discovered resource. Performs a transactional read-modify-write + * to merge `accepts` deduped by (scheme, network) — Postgres lacks a + * concise inline jsonb array merge with dedup semantics, so the + * read-modify-write is both clearer and idiomatic. + * + * @param input - Upsert payload assembled by `installBazaarFacilitator`. + * @returns A promise that resolves once the row has been persisted. + */ + async upsert(input: CatalogUpsertInput): Promise { + const { discovered, paymentRequirements, extensions } = input; + const method = isMCP(discovered) ? null : (discovered.method ?? null); + const toolName = isMCP(discovered) ? discovered.toolName : null; + const routeTemplate = isMCP(discovered) ? null : (discovered.routeTemplate ?? null); + + await this.db.transaction(async tx => { + const existing = await tx + .select() + .from(discoveredResources) + .where( + and( + eq(discoveredResources.resourceUrl, discovered.resourceUrl), + method === null + ? sql`${discoveredResources.method} IS NULL` + : eq(discoveredResources.method, method), + toolName === null + ? sql`${discoveredResources.toolName} IS NULL` + : eq(discoveredResources.toolName, toolName), + ), + ) + .limit(1); + + const now = new Date(); + + if (existing.length === 0) { + await tx.insert(discoveredResources).values({ + resourceUrl: discovered.resourceUrl, + type: isMCP(discovered) ? "mcp" : "http", + method, + toolName, + routeTemplate, + description: discovered.description ?? null, + mimeType: discovered.mimeType ?? null, + serviceName: discovered.serviceName ?? null, + tags: discovered.tags ?? null, + iconUrl: discovered.iconUrl ?? null, + x402Version: discovered.x402Version, + discoveryInfo: discovered.discoveryInfo as DiscoveryInfo, + accepts: [paymentRequirements], + extensions: extensions ?? null, + firstSeenAt: now, + lastSeenAt: now, + }); + return; + } + + const row = existing[0]; + const accepts = row.accepts as PaymentRequirements[]; + const idx = accepts.findIndex( + a => a.scheme === paymentRequirements.scheme && a.network === paymentRequirements.network, + ); + const nextAccepts = + idx === -1 + ? [...accepts, paymentRequirements] + : accepts.map((a, i) => (i === idx ? paymentRequirements : a)); + + await tx + .update(discoveredResources) + .set({ + // Refresh metadata so renamed services and updated descriptions propagate. + description: discovered.description ?? row.description, + mimeType: discovered.mimeType ?? row.mimeType, + serviceName: discovered.serviceName ?? row.serviceName, + tags: discovered.tags ?? row.tags, + iconUrl: discovered.iconUrl ?? row.iconUrl, + discoveryInfo: discovered.discoveryInfo as DiscoveryInfo, + accepts: nextAccepts, + extensions: extensions ? { ...(row.extensions ?? {}), ...extensions } : row.extensions, + lastSeenAt: now, + }) + .where(eq(discoveredResources.id, row.id)); + }); + } + + /** + * Lists rows matching the filters with offset pagination. Filters use + * jsonb containment against `accepts` so they benefit from the GIN index. + * + * @param params - Optional filters / pagination. + * @returns The discovery response including `pagination.total`. + */ + async list(params: ListDiscoveryResourcesParams = {}): Promise { + const limit = clampLimit(params.limit, DEFAULT_LIMIT); + const offset = params.offset !== undefined ? Math.max(0, Math.floor(params.offset)) : 0; + const filter = this.buildFilter(params); + + const items = await this.db + .select() + .from(discoveredResources) + .where(filter) + .orderBy(asc(discoveredResources.firstSeenAt), asc(discoveredResources.id)) + .limit(limit) + .offset(offset); + + const totalRow = await this.db + .select({ count: sql`count(*)::int` }) + .from(discoveredResources) + .where(filter); + const total = totalRow[0]?.count ?? 0; + + return { + x402Version: 2, + items: items.map(rowToDiscoveryResource), + pagination: { limit, offset, total }, + }; + } + + /** + * Full-text searches the catalog using websearch_to_tsquery + ts_rank_cd + * over the generated `search_vector` column. Cursor pagination is keyset: + * cursor is the base64-encoded (rank, id) of the last row of the prior page. + * + * @param params - Search params (`query` required). + * @returns The search response. + */ + async search(params: SearchDiscoveryResourcesParams): Promise { + const query = params.query; + if (!query || query.trim().length === 0) { + return { x402Version: 2, resources: [] }; + } + const limit = params.limit !== undefined ? clampLimit(params.limit, DEFAULT_LIMIT) : undefined; + const baseFilter = this.buildFilter(params); + const matchFilter = searchVectorMatch(query); + const filter = baseFilter ? and(baseFilter, matchFilter) : matchFilter; + const rank = searchVectorRank(query); + + // Keyset pagination: cursor encodes the (rank, id) of the last row served. + // ORDER BY is `rank DESC, id ASC` so the seek predicate is: + // rank < cursor_rank OR (rank = cursor_rank AND id > cursor_id) + // We can't use a single tuple compare here because the two columns sort in + // opposite directions; Postgres tuple comparison would treat them uniformly. + let cursorPredicate; + if (params.cursor) { + const decoded = decodeCursor(params.cursor); + if (decoded) { + cursorPredicate = sql`(${rank} < ${decoded.rank}) OR (${rank} = ${decoded.rank} AND ${discoveredResources.id} > ${decoded.id})`; + } + } + + const fetchLimit = limit !== undefined ? limit + 1 : MAX_LIMIT; + const rows = await this.db + .select({ + row: discoveredResources, + rank: rank.as("rank"), + }) + .from(discoveredResources) + .where(cursorPredicate ? and(filter, cursorPredicate) : filter) + .orderBy(desc(rank), asc(discoveredResources.id)) + .limit(fetchLimit); + + let nextCursor: string | null = null; + let page = rows; + if (limit !== undefined && rows.length > limit) { + page = rows.slice(0, limit); + const last = page[page.length - 1]; + nextCursor = encodeCursor({ rank: Number(last.rank), id: last.row.id }); + } + + return { + x402Version: 2, + resources: page.map(r => rowToDiscoveryResource(r.row)), + pagination: limit !== undefined ? { limit, cursor: nextCursor } : null, + }; + } + + /** + * Builds the shared SQL filter used by both `list` and `search`. + * + * @param params - Filter params from the request. + * @returns The combined drizzle SQL predicate, or undefined when no filter is active. + */ + private buildFilter(params: ListDiscoveryResourcesParams | SearchDiscoveryResourcesParams) { + const clauses = [] as ReturnType[]; + if (params.type !== undefined) { + clauses.push(sql`${discoveredResources.type} = ${params.type}`); + } + if (params.payTo !== undefined) { + clauses.push( + sql`${discoveredResources.accepts} @> ${JSON.stringify([{ payTo: params.payTo }])}::jsonb`, + ); + } + if (params.scheme !== undefined) { + clauses.push( + sql`${discoveredResources.accepts} @> ${JSON.stringify([{ scheme: params.scheme }])}::jsonb`, + ); + } + if (params.network !== undefined) { + clauses.push( + sql`${discoveredResources.accepts} @> ${JSON.stringify([{ network: params.network }])}::jsonb`, + ); + } + if (params.extensions !== undefined) { + clauses.push(sql`${discoveredResources.extensions} ? ${params.extensions}`); + } + return clauses.length === 0 ? undefined : and(...clauses); + } +} + +/** + * Encodes a `(rank, id)` pair as a base64url cursor for search pagination. + * + * @param value - The pair to encode. + * @param value.rank - The ts_rank_cd score for the last row served. + * @param value.id - The numeric id of the last row served (tiebreaker). + * @returns The base64url-encoded cursor string. + */ +function encodeCursor(value: { rank: number; id: number }): string { + return Buffer.from(`${value.rank};${value.id}`, "utf8").toString("base64url"); +} + +/** + * Decodes a cursor produced by `encodeCursor`. Returns null on any malformed input. + * + * @param cursor - The base64url cursor string from a previous page. + * @returns The decoded `(rank, id)` pair, or `null` if the cursor is invalid. + */ +function decodeCursor(cursor: string): { rank: number; id: number } | null { + try { + const decoded = Buffer.from(cursor, "base64url").toString("utf8"); + const [rankStr, idStr] = decoded.split(";"); + const rank = Number(rankStr); + const id = Number(idStr); + if (!Number.isFinite(rank) || !Number.isFinite(id)) return null; + return { rank, id }; + } catch { + return null; + } +} diff --git a/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/schema.ts b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/schema.ts new file mode 100644 index 0000000000..f129637d2d --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator-service/postgres/schema.ts @@ -0,0 +1,73 @@ +/** + * Drizzle schema for the bazaar discovered_resources table. + * + * The generated `search_vector` column and its GIN index are created via raw + * SQL in `migrations/0001_init.sql`; Drizzle's typed schema does not (yet) + * model generated columns or `NULLS NOT DISTINCT` unique indexes cleanly, so + * those live in the migration file. The catalog implementation reads/writes + * everything but the generated column. + */ + +import { sql } from "drizzle-orm"; +import { bigserial, index, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import type { PaymentRequirements } from "@x402/core/types"; +import type { DiscoveryInfo } from "../../types"; + +export const discoveredResources = pgTable( + "discovered_resources", + { + id: bigserial("id", { mode: "number" }).primaryKey(), + resourceUrl: text("resource_url").notNull(), + type: text("type", { enum: ["http", "mcp"] }).notNull(), + method: text("method"), + toolName: text("tool_name"), + routeTemplate: text("route_template"), + description: text("description"), + mimeType: text("mime_type"), + serviceName: text("service_name"), + tags: text("tags").array(), + iconUrl: text("icon_url"), + x402Version: integer("x402_version").notNull(), + discoveryInfo: jsonb("discovery_info").$type().notNull(), + accepts: jsonb("accepts").$type().notNull(), + extensions: jsonb("extensions").$type>(), + firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).notNull().defaultNow(), + lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(), + }, + table => [ + // GIN index on the search_vector column is declared in the SQL migration + // (Drizzle can't reference generated columns it doesn't model). The btree + // indexes below cover the common filter paths used by `list`. + index("discovered_resources_type_idx").on(table.type), + index("discovered_resources_last_seen_idx").on(table.lastSeenAt), + // Catalog key (resource_url, method, tool_name) with NULLS NOT DISTINCT is + // also declared in the SQL migration — drizzle-kit emits it as a partial + // unique index that doesn't match the spec's requirement that NULL slots + // participate in uniqueness. + index("discovered_resources_resource_url_idx").on(table.resourceUrl), + ], +); + +export type DiscoveredResourcesRow = typeof discoveredResources.$inferSelect; +export type NewDiscoveredResourcesRow = typeof discoveredResources.$inferInsert; + +/** + * SQL fragment that produces the websearch tsquery for `search_vector`. + * Exported so the Postgres catalog and any consumer can ORDER BY the same rank. + * + * @param query - The user-supplied natural-language query. + * @returns A drizzle SQL fragment usable in WHERE / ORDER BY clauses. + */ +export function searchVectorMatch(query: string) { + return sql`search_vector @@ websearch_to_tsquery('english', ${query})`; +} + +/** + * SQL fragment producing the ts_rank_cd score for the same query. + * + * @param query - The user-supplied natural-language query. + * @returns A drizzle SQL fragment producing a real-valued rank. + */ +export function searchVectorRank(query: string) { + return sql`ts_rank_cd(search_vector, websearch_to_tsquery('english', ${query}))`; +} diff --git a/typescript/packages/extensions/src/bazaar/index.ts b/typescript/packages/extensions/src/bazaar/index.ts index 6a6afa1755..5ba6d0fc7e 100644 --- a/typescript/packages/extensions/src/bazaar/index.ts +++ b/typescript/packages/extensions/src/bazaar/index.ts @@ -141,3 +141,12 @@ export { type DiscoveryResourcesResponse, type SearchDiscoveryResourcesResponse, } from "./facilitatorClient"; + +// Export facilitator service (for facilitators hosting discovery endpoints) +export { + installBazaarFacilitator, + InMemoryBazaarCatalog, + type BazaarCatalog, + type CatalogUpsertInput, + type InstallBazaarFacilitatorOptions, +} from "./facilitator-service"; diff --git a/typescript/packages/extensions/test/bazaar-facilitator-service.test.ts b/typescript/packages/extensions/test/bazaar-facilitator-service.test.ts new file mode 100644 index 0000000000..c206f2bc7f --- /dev/null +++ b/typescript/packages/extensions/test/bazaar-facilitator-service.test.ts @@ -0,0 +1,489 @@ +/** + * Tests for the Bazaar facilitator service helpers: + * - InMemoryBazaarCatalog + * - installBazaarFacilitator + * + * The catalog tests cover upsert/merge/list/search semantics. The install + * tests use a minimal mock x402Facilitator to verify the hook wiring; the + * full verify/settle integration is exercised by the e2e suite. + */ + +import { describe, expect, it, vi } from "vitest"; +import type { PaymentPayload, PaymentRequirements } from "@x402/core/types"; +import type { x402Facilitator } from "@x402/core/facilitator"; + +import { + BAZAAR, + InMemoryBazaarCatalog, + declareDiscoveryExtension, + installBazaarFacilitator, +} from "../src/bazaar/index"; +import type { CatalogUpsertInput } from "../src/bazaar/facilitator-service"; + +// ---------- helpers ---------------------------------------------------------- + +const REQ_BASE_EVM: PaymentRequirements = { + scheme: "exact", + network: "eip155:8453", + asset: "0xUSDC", + amount: "100000", + payTo: "0xMerchant", + maxTimeoutSeconds: 60, + extra: {}, +}; + +const REQ_SVM: PaymentRequirements = { + scheme: "exact", + network: "solana:mainnet", + asset: "USDC", + amount: "100000", + payTo: "SolanaMerchant", + maxTimeoutSeconds: 60, + extra: {}, +}; + +/** + * Builds a v2 PaymentPayload for an HTTP endpoint with a bazaar extension. + * + * @param opts - Fixture overrides. + * @param opts.url - Resource URL recorded in the payload. + * @param opts.method - HTTP method to declare ("GET" by default). + * @param opts.description - Optional resource description. + * @param opts.serviceName - Optional service name metadata. + * @param opts.tags - Optional service tags. + * @returns A PaymentPayload suitable for feeding into extractDiscoveryInfo. + */ +function makeHttpPayload(opts: { + url: string; + method?: "GET" | "POST"; + description?: string; + serviceName?: string; + tags?: string[]; +}): PaymentPayload { + const declared = declareDiscoveryExtension( + opts.method === "POST" + ? { + method: "POST", + input: { q: "x" }, + inputSchema: { properties: { q: { type: "string" } } }, + bodyType: "json", + } + : { + method: "GET", + input: { q: "x" }, + inputSchema: { properties: { q: { type: "string" } } }, + }, + ); + return { + x402Version: 2, + payload: {}, + accepted: REQ_BASE_EVM, + resource: { + url: opts.url, + description: opts.description, + serviceName: opts.serviceName, + tags: opts.tags, + }, + extensions: { [BAZAAR.key]: declared.bazaar }, + } as unknown as PaymentPayload; +} + +/** + * Builds a v2 PaymentPayload for an MCP tool with a bazaar extension. + * + * @param opts - Fixture overrides. + * @param opts.url - MCP server URL. + * @param opts.toolName - MCP tool name (catalog key for the row). + * @param opts.description - Optional tool description. + * @returns A PaymentPayload suitable for feeding into extractDiscoveryInfo. + */ +function makeMcpPayload(opts: { + url: string; + toolName: string; + description?: string; +}): PaymentPayload { + const declared = declareDiscoveryExtension({ + toolName: opts.toolName, + description: opts.description, + inputSchema: { + type: "object", + properties: { ticker: { type: "string" } }, + required: ["ticker"], + }, + }); + return { + x402Version: 2, + payload: {}, + accepted: REQ_BASE_EVM, + resource: { url: opts.url, description: opts.description }, + extensions: { [BAZAAR.key]: declared.bazaar }, + } as unknown as PaymentPayload; +} + +/** + * Extracts a discovery resource from `payload` and upserts it into `catalog`. + * Mirrors what `installBazaarFacilitator`'s hook does, but synchronously so tests + * can assert directly on the catalog after each call. + * + * @param catalog - The catalog under test. + * @param payload - The PaymentPayload carrying a bazaar extension. + * @param requirements - The selected PaymentRequirements for this payment. + */ +async function ingest( + catalog: InMemoryBazaarCatalog, + payload: PaymentPayload, + requirements: PaymentRequirements = REQ_BASE_EVM, +) { + // Re-derive the discovered resource using the same extraction the install + // hook performs, so tests describe the catalog API rather than re-test extraction. + const { extractDiscoveryInfo } = await import("../src/bazaar/facilitator"); + const discovered = extractDiscoveryInfo(payload, {} as never); + if (!discovered) throw new Error("extractDiscoveryInfo returned null"); + await catalog.upsert({ discovered, paymentRequirements: requirements }); +} + +// ---------- InMemoryBazaarCatalog ------------------------------------------- + +describe("InMemoryBazaarCatalog", () => { + it("stores a single HTTP row with the selected accepts entry", async () => { + const catalog = new InMemoryBazaarCatalog(); + await ingest( + catalog, + makeHttpPayload({ url: "https://api.example.com/weather", method: "GET" }), + ); + + const { items, pagination } = await catalog.list(); + expect(pagination.total).toBe(1); + expect(items[0].resource).toBe("https://api.example.com/weather"); + expect(items[0].type).toBe("http"); + expect(items[0].accepts).toEqual([REQ_BASE_EVM]); + }); + + it("merges accepts on second upsert with a different (scheme, network)", async () => { + const catalog = new InMemoryBazaarCatalog(); + const payload = makeHttpPayload({ url: "https://api.example.com/weather", method: "GET" }); + await ingest(catalog, payload, REQ_BASE_EVM); + await ingest(catalog, payload, REQ_SVM); + + const { items, pagination } = await catalog.list(); + expect(pagination.total).toBe(1); + expect(items[0].accepts).toHaveLength(2); + expect(items[0].accepts.map(a => a.network)).toEqual( + expect.arrayContaining(["eip155:8453", "solana:mainnet"]), + ); + }); + + it("replaces in-place when (scheme, network) matches — no growth", async () => { + const catalog = new InMemoryBazaarCatalog(); + const payload = makeHttpPayload({ url: "https://api.example.com/weather", method: "GET" }); + await ingest(catalog, payload, REQ_BASE_EVM); + await ingest(catalog, payload, { ...REQ_BASE_EVM, amount: "200000" }); + + const { items } = await catalog.list(); + expect(items[0].accepts).toHaveLength(1); + expect(items[0].accepts[0].amount).toBe("200000"); + }); + + it("keys HTTP rows by method — GET and POST on the same URL are distinct", async () => { + const catalog = new InMemoryBazaarCatalog(); + await ingest(catalog, makeHttpPayload({ url: "https://api.example.com/x", method: "GET" })); + await ingest(catalog, makeHttpPayload({ url: "https://api.example.com/x", method: "POST" })); + + const { pagination } = await catalog.list(); + expect(pagination.total).toBe(2); + }); + + it("keys MCP rows by toolName — distinct tools at the same URL are distinct rows", async () => { + const catalog = new InMemoryBazaarCatalog(); + await ingest( + catalog, + makeMcpPayload({ url: "https://api.example.com/mcp", toolName: "analyze" }), + ); + await ingest( + catalog, + makeMcpPayload({ url: "https://api.example.com/mcp", toolName: "search" }), + ); + + const { items, pagination } = await catalog.list(); + expect(pagination.total).toBe(2); + expect(items.map(i => i.type)).toEqual(["mcp", "mcp"]); + }); + + it("list filters by type", async () => { + const catalog = new InMemoryBazaarCatalog(); + await ingest(catalog, makeHttpPayload({ url: "https://a.com/x", method: "GET" })); + await ingest(catalog, makeMcpPayload({ url: "https://b.com/mcp", toolName: "t" })); + + const httpOnly = await catalog.list({ type: "http" }); + expect(httpOnly.pagination.total).toBe(1); + expect(httpOnly.items[0].type).toBe("http"); + + const mcpOnly = await catalog.list({ type: "mcp" }); + expect(mcpOnly.pagination.total).toBe(1); + expect(mcpOnly.items[0].type).toBe("mcp"); + }); + + it("list filters by payTo / scheme / network across the accepts array", async () => { + const catalog = new InMemoryBazaarCatalog(); + const a = makeHttpPayload({ url: "https://a.com/x", method: "GET" }); + const b = makeHttpPayload({ url: "https://b.com/x", method: "GET" }); + await ingest(catalog, a, REQ_BASE_EVM); + await ingest(catalog, b, REQ_SVM); + + expect((await catalog.list({ payTo: "0xMerchant" })).pagination.total).toBe(1); + expect((await catalog.list({ payTo: "SolanaMerchant" })).pagination.total).toBe(1); + expect((await catalog.list({ network: "eip155:8453" })).pagination.total).toBe(1); + expect((await catalog.list({ scheme: "exact" })).pagination.total).toBe(2); + }); + + it("list paginates with limit/offset", async () => { + const catalog = new InMemoryBazaarCatalog(); + for (let i = 0; i < 5; i++) { + await ingest(catalog, makeHttpPayload({ url: `https://a.com/x${i}`, method: "GET" })); + } + const page1 = await catalog.list({ limit: 2, offset: 0 }); + const page2 = await catalog.list({ limit: 2, offset: 2 }); + const page3 = await catalog.list({ limit: 2, offset: 4 }); + expect(page1.items).toHaveLength(2); + expect(page2.items).toHaveLength(2); + expect(page3.items).toHaveLength(1); + expect(page1.pagination.total).toBe(5); + }); + + it("clamps invalid limit/offset to safe values", async () => { + const catalog = new InMemoryBazaarCatalog(); + await ingest(catalog, makeHttpPayload({ url: "https://a.com/x", method: "GET" })); + const negOffset = await catalog.list({ offset: -5 }); + expect(negOffset.items).toHaveLength(1); + const tooBig = await catalog.list({ limit: 999999 }); + expect(tooBig.pagination.limit).toBeLessThanOrEqual(500); + }); + + it("search ranks serviceName matches above description above tags", async () => { + const catalog = new InMemoryBazaarCatalog(); + await ingest( + catalog, + makeHttpPayload({ + url: "https://weather.example.com/forecast", + method: "GET", + serviceName: "Weather", + description: "Forecasts for cities", + tags: ["meteo"], + }), + ); + await ingest( + catalog, + makeHttpPayload({ + url: "https://news.example.com/finance", + method: "GET", + serviceName: "Finance News", + description: "Articles about weather and markets", + }), + ); + + const result = await catalog.search({ query: "weather" }); + expect(result.resources).toHaveLength(2); + // serviceName "Weather" beats description-only match. + expect(result.resources[0].resource).toBe("https://weather.example.com/forecast"); + }); + + it("search omits non-matching rows", async () => { + const catalog = new InMemoryBazaarCatalog(); + await ingest( + catalog, + makeHttpPayload({ + url: "https://x.com/y", + method: "GET", + serviceName: "Finance", + }), + ); + const result = await catalog.search({ query: "weather" }); + expect(result.resources).toHaveLength(0); + }); + + it("search paginates with cursor when limit is supplied", async () => { + const catalog = new InMemoryBazaarCatalog(); + for (let i = 0; i < 6; i++) { + await ingest( + catalog, + makeHttpPayload({ + url: `https://a${i}.example.com/x`, + method: "GET", + description: `weather service ${i}`, + }), + ); + } + const page1 = await catalog.search({ query: "weather", limit: 4 }); + expect(page1.resources).toHaveLength(4); + expect(page1.pagination?.cursor).not.toBeNull(); + + const page2 = await catalog.search({ + query: "weather", + limit: 4, + cursor: page1.pagination!.cursor!, + }); + expect(page2.resources).toHaveLength(2); + expect(page2.pagination?.cursor).toBeNull(); + }); + + it("canonicalizes dynamic routes via routeTemplate (one row regardless of param value)", async () => { + // Mirror what bazaarResourceServerExtension.enrichDeclaration produces for a + // dynamic route: pathParams in info.input AND in the schema's input properties. + const declared = declareDiscoveryExtension({ + method: "GET", + input: { id: "123" }, + inputSchema: { properties: { id: { type: "string" } } }, + }); + const baseSchema = declared.bazaar.schema; + const withTemplate = { + ...declared.bazaar, + info: { + ...declared.bazaar.info, + input: { ...declared.bazaar.info.input, pathParams: { id: "123" } }, + }, + schema: { + ...baseSchema, + properties: { + ...baseSchema.properties, + input: { + ...baseSchema.properties.input, + properties: { + ...baseSchema.properties.input.properties, + pathParams: { type: "object" }, + }, + }, + }, + }, + routeTemplate: "/users/:id", + }; + + const catalog = new InMemoryBazaarCatalog(); + const payload1 = { + x402Version: 2, + payload: {}, + accepted: REQ_BASE_EVM, + resource: { url: "https://api.example.com/users/123" }, + extensions: { [BAZAAR.key]: withTemplate }, + } as unknown as PaymentPayload; + const payload2 = { + ...payload1, + resource: { url: "https://api.example.com/users/456" }, + } as unknown as PaymentPayload; + + await ingest(catalog, payload1); + await ingest(catalog, payload2); + + const { items, pagination } = await catalog.list(); + expect(pagination.total).toBe(1); + expect(items[0].resource).toBe("https://api.example.com/users/:id"); + }); +}); + +// ---------- installBazaarFacilitator ----------------------------------------- + +describe("installBazaarFacilitator", () => { + /** + * Minimal mock that captures the registered after-verify hook. We exercise + * the hook directly to assert wiring without standing up a full facilitator. + * + * @returns A `{ mock, state }` pair: `mock` is cast to `x402Facilitator`, `state` records the captured registration data. + */ + function makeMockFacilitator() { + const state: { + registeredKeys: string[]; + afterVerifyHook: ((ctx: never) => Promise) | null; + } = { registeredKeys: [], afterVerifyHook: null }; + + const mock = { + registerExtension(ext: { key: string }) { + state.registeredKeys.push(ext.key); + return mock; + }, + onAfterVerify(hook: (ctx: never) => Promise) { + state.afterVerifyHook = hook; + return mock; + }, + } as unknown as x402Facilitator; + + return { mock, state }; + } + + /** + * Builds a minimal `FacilitatorVerifyResultContext`-shaped object for hook tests. + * + * @param payload - The PaymentPayload to feed the hook. + * @param requirements - The PaymentRequirements to feed the hook. + * @returns A context object cast to `never` (caller doesn't need the real type). + */ + function makeVerifyContext( + payload: PaymentPayload, + requirements: PaymentRequirements = REQ_BASE_EVM, + ) { + return { paymentPayload: payload, requirements, result: { isValid: true } } as never; + } + + it("registers the bazaar extension marker", () => { + const { mock, state } = makeMockFacilitator(); + installBazaarFacilitator(mock, new InMemoryBazaarCatalog()); + expect(state.registeredKeys).toContain(BAZAAR.key); + }); + + it("forwards a payload with a bazaar extension to catalog.upsert", async () => { + const catalog = new InMemoryBazaarCatalog(); + const { mock, state } = makeMockFacilitator(); + installBazaarFacilitator(mock, catalog); + + const payload = makeHttpPayload({ url: "https://api.example.com/x", method: "GET" }); + await state.afterVerifyHook!(makeVerifyContext(payload)); + + const { items, pagination } = await catalog.list(); + expect(pagination.total).toBe(1); + expect(items[0].resource).toBe("https://api.example.com/x"); + }); + + it("does not call upsert when the payload has no bazaar extension", async () => { + const upsertSpy = vi.fn(); + const catalog: import("../src/bazaar/facilitator-service").BazaarCatalog = { + upsert: upsertSpy as unknown as (i: CatalogUpsertInput) => Promise, + list: async () => ({ + x402Version: 2, + items: [], + pagination: { limit: 0, offset: 0, total: 0 }, + }), + search: async () => ({ x402Version: 2, resources: [] }), + }; + const { mock, state } = makeMockFacilitator(); + installBazaarFacilitator(mock, catalog); + + const payload = { + x402Version: 2, + payload: {}, + accepted: REQ_BASE_EVM, + resource: { url: "https://api.example.com/x" }, + } as unknown as PaymentPayload; + + await state.afterVerifyHook!(makeVerifyContext(payload)); + expect(upsertSpy).not.toHaveBeenCalled(); + }); + + it("swallows catalog errors via the onError handler — does not throw", async () => { + const onError = vi.fn(); + const catalog: import("../src/bazaar/facilitator-service").BazaarCatalog = { + upsert: async () => { + throw new Error("boom"); + }, + list: async () => ({ + x402Version: 2, + items: [], + pagination: { limit: 0, offset: 0, total: 0 }, + }), + search: async () => ({ x402Version: 2, resources: [] }), + }; + const { mock, state } = makeMockFacilitator(); + installBazaarFacilitator(mock, catalog, { onError }); + + const payload = makeHttpPayload({ url: "https://api.example.com/x", method: "GET" }); + await expect(state.afterVerifyHook!(makeVerifyContext(payload))).resolves.toBeUndefined(); + expect(onError).toHaveBeenCalledOnce(); + }); +}); diff --git a/typescript/packages/extensions/test/bazaar-postgres.test.ts b/typescript/packages/extensions/test/bazaar-postgres.test.ts new file mode 100644 index 0000000000..3c94a51d6c --- /dev/null +++ b/typescript/packages/extensions/test/bazaar-postgres.test.ts @@ -0,0 +1,344 @@ +/** + * Integration tests for the Postgres-backed Bazaar catalog. + * + * Uses Testcontainers to spin up an ephemeral Postgres for each `describe` + * block. Gated behind `RUN_DB_TESTS=1` so CI / contributors without Docker + * don't have to pay the startup cost on every test run. + * + * pnpm test:db # runs this file in addition to the rest + */ + +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { Pool } from "pg"; +import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; +import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql"; + +import type { PaymentPayload, PaymentRequirements } from "@x402/core/types"; +import { BAZAAR, declareDiscoveryExtension, extractDiscoveryInfo } from "../src/bazaar/index"; +import { PostgresBazaarCatalog, migrateBazaar } from "../src/bazaar/facilitator-service/postgres"; + +const SUITE_ENABLED = process.env.RUN_DB_TESTS === "1"; +const describeDb = SUITE_ENABLED ? describe : describe.skip; + +const REQ_BASE_EVM: PaymentRequirements = { + scheme: "exact", + network: "eip155:8453", + asset: "0xUSDC", + amount: "100000", + payTo: "0xMerchant", + maxTimeoutSeconds: 60, + extra: {}, +}; + +const REQ_SVM: PaymentRequirements = { + scheme: "exact", + network: "solana:mainnet", + asset: "USDC", + amount: "100000", + payTo: "SolanaMerchant", + maxTimeoutSeconds: 60, + extra: {}, +}; + +/** + * Builds a v2 HTTP PaymentPayload carrying a bazaar discovery extension. + * + * @param opts - Fixture overrides. + * @param opts.url - Resource URL. + * @param opts.method - HTTP method ("GET" default). + * @param opts.description - Optional description. + * @param opts.serviceName - Optional service name metadata. + * @param opts.tags - Optional tags array. + * @returns A v2 PaymentPayload suitable for extractDiscoveryInfo. + */ +function makeHttpPayload(opts: { + url: string; + method?: "GET" | "POST"; + description?: string; + serviceName?: string; + tags?: string[]; +}): PaymentPayload { + const declared = declareDiscoveryExtension( + opts.method === "POST" + ? { + method: "POST", + input: { q: "x" }, + inputSchema: { properties: { q: { type: "string" } } }, + bodyType: "json", + } + : { + method: "GET", + input: { q: "x" }, + inputSchema: { properties: { q: { type: "string" } } }, + }, + ); + return { + x402Version: 2, + payload: {}, + accepted: REQ_BASE_EVM, + resource: { + url: opts.url, + description: opts.description, + serviceName: opts.serviceName, + tags: opts.tags, + }, + extensions: { [BAZAAR.key]: declared.bazaar }, + } as unknown as PaymentPayload; +} + +/** + * Builds a v2 MCP PaymentPayload carrying a bazaar discovery extension. + * + * @param opts - Fixture overrides. + * @param opts.url - MCP server URL. + * @param opts.toolName - MCP tool name. + * @param opts.description - Optional tool description. + * @returns A v2 PaymentPayload. + */ +function makeMcpPayload(opts: { + url: string; + toolName: string; + description?: string; +}): PaymentPayload { + const declared = declareDiscoveryExtension({ + toolName: opts.toolName, + description: opts.description, + inputSchema: { + type: "object", + properties: { ticker: { type: "string" } }, + required: ["ticker"], + }, + }); + return { + x402Version: 2, + payload: {}, + accepted: REQ_BASE_EVM, + resource: { url: opts.url, description: opts.description }, + extensions: { [BAZAAR.key]: declared.bazaar }, + } as unknown as PaymentPayload; +} + +describeDb("PostgresBazaarCatalog (Testcontainers)", () => { + let container: StartedPostgreSqlContainer; + let pool: Pool; + let db: NodePgDatabase; + let catalog: PostgresBazaarCatalog; + + beforeAll(async () => { + container = await new PostgreSqlContainer("postgres:16-alpine").start(); + pool = new Pool({ connectionString: container.getConnectionUri() }); + db = drizzle(pool); + await migrateBazaar(db); + catalog = new PostgresBazaarCatalog(db); + }, 120_000); + + afterAll(async () => { + await pool?.end(); + await container?.stop(); + }); + + /** + * Truncates the table between tests. Faster than restarting the container. + */ + async function reset() { + await pool.query("TRUNCATE TABLE discovered_resources RESTART IDENTITY"); + } + + /** + * Mirrors `installBazaarFacilitator`'s extraction + upsert flow for tests. + * + * @param payload - The PaymentPayload carrying the bazaar extension. + * @param requirements - The selected PaymentRequirements. + */ + async function ingest(payload: PaymentPayload, requirements: PaymentRequirements = REQ_BASE_EVM) { + const discovered = extractDiscoveryInfo(payload, {} as never); + if (!discovered) throw new Error("extractDiscoveryInfo returned null"); + await catalog.upsert({ discovered, paymentRequirements: requirements }); + } + + it("upserts a new row and surfaces it via list()", async () => { + await reset(); + await ingest(makeHttpPayload({ url: "https://api.example.com/weather", method: "GET" })); + + const { items, pagination } = await catalog.list(); + expect(pagination.total).toBe(1); + expect(items[0].resource).toBe("https://api.example.com/weather"); + expect(items[0].accepts).toHaveLength(1); + expect(items[0].accepts[0].network).toBe("eip155:8453"); + }); + + it("merges accepts deduped by (scheme, network) and replaces in-place on collision", async () => { + await reset(); + const payload = makeHttpPayload({ url: "https://api.example.com/x", method: "GET" }); + await ingest(payload, REQ_BASE_EVM); + await ingest(payload, REQ_SVM); + await ingest(payload, { ...REQ_BASE_EVM, amount: "999" }); + + const { items } = await catalog.list(); + expect(items).toHaveLength(1); + expect(items[0].accepts).toHaveLength(2); + const evm = items[0].accepts.find(a => a.network === "eip155:8453"); + expect(evm?.amount).toBe("999"); // replaced, not duplicated + }); + + it("treats GET and POST on the same URL as distinct rows", async () => { + await reset(); + await ingest(makeHttpPayload({ url: "https://api.example.com/x", method: "GET" })); + await ingest(makeHttpPayload({ url: "https://api.example.com/x", method: "POST" })); + expect((await catalog.list()).pagination.total).toBe(2); + }); + + it("treats distinct MCP toolNames at the same URL as distinct rows", async () => { + await reset(); + await ingest(makeMcpPayload({ url: "https://api.example.com/mcp", toolName: "analyze" })); + await ingest(makeMcpPayload({ url: "https://api.example.com/mcp", toolName: "search" })); + const { items, pagination } = await catalog.list(); + expect(pagination.total).toBe(2); + expect(items.every(i => i.type === "mcp")).toBe(true); + }); + + it("filters by type / payTo / scheme / network via jsonb containment", async () => { + await reset(); + await ingest(makeHttpPayload({ url: "https://a.com/x", method: "GET" }), REQ_BASE_EVM); + await ingest(makeHttpPayload({ url: "https://b.com/x", method: "GET" }), REQ_SVM); + await ingest(makeMcpPayload({ url: "https://c.com/mcp", toolName: "t" })); + + expect((await catalog.list({ type: "mcp" })).pagination.total).toBe(1); + expect((await catalog.list({ payTo: "SolanaMerchant" })).pagination.total).toBe(1); + expect((await catalog.list({ network: "eip155:8453" })).pagination.total).toBe(2); + // MCP row was also ingested with REQ_BASE_EVM, so total for eip155 is 2 (a + mcp). + expect((await catalog.list({ scheme: "exact" })).pagination.total).toBe(3); + }); + + it("paginates list() with limit/offset", async () => { + await reset(); + for (let i = 0; i < 5; i++) { + await ingest(makeHttpPayload({ url: `https://a.com/x${i}`, method: "GET" })); + } + const page1 = await catalog.list({ limit: 2, offset: 0 }); + const page2 = await catalog.list({ limit: 2, offset: 2 }); + const page3 = await catalog.list({ limit: 2, offset: 4 }); + expect(page1.items).toHaveLength(2); + expect(page2.items).toHaveLength(2); + expect(page3.items).toHaveLength(1); + expect(page1.pagination.total).toBe(5); + }); + + it("ranks tsvector matches: serviceName > description > tags", async () => { + await reset(); + await ingest( + makeHttpPayload({ + url: "https://weather.example.com/forecast", + method: "GET", + serviceName: "Weather Service", + description: "Forecasts for cities", + tags: ["meteo"], + }), + ); + await ingest( + makeHttpPayload({ + url: "https://news.example.com/finance", + method: "GET", + serviceName: "Finance News", + description: "Articles about weather and markets", + }), + ); + + const result = await catalog.search({ query: "weather" }); + expect(result.resources.length).toBeGreaterThanOrEqual(1); + expect(result.resources[0].resource).toBe("https://weather.example.com/forecast"); + }); + + it("returns empty results for searches with no matches", async () => { + await reset(); + await ingest( + makeHttpPayload({ + url: "https://x.com/y", + method: "GET", + serviceName: "Finance", + }), + ); + const result = await catalog.search({ query: "completelyunrelatedterm" }); + expect(result.resources).toHaveLength(0); + }); + + it("paginates search() with a stable keyset cursor", async () => { + await reset(); + for (let i = 0; i < 6; i++) { + await ingest( + makeHttpPayload({ + url: `https://a${i}.example.com/x`, + method: "GET", + description: `weather service ${i}`, + }), + ); + } + const seen: string[] = []; + let cursor: string | null = null; + let pages = 0; + do { + const page = await catalog.search({ + query: "weather", + limit: 2, + ...(cursor ? { cursor } : {}), + }); + for (const r of page.resources) seen.push(r.resource); + cursor = page.pagination?.cursor ?? null; + pages++; + if (pages > 10) throw new Error("search pagination did not terminate"); + } while (cursor); + + expect(seen).toHaveLength(6); + expect(new Set(seen).size).toBe(6); // no duplicates across pages + }); + + it("canonicalizes dynamic routes through routeTemplate (one row regardless of param value)", async () => { + await reset(); + const declared = declareDiscoveryExtension({ + method: "GET", + input: { id: "x" }, + inputSchema: { properties: { id: { type: "string" } } }, + }); + const base = declared.bazaar; + const withTemplate = { + ...base, + info: { ...base.info, input: { ...base.info.input, pathParams: { id: "123" } } }, + schema: { + ...base.schema, + properties: { + ...base.schema.properties, + input: { + ...base.schema.properties.input, + properties: { + ...base.schema.properties.input.properties, + pathParams: { type: "object" }, + }, + }, + }, + }, + routeTemplate: "/users/:id", + }; + const mkPayload = (concreteUrl: string) => + ({ + x402Version: 2, + payload: {}, + accepted: REQ_BASE_EVM, + resource: { url: concreteUrl }, + extensions: { [BAZAAR.key]: withTemplate }, + }) as unknown as PaymentPayload; + + await ingest(mkPayload("https://api.example.com/users/123")); + await ingest(mkPayload("https://api.example.com/users/456")); + + const { items, pagination } = await catalog.list(); + expect(pagination.total).toBe(1); + expect(items[0].resource).toBe("https://api.example.com/users/:id"); + }); + + it("is idempotent across re-runs of migrateBazaar", async () => { + await migrateBazaar(db); + await migrateBazaar(db); + const rows = await pool.query("SELECT count(*) FROM bazaar_schema_migrations"); + expect(Number(rows.rows[0].count)).toBe(1); + }); +}); diff --git a/typescript/packages/extensions/tsup.config.ts b/typescript/packages/extensions/tsup.config.ts index bd85a73925..7a8985a871 100644 --- a/typescript/packages/extensions/tsup.config.ts +++ b/typescript/packages/extensions/tsup.config.ts @@ -4,6 +4,7 @@ const baseConfig = { entry: { index: "src/index.ts", "bazaar/index": "src/bazaar/index.ts", + "bazaar/postgres/index": "src/bazaar/facilitator-service/postgres/index.ts", "sign-in-with-x/index": "src/sign-in-with-x/index.ts", "offer-receipt/index": "src/offer-receipt/index.ts", "payment-identifier/index": "src/payment-identifier/index.ts", diff --git a/typescript/pnpm-lock.yaml b/typescript/pnpm-lock.yaml index dc0df6b310..4c41b987d4 100644 --- a/typescript/pnpm-lock.yaml +++ b/typescript/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: packages/extensions: dependencies: + '@neondatabase/serverless': + specifier: ^0.10.0 + version: 0.10.4 '@noble/curves': specifier: ^1.9.0 version: 1.9.7 @@ -96,6 +99,9 @@ importers: ajv: specifier: ^8.17.1 version: 8.17.1 + drizzle-orm: + specifier: ^0.36.4 + version: 0.36.4(@neondatabase/serverless@0.10.4)(@types/pg@8.20.0)(@types/react@19.1.12)(pg@8.20.0)(react@19.2.3) jose: specifier: ^5.9.6 version: 5.10.0 @@ -112,15 +118,24 @@ importers: '@eslint/js': specifier: ^9.24.0 version: 9.34.0 + '@testcontainers/postgresql': + specifier: ^10.16.0 + version: 10.28.0 '@types/node': specifier: ^22.13.4 version: 22.18.0 + '@types/pg': + specifier: ^8.11.10 + version: 8.20.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + drizzle-kit: + specifier: ^0.30.0 + version: 0.30.6 eslint: specifier: ^9.24.0 version: 9.34.0(jiti@2.6.1) @@ -133,6 +148,9 @@ importers: eslint-plugin-prettier: specifier: ^5.2.6 version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) + pg: + specifier: ^8.13.1 + version: 8.20.0 prettier: specifier: 3.5.2 version: 3.5.2 @@ -1578,6 +1596,9 @@ importers: '@x402/svm': specifier: workspace:* version: link:../packages/mechanisms/svm + drizzle-orm: + specifier: ^0.36.4 + version: 0.36.4(@neondatabase/serverless@0.10.4)(@types/pg@8.20.0)(@types/react@19.1.12)(pg@8.20.0)(react@19.2.3) lottie-react: specifier: ^2.4.1 version: 2.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1587,6 +1608,9 @@ importers: next: specifier: ^16.0.10 version: 16.0.10(@babel/core@7.28.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + pg: + specifier: ^8.13.1 + version: 8.20.0 react: specifier: ^19.2.3 version: 19.2.3 @@ -1618,6 +1642,9 @@ importers: '@types/node': specifier: ^22.13.4 version: 22.18.0 + '@types/pg': + specifier: ^8.11.10 + version: 8.20.0 '@types/react': specifier: ^19 version: 19.1.12 @@ -2288,6 +2315,9 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@base-org/account@1.1.1': resolution: {integrity: sha512-IfVJPrDPhHfqXRDb89472hXkpvJuQQR7FDI9isLPHEqSYt/45whIoBxSPgZ0ssTt379VhQo4+87PWI1DoLSfAQ==} @@ -2413,6 +2443,9 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@ecies/ciphers@0.2.4': resolution: {integrity: sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} @@ -2435,102 +2468,308 @@ packages: resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==} engines: {node: '>=18'} + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.9': resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} 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.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.9': resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} 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.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.9': resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} 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.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.9': resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} 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.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.9': resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} 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.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.9': resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} 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.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.9': resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} 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.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.9': resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} 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.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.9': resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} 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.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.9': resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} 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.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.9': resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} 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.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.9': resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} 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.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.9': resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} 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.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.9': resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} 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.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.9': resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} 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.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.9': resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} 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.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.9': resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} @@ -2543,6 +2782,18 @@ packages: 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.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.9': resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} @@ -2555,6 +2806,18 @@ packages: 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.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.9': resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} @@ -2567,24 +2830,72 @@ packages: 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.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.9': resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} 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.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.9': resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} 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.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.9': resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} 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.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.9': resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} @@ -2736,6 +3047,10 @@ packages: '@fastify/ajv-compiler@4.0.5': resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@fastify/error@4.2.0': resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} @@ -3151,6 +3466,9 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@neondatabase/serverless@0.10.4': + resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==} + '@next/env@16.0.10': resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} @@ -3299,6 +3617,9 @@ packages: peerDependencies: algosdk: ^3.5.2 + '@petamoriken/float16@3.9.3': + resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -4904,6 +5225,9 @@ packages: '@tanstack/store@0.8.0': resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} + '@testcontainers/postgresql@10.28.0': + resolution: {integrity: sha512-NN25rruG5D4Q7pCNIJuHwB+G85OSeJ3xHZ2fWx0O6sPoPEfCYwvpj8mq99cyn68nxFkFYZeyrZJtSFO+FnydiA==} + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -4965,6 +5289,12 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@3.3.47': + resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -5010,6 +5340,9 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@22.18.0': resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==} @@ -5019,6 +5352,12 @@ packages: '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/pg@8.11.6': + resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -5042,6 +5381,15 @@ packages: '@types/serve-static@1.15.8': resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} + + '@types/ssh2@0.5.52': + resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} + + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -5661,6 +6009,14 @@ packages: apg-js@4.4.0: resolution: {integrity: sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q==} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + are-docs-informative@0.0.2: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} @@ -5717,6 +6073,9 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + asn1js@3.0.6: resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} engines: {node: '>=12.0.0'} @@ -5732,9 +6091,15 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + async-mutex@0.2.6: resolution: {integrity: sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -5768,6 +6133,14 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + babel-plugin-polyfill-corejs2@0.4.14: resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: @@ -5789,6 +6162,47 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.1: + resolution: {integrity: sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.9.1: + resolution: {integrity: sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.13.1: + resolution: {integrity: sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.3: + resolution: {integrity: sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==} + base-x@3.0.11: resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} @@ -5802,6 +6216,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -5825,6 +6242,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blakejs@1.2.1: resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} @@ -5887,9 +6307,16 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -5897,12 +6324,20 @@ packages: resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} engines: {node: '>=6.14.2'} + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} peerDependencies: esbuild: '>=0.18' + byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -5972,6 +6407,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chrome-launcher@0.15.2: resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} engines: {node: '>=12.13.0'} @@ -6056,6 +6494,10 @@ packages: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -6122,11 +6564,19 @@ packages: typescript: optional: true + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} hasBin: true + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cross-fetch@3.2.0: resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} @@ -6358,6 +6808,18 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + docker-compose@0.24.8: + resolution: {integrity: sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==} + engines: {node: '>= 6.0.0'} + + docker-modem@5.0.7: + resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} + engines: {node: '>= 8.0'} + + dockerode@4.0.12: + resolution: {integrity: sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==} + engines: {node: '>= 8.0'} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -6386,6 +6848,102 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} + drizzle-kit@0.30.6: + resolution: {integrity: sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==} + hasBin: true + + drizzle-orm@0.36.4: + resolution: {integrity: sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=3' + '@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' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/react': '>=18' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + react: '>=18' + 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/react': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + react: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -6452,6 +7010,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -6505,6 +7067,21 @@ packages: es6-promisify@5.0.0: resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.9: resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} @@ -6703,6 +7280,9 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -6766,6 +7346,9 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -6931,6 +7514,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -6954,6 +7540,11 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gel@2.2.0: + resolution: {integrity: sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==} + engines: {node: '>= 18.0.0'} + hasBin: true + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6966,6 +7557,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-port@7.2.0: + resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==} + engines: {node: '>=16'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -7370,6 +7965,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + isomorphic-ws@4.0.1: resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} peerDependencies: @@ -7569,6 +8168,10 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -7908,6 +8511,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -7927,6 +8534,9 @@ packages: typescript: optional: true + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -7974,6 +8584,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nan@2.27.0: + resolution: {integrity: sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -8103,6 +8716,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + ofetch@1.4.1: resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} @@ -8287,6 +8903,48 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg-types@4.1.0: + resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==} + engines: {node: '>=10'} + + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -8429,6 +9087,41 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + preact@10.24.2: resolution: {integrity: sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==} @@ -8469,12 +9162,23 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + promise@8.3.0: resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + properties-reader@2.3.0: + resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} + engines: {node: '>=14'} + protobufjs@7.5.4: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} @@ -8633,6 +9337,13 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -8715,6 +9426,10 @@ packages: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -8885,6 +9600,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -8948,6 +9666,9 @@ packages: spdx-license-ids@3.0.22: resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + split-on-first@1.1.0: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} @@ -8959,6 +9680,13 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + ssh-remote-port-forward@1.0.4: + resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -9000,6 +9728,9 @@ packages: stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + streamx@2.25.0: + resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -9127,6 +9858,22 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-fs@3.1.2: + resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -9136,6 +9883,12 @@ packages: engines: {node: '>=10'} hasBin: true + testcontainers@10.28.0: + resolution: {integrity: sha512-1fKrRRCsgAQNkarjHCMKzBKXSJFmzNTiTbhb5E/j5hflRXChEtHvkefjaHlgkNUjfw92/Dq8LTgwQn6RDBFbMg==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + text-encoding-utf-8@1.0.2: resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} @@ -9199,6 +9952,10 @@ packages: resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} hasBin: true + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -9339,6 +10096,9 @@ packages: tweetnacl-ts@1.0.3: resolution: {integrity: sha512-C5I/dWf6xjAXaCDlf84T4HvozU/8ycAlq5WRllF1hAeeq5390tfXD+bNas5bhEV0HMSOx8bsQYpLjPl8wfnEeQ==} + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} @@ -9405,6 +10165,9 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -9414,6 +10177,10 @@ packages: undici-types@7.22.0: resolution: {integrity: sha512-RKZvifiL60xdsIuC80UY0dq8Z7DbJUV8/l2hOVbyZAxBzEeQU4Z58+4ZzJ6WN2Lidi9KzT5EbiGX+PI/UGYuRw==} + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -9542,6 +10309,11 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -9769,6 +10541,11 @@ packages: engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -9918,6 +10695,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -10832,6 +11613,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@balena/dockerignore@1.0.2': {} + '@base-org/account@1.1.1(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 @@ -11228,6 +12011,8 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@drizzle-team/brocli@0.10.2': {} + '@ecies/ciphers@0.2.4(@noble/ciphers@1.3.0)': dependencies: '@noble/ciphers': 1.3.0 @@ -11261,79 +12046,224 @@ snapshots: esquery: 1.6.0 jsdoc-type-pratt-parser: 4.1.0 + '@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.10.1 + + '@esbuild/aix-ppc64@0.19.12': + optional: true + '@esbuild/aix-ppc64@0.25.9': optional: true + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + '@esbuild/android-arm64@0.25.9': optional: true + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + '@esbuild/android-arm@0.25.9': optional: true + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + '@esbuild/android-x64@0.25.9': optional: true + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + '@esbuild/darwin-arm64@0.25.9': optional: true + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + '@esbuild/darwin-x64@0.25.9': optional: true + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + '@esbuild/freebsd-arm64@0.25.9': optional: true + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + '@esbuild/freebsd-x64@0.25.9': optional: true + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + '@esbuild/linux-arm64@0.25.9': optional: true - '@esbuild/linux-arm@0.25.9': + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + + '@esbuild/linux-arm@0.25.9': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + + '@esbuild/linux-ia32@0.25.9': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + + '@esbuild/linux-loong64@0.25.9': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + + '@esbuild/linux-mips64el@0.25.9': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + + '@esbuild/linux-ppc64@0.25.9': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + + '@esbuild/linux-riscv64@0.25.9': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + + '@esbuild/linux-s390x@0.25.9': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + + '@esbuild/linux-x64@0.25.9': + optional: true + + '@esbuild/netbsd-arm64@0.25.9': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + + '@esbuild/netbsd-x64@0.25.9': + optional: true + + '@esbuild/openbsd-arm64@0.25.9': optional: true - '@esbuild/linux-ia32@0.25.9': + '@esbuild/openbsd-x64@0.18.20': optional: true - '@esbuild/linux-loong64@0.25.9': + '@esbuild/openbsd-x64@0.19.12': optional: true - '@esbuild/linux-mips64el@0.25.9': + '@esbuild/openbsd-x64@0.25.9': optional: true - '@esbuild/linux-ppc64@0.25.9': + '@esbuild/openharmony-arm64@0.25.9': optional: true - '@esbuild/linux-riscv64@0.25.9': + '@esbuild/sunos-x64@0.18.20': optional: true - '@esbuild/linux-s390x@0.25.9': + '@esbuild/sunos-x64@0.19.12': optional: true - '@esbuild/linux-x64@0.25.9': + '@esbuild/sunos-x64@0.25.9': optional: true - '@esbuild/netbsd-arm64@0.25.9': + '@esbuild/win32-arm64@0.18.20': optional: true - '@esbuild/netbsd-x64@0.25.9': + '@esbuild/win32-arm64@0.19.12': optional: true - '@esbuild/openbsd-arm64@0.25.9': + '@esbuild/win32-arm64@0.25.9': optional: true - '@esbuild/openbsd-x64@0.25.9': + '@esbuild/win32-ia32@0.18.20': optional: true - '@esbuild/openharmony-arm64@0.25.9': + '@esbuild/win32-ia32@0.19.12': optional: true - '@esbuild/sunos-x64@0.25.9': + '@esbuild/win32-ia32@0.25.9': optional: true - '@esbuild/win32-arm64@0.25.9': + '@esbuild/win32-x64@0.18.20': optional: true - '@esbuild/win32-ia32@0.25.9': + '@esbuild/win32-x64@0.19.12': optional: true '@esbuild/win32-x64@0.25.9': @@ -11590,6 +12520,8 @@ snapshots: ajv-formats: 3.0.1(ajv@8.17.1) fast-uri: 3.1.0 + '@fastify/busboy@2.1.1': {} + '@fastify/error@4.2.0': {} '@fastify/fast-json-stringify-compiler@5.0.3': @@ -12163,6 +13095,10 @@ snapshots: '@tybys/wasm-util': 0.10.0 optional: true + '@neondatabase/serverless@0.10.4': + dependencies: + '@types/pg': 8.11.6 + '@next/env@16.0.10': {} '@next/eslint-plugin-next@16.0.6': @@ -12275,6 +13211,8 @@ snapshots: - bufferutil - utf-8-validate + '@petamoriken/float16@3.9.3': {} + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -14856,6 +15794,15 @@ snapshots: '@tanstack/store@0.8.0': {} + '@testcontainers/postgresql@10.28.0': + dependencies: + testcontainers: 10.28.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + '@trysound/sax@0.2.0': {} '@txnlab/use-wallet@4.6.0(@blockshake/defly-connect@1.2.1(algosdk@3.5.2)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@perawallet/connect@1.5.2(algosdk@3.5.2)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@walletconnect/sign-client@2.23.9(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(algosdk@3.5.2)(lute-connect@1.7.0)': @@ -14899,6 +15846,17 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 22.19.17 + '@types/ssh2': 1.15.5 + + '@types/dockerode@3.3.47': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 22.19.17 + '@types/ssh2': 1.15.5 + '@types/estree@1.0.8': {} '@types/express-serve-static-core@5.0.7': @@ -14944,6 +15902,10 @@ snapshots: '@types/node@12.20.55': {} + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@22.18.0': dependencies: undici-types: 6.21.0 @@ -14957,6 +15919,18 @@ snapshots: undici-types: 6.19.8 optional: true + '@types/pg@8.11.6': + dependencies: + '@types/node': 22.19.17 + pg-protocol: 1.13.0 + pg-types: 4.1.0 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 22.19.17 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -14984,6 +15958,19 @@ snapshots: '@types/node': 22.19.17 '@types/send': 0.17.5 + '@types/ssh2-streams@0.1.13': + dependencies: + '@types/node': 22.19.17 + + '@types/ssh2@0.5.52': + dependencies: + '@types/node': 22.19.17 + '@types/ssh2-streams': 0.1.13 + + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + '@types/trusted-types@2.0.7': {} '@types/uuid@8.3.4': {} @@ -16438,6 +17425,30 @@ snapshots: apg-js@4.4.0: {} + archiver-utils@5.0.2: + dependencies: + glob: 10.4.5 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.2.0 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + are-docs-informative@0.0.2: {} argparse@1.0.10: @@ -16521,6 +17532,10 @@ snapshots: asap@2.0.6: {} + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + asn1js@3.0.6: dependencies: pvtsutils: 1.3.6 @@ -16533,10 +17548,14 @@ snapshots: async-function@1.0.0: {} + async-lock@1.4.1: {} + async-mutex@0.2.6: dependencies: tslib: 2.8.1 + async@3.2.6: {} + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} @@ -16575,6 +17594,8 @@ snapshots: axobject-query@4.1.0: {} + b4a@1.8.1: {} + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.3): dependencies: '@babel/compat-data': 7.28.0 @@ -16605,6 +17626,38 @@ snapshots: balanced-match@1.0.2: {} + bare-events@2.8.2: {} + + bare-fs@4.7.1: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.13.1(bare-events@2.8.2) + bare-url: 2.4.3 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.9.1: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.9.1 + + bare-stream@2.13.1(bare-events@2.8.2): + dependencies: + streamx: 2.25.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.3: + dependencies: + bare-path: 3.0.0 + base-x@3.0.11: dependencies: safe-buffer: 5.2.1 @@ -16615,6 +17668,10 @@ snapshots: base64-js@1.5.1: {} + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -16638,6 +17695,12 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + blakejs@1.2.1: {} bn.js@4.11.8: {} @@ -16725,8 +17788,15 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-crc32@1.0.0: {} + buffer-from@1.1.2: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -16736,11 +17806,16 @@ snapshots: dependencies: node-gyp-build: 4.8.4 + buildcheck@0.0.7: + optional: true + bundle-require@5.1.0(esbuild@0.25.9): dependencies: esbuild: 0.25.9 load-tsconfig: 0.2.5 + byline@5.0.0: {} + bytes@3.1.2: {} cac@6.7.14: {} @@ -16807,6 +17882,8 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@1.1.4: {} + chrome-launcher@0.15.2: dependencies: '@types/node': 22.19.17 @@ -16882,6 +17959,14 @@ snapshots: comment-parser@1.4.1: {} + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -16937,8 +18022,19 @@ snapshots: optionalDependencies: typescript: 5.9.2 + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.27.0 + optional: true + crc-32@1.2.2: {} + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + cross-fetch@3.2.0: dependencies: node-fetch: 2.7.0 @@ -17140,6 +18236,31 @@ snapshots: dependencies: path-type: 4.0.0 + docker-compose@0.24.8: + dependencies: + yaml: 2.8.1 + + docker-modem@5.0.7: + dependencies: + debug: 4.4.3 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@4.0.12: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.12.6 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7 + protobufjs: 7.5.4 + tar-fs: 2.1.4 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -17171,6 +18292,24 @@ snapshots: dotenv@8.6.0: {} + drizzle-kit@0.30.6: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.19.12 + esbuild-register: 3.6.0(esbuild@0.19.12) + gel: 2.2.0 + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.36.4(@neondatabase/serverless@0.10.4)(@types/pg@8.20.0)(@types/react@19.1.12)(pg@8.20.0)(react@19.2.3): + optionalDependencies: + '@neondatabase/serverless': 0.10.4 + '@types/pg': 8.20.0 + '@types/react': 19.1.12 + pg: 8.20.0 + react: 19.2.3 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -17249,6 +18388,8 @@ snapshots: entities@6.0.1: {} + env-paths@3.0.0: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -17370,6 +18511,64 @@ snapshots: dependencies: es6-promise: 4.2.8 + esbuild-register@3.6.0(esbuild@0.19.12): + dependencies: + debug: 4.4.3 + esbuild: 0.19.12 + transitivePeerDependencies: + - supports-color + + 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.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + esbuild@0.25.9: optionalDependencies: '@esbuild/aix-ppc64': 0.25.9 @@ -17735,6 +18934,12 @@ snapshots: eventemitter3@5.0.4: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -17842,6 +19047,8 @@ snapshots: fast-diff@1.3.0: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.1: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -18035,6 +19242,8 @@ snapshots: fresh@2.0.0: {} + fs-constants@1.0.0: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -18063,6 +19272,17 @@ snapshots: functions-have-names@1.2.3: {} + gel@2.2.0: + dependencies: + '@petamoriken/float16': 3.9.3 + debug: 4.4.3 + env-paths: 3.0.0 + semver: 7.7.4 + shell-quote: 1.8.3 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -18080,6 +19300,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-port@7.2.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -18492,6 +19714,8 @@ snapshots: isexe@2.0.0: {} + isexe@3.1.5: {} + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -18731,6 +19955,10 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + leven@3.1.0: {} levn@0.4.1: @@ -19133,6 +20361,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -19145,6 +20377,8 @@ snapshots: optionalDependencies: typescript: 5.9.2 + mkdirp-classic@0.5.3: {} + mkdirp@1.0.4: {} mlly@1.8.0: @@ -19184,6 +20418,9 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nan@2.27.0: + optional: true + nanoid@3.3.11: {} napi-postinstall@0.3.3: {} @@ -19302,6 +20539,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obuf@1.1.2: {} + ofetch@1.4.1: dependencies: destr: 2.0.5 @@ -19535,6 +20774,53 @@ snapshots: pathval@2.0.1: {} + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + + pg-int8@1.0.1: {} + + pg-numeric@1.0.2: {} + + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg-types@4.1.0: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.4 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -19696,6 +20982,28 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.1: {} + + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + + postgres-date@1.0.7: {} + + postgres-date@2.1.0: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} + preact@10.24.2: {} preact@10.29.1: {} @@ -19724,6 +21032,8 @@ snapshots: process-warning@5.0.0: {} + process@0.11.10: {} + promise@8.3.0: dependencies: asap: 2.0.6 @@ -19734,6 +21044,16 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + properties-reader@2.3.0: + dependencies: + mkdirp: 1.0.4 + protobufjs@7.5.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -19995,6 +21315,18 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + readdirp@4.1.2: {} real-require@0.1.0: {} @@ -20076,6 +21408,8 @@ snapshots: ret@0.5.0: {} + retry@0.12.0: {} + reusify@1.1.0: {} rfc4648@1.5.3: {} @@ -20345,6 +21679,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} slash@3.0.0: {} @@ -20413,12 +21749,27 @@ snapshots: spdx-license-ids@3.0.22: {} + split-ca@1.0.1: {} + split-on-first@1.1.0: {} split2@4.2.0: {} sprintf-js@1.0.3: {} + ssh-remote-port-forward@1.0.4: + dependencies: + '@types/ssh2': 0.5.52 + ssh2: 1.17.0 + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.27.0 + stable-hash@0.0.5: {} stackback@0.0.2: {} @@ -20450,6 +21801,15 @@ snapshots: stream-shift@1.0.3: {} + streamx@2.25.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + strict-uri-encode@2.0.0: {} string-width@4.2.3: @@ -20597,6 +21957,51 @@ snapshots: tapable@2.3.0: {} + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-fs@3.1.2: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.0 + optionalDependencies: + bare-fs: 4.7.1 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.7.1 + fast-fifo: 1.3.2 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + teex@1.0.1: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + term-size@2.2.1: {} terser@5.46.1: @@ -20606,6 +22011,35 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + testcontainers@10.28.0: + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 3.3.47 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.4.3 + docker-compose: 0.24.8 + dockerode: 4.0.12 + get-port: 7.2.0 + proper-lockfile: 4.1.2 + properties-reader: 2.3.0 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.1.2 + tmp: 0.2.5 + undici: 5.29.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + text-encoding-utf-8@1.0.2: {} thenify-all@1.6.0: @@ -20664,6 +22098,8 @@ snapshots: tldts-core: 7.0.28 optional: true + tmp@0.2.5: {} + tmpl@1.0.5: {} to-buffer@1.2.2: @@ -20798,6 +22234,8 @@ snapshots: dependencies: tslib: 1.14.1 + tweetnacl@0.14.5: {} + tweetnacl@1.0.3: {} type-check@0.4.0: @@ -20886,6 +22324,8 @@ snapshots: uncrypto@0.1.3: {} + undici-types@5.26.5: {} + undici-types@6.19.8: optional: true @@ -20893,6 +22333,10 @@ snapshots: undici-types@7.22.0: {} + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -20992,6 +22436,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@10.0.0: {} + uuid@8.3.2: {} uuid@9.0.1: {} @@ -21438,6 +22884,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.5 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -21559,6 +23009,12 @@ snapshots: yocto-queue@0.1.0: {} + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/typescript/site/app/facilitator/discovery/catalog.ts b/typescript/site/app/facilitator/discovery/catalog.ts new file mode 100644 index 0000000000..e75e5d57f6 --- /dev/null +++ b/typescript/site/app/facilitator/discovery/catalog.ts @@ -0,0 +1,53 @@ +/** + * Catalog singleton for the bazaar discovery service. + * + * - `BAZAAR_DATABASE_URL` set → Postgres-backed catalog (auto-migrate on boot). + * - `BAZAAR_DATABASE_URL` unset → in-memory catalog with a warning. Useful for + * local dev without a database; cataloged resources are lost on cold start. + * + * Lazy initialization mirrors `getFacilitator()` in [..](../index.ts) — the catalog + * is created on first access so dev preview deploys without env config don't + * crash at module load. + */ + +import type { BazaarCatalog } from "@x402/extensions"; +import { InMemoryBazaarCatalog } from "@x402/extensions"; + +let _catalogPromise: Promise | null = null; + +/** + * Builds the configured catalog. Selects Postgres if `BAZAAR_DATABASE_URL` is + * present; otherwise falls back to an in-memory catalog. + * + * @returns A configured `BazaarCatalog`. + */ +async function createCatalog(): Promise { + const url = process.env.BAZAAR_DATABASE_URL; + if (!url) { + console.warn( + "[bazaar] BAZAAR_DATABASE_URL unset — using in-memory catalog. " + + "Discovered resources will be lost on cold start.", + ); + return new InMemoryBazaarCatalog(); + } + + // Dynamic import so the in-memory path doesn't load drizzle/pg in tests. + const { createPostgresBazaarCatalog } = await import("@x402/extensions/bazaar/postgres"); + const handle = await createPostgresBazaarCatalog({ + connectionString: url, + migrate: true, + }); + return handle.catalog; +} + +/** + * Returns the singleton `BazaarCatalog`, initializing on first access. + * + * @returns A promise resolving to the configured catalog. + */ +export async function getBazaarCatalog(): Promise { + if (!_catalogPromise) { + _catalogPromise = createCatalog(); + } + return _catalogPromise; +} diff --git a/typescript/site/app/facilitator/discovery/resources/route.ts b/typescript/site/app/facilitator/discovery/resources/route.ts new file mode 100644 index 0000000000..f6defc2e94 --- /dev/null +++ b/typescript/site/app/facilitator/discovery/resources/route.ts @@ -0,0 +1,69 @@ +/** + * `GET /facilitator/discovery/resources` + * + * Lists x402 resources cataloged by the bazaar extension hook. Spec: + * https://github.com/x402-foundation/x402/blob/main/specs/extensions/bazaar.md + */ + +import type { ListDiscoveryResourcesParams } from "@x402/extensions"; +import { getBazaarCatalog } from "../catalog"; + +/** + * Reads, validates, and clamps the query params for the list endpoint. + * + * @param url - The incoming request URL. + * @returns A `ListDiscoveryResourcesParams` object ready for the catalog. + */ +function parseListParams(url: URL): ListDiscoveryResourcesParams { + const params: ListDiscoveryResourcesParams = {}; + const type = url.searchParams.get("type"); + const payTo = url.searchParams.get("payTo"); + const scheme = url.searchParams.get("scheme"); + const network = url.searchParams.get("network"); + const extensions = url.searchParams.get("extensions"); + const limitRaw = url.searchParams.get("limit"); + const offsetRaw = url.searchParams.get("offset"); + if (type) params.type = type; + if (payTo) params.payTo = payTo; + if (scheme) params.scheme = scheme; + if (network) params.network = network; + if (extensions) params.extensions = extensions; + if (limitRaw !== null) { + const n = Number(limitRaw); + if (!Number.isFinite(n)) throw new Error("limit must be a finite number"); + params.limit = n; + } + if (offsetRaw !== null) { + const n = Number(offsetRaw); + if (!Number.isFinite(n)) throw new Error("offset must be a finite number"); + params.offset = n; + } + return params; +} + +/** + * Handles GET requests. Returns the catalog list response or a 400 with a + * structured error matching the verify/settle route conventions. + * + * @param req - The incoming request. + * @returns The JSON response. + */ +export async function GET(req: Request) { + let params: ListDiscoveryResourcesParams; + try { + params = parseListParams(new URL(req.url)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return Response.json({ error: "invalid_query", errorMessage: message }, { status: 400 }); + } + + try { + const catalog = await getBazaarCatalog(); + const response = await catalog.list(params); + return Response.json(response); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error("[bazaar] /discovery/resources error:", message); + return Response.json({ error: "unexpected_error", errorMessage: message }, { status: 500 }); + } +} diff --git a/typescript/site/app/facilitator/discovery/search/route.ts b/typescript/site/app/facilitator/discovery/search/route.ts new file mode 100644 index 0000000000..7f7cd83a95 --- /dev/null +++ b/typescript/site/app/facilitator/discovery/search/route.ts @@ -0,0 +1,70 @@ +/** + * `GET /facilitator/discovery/search` + * + * Natural-language search across cataloged x402 resources. Spec: + * https://github.com/x402-foundation/x402/blob/main/specs/extensions/bazaar.md + */ + +import type { SearchDiscoveryResourcesParams } from "@x402/extensions"; +import { getBazaarCatalog } from "../catalog"; + +/** + * Reads, validates, and clamps the query params for the search endpoint. + * `query` is required; everything else mirrors the list filters. + * + * @param url - The incoming request URL. + * @returns A `SearchDiscoveryResourcesParams` object. + * @throws Error with a human-readable message when `query` is missing/empty. + */ +function parseSearchParams(url: URL): SearchDiscoveryResourcesParams { + const query = url.searchParams.get("query"); + if (!query || query.trim().length === 0) { + throw new Error("query is required"); + } + const params: SearchDiscoveryResourcesParams = { query }; + const type = url.searchParams.get("type"); + const payTo = url.searchParams.get("payTo"); + const scheme = url.searchParams.get("scheme"); + const network = url.searchParams.get("network"); + const extensions = url.searchParams.get("extensions"); + const limitRaw = url.searchParams.get("limit"); + const cursor = url.searchParams.get("cursor"); + if (type) params.type = type; + if (payTo) params.payTo = payTo; + if (scheme) params.scheme = scheme; + if (network) params.network = network; + if (extensions) params.extensions = extensions; + if (limitRaw !== null) { + const n = Number(limitRaw); + if (!Number.isFinite(n)) throw new Error("limit must be a finite number"); + params.limit = n; + } + if (cursor) params.cursor = cursor; + return params; +} + +/** + * Handles GET requests. Mirrors the resources route's error envelope. + * + * @param req - The incoming request. + * @returns The JSON response. + */ +export async function GET(req: Request) { + let params: SearchDiscoveryResourcesParams; + try { + params = parseSearchParams(new URL(req.url)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return Response.json({ error: "invalid_query", errorMessage: message }, { status: 400 }); + } + + try { + const catalog = await getBazaarCatalog(); + const response = await catalog.search(params); + return Response.json(response); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error("[bazaar] /discovery/search error:", message); + return Response.json({ error: "unexpected_error", errorMessage: message }, { status: 500 }); + } +} diff --git a/typescript/site/app/facilitator/index.ts b/typescript/site/app/facilitator/index.ts index 93e6c93616..27d9571400 100644 --- a/typescript/site/app/facilitator/index.ts +++ b/typescript/site/app/facilitator/index.ts @@ -13,7 +13,9 @@ import { UptoEvmScheme } from "@x402/evm/upto/facilitator"; import { EIP2612_GAS_SPONSORING, createErc20ApprovalGasSponsoringExtension, + installBazaarFacilitator, } from "@x402/extensions"; +import { getBazaarCatalog } from "./discovery/catalog"; import { createEd25519Signer } from "@x402/stellar"; import { ExactStellarScheme } from "@x402/stellar/exact/facilitator"; import { toFacilitatorSvmSigner } from "@x402/svm"; @@ -186,6 +188,12 @@ async function createFacilitator(): Promise { .registerExtension(EIP2612_GAS_SPONSORING) .registerExtension(createErc20ApprovalGasSponsoringExtension(erc20ApprovalSigner)); + // Install bazaar discovery: registers the extension marker and an + // onAfterVerify hook that catalogs each verified payment that carries a + // bazaar declaration. Cataloging is best-effort — failures are logged but + // never propagate to the verify response. + installBazaarFacilitator(facilitator, await getBazaarCatalog()); + return facilitator; } diff --git a/typescript/site/package.json b/typescript/site/package.json index 38aea7a1b6..c5513bb1a8 100644 --- a/typescript/site/package.json +++ b/typescript/site/package.json @@ -31,6 +31,8 @@ "@x402/stellar": "workspace:*", "@x402/svm": "workspace:*", "@algorandfoundation/algokit-utils": "10.0.0-alpha.46", + "drizzle-orm": "^0.36.4", + "pg": "^8.13.1", "lottie-react": "^2.4.1", "motion": "^11.18.0", "next": "^16.0.10", @@ -46,6 +48,7 @@ "@svgr/webpack": "^8.1.0", "@tailwindcss/postcss": "^4.0.0", "@types/node": "^22.13.4", + "@types/pg": "^8.11.10", "@types/react": "^19", "@types/react-dom": "^19", "@typescript-eslint/eslint-plugin": "^8.29.1",