From 0ab1d665ddddfe4e20a88c65c687aeaeae8ff7af Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 5 May 2026 23:41:30 +1000 Subject: [PATCH] feat(bench): add @cipherstash/bench for index-engagement validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new private workspace package that runs encrypted query forms through EXPLAIN against a 10k-row fixture, asserting that each integration's emitted SQL engages the canonical EQL functional indexes (hmac_256 / bloom_filter / ste_vec) instead of falling back to seq scan. Drizzle adapter is in; encryptedSupabase and Prisma scaffold to follow. Two layers: - __tests__/ — vitest assertions on plan shape (cheap, CI-runnable). - __benches__/ — vitest --bench timings (on-demand). The drizzle/operators.explain.test.ts file currently fails on eq / inArray — that's the pre-fix repro for the bare-equality bug. The fix follows in a stacked branch. --- packages/bench/.gitignore | 1 + packages/bench/README.md | 49 ++++ .../__benches__/drizzle/operators.bench.ts | 69 +++++ packages/bench/__tests__/db-only.test.ts | 56 ++++ .../drizzle/operators.explain.test.ts | 222 ++++++++++++++++ packages/bench/__tests__/harness.test.ts | 36 +++ packages/bench/package.json | 25 ++ packages/bench/sql/schema.sql | 30 +++ packages/bench/src/cli/reset.ts | 16 ++ packages/bench/src/cli/setup.ts | 22 ++ packages/bench/src/drizzle/setup.ts | 80 ++++++ packages/bench/src/harness/db.ts | 34 +++ packages/bench/src/harness/explain.ts | 104 ++++++++ packages/bench/src/harness/seed.ts | 84 ++++++ packages/bench/tsconfig.json | 18 ++ packages/bench/vitest.config.ts | 18 ++ pnpm-lock.yaml | 251 +++++++++--------- 17 files changed, 985 insertions(+), 130 deletions(-) create mode 100644 packages/bench/.gitignore create mode 100644 packages/bench/README.md create mode 100644 packages/bench/__benches__/drizzle/operators.bench.ts create mode 100644 packages/bench/__tests__/db-only.test.ts create mode 100644 packages/bench/__tests__/drizzle/operators.explain.test.ts create mode 100644 packages/bench/__tests__/harness.test.ts create mode 100644 packages/bench/package.json create mode 100644 packages/bench/sql/schema.sql create mode 100644 packages/bench/src/cli/reset.ts create mode 100644 packages/bench/src/cli/setup.ts create mode 100644 packages/bench/src/drizzle/setup.ts create mode 100644 packages/bench/src/harness/db.ts create mode 100644 packages/bench/src/harness/explain.ts create mode 100644 packages/bench/src/harness/seed.ts create mode 100644 packages/bench/tsconfig.json create mode 100644 packages/bench/vitest.config.ts diff --git a/packages/bench/.gitignore b/packages/bench/.gitignore new file mode 100644 index 00000000..fbca2253 --- /dev/null +++ b/packages/bench/.gitignore @@ -0,0 +1 @@ +results/ diff --git a/packages/bench/README.md b/packages/bench/README.md new file mode 100644 index 00000000..af5c3e38 --- /dev/null +++ b/packages/bench/README.md @@ -0,0 +1,49 @@ +# @cipherstash/bench + +Performance / index-engagement benchmarks for stack integrations. + +This package validates that each integration emits SQL that engages the canonical +EQL functional indexes (`eql_v2.hmac_256`, `eql_v2.bloom_filter`, `eql_v2.ste_vec`) +on a Supabase-shaped install (no operator classes). It runs in two layers: + +1. **EXPLAIN-shape tests** (`__tests__/`) — vitest tests that assert on + `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` output. Pass/fail. Cheap. +2. **Wall-clock benches** (`__benches__/`) — vitest `--bench` (tinybench) + measuring median / p95 latency. On-demand; emits JSON to `results/`. + +## Prerequisites + +- Local Postgres + EQL via the repo-root `local/docker-compose.yml`: + ```bash + cd ../../local && docker compose up -d + ``` +- A CipherStash profile signed in (`stash login`). Auth is read from the + CipherStash profile; no environment variables required. +- `DATABASE_URL` only needs to be set if you want to override the default + (`postgres://cipherstash:password@localhost:5432/cipherstash`). + +## Run + +The bench package's tests are **developer-run only** — they're not invoked by +the repo's CI `test` step (the scripts are deliberately named `test:local` / +`bench:local` so turbo's default `test` task skips this package). + +```bash +# Credential-free smoke (verifies schema + EXPLAIN harness): +pnpm test:local -- db-only + +# Full suite (requires CipherStash auth via `stash login`, seeds 10k rows on first run): +pnpm db:setup # apply schema + seed BENCH_ROWS rows (default 10k) +pnpm test:local # EXPLAIN-shape assertions for #421 / #422 +pnpm bench:local # timing benches (slow) +pnpm db:reset # drop schema (keeps EQL install) +``` + +`__tests__/db-only.test.ts` only touches Postgres + the EQL install and is the +recommended starter — it's enough to verify the harness locally before wiring +auth. The other tests under `__tests__/` and the benches under `__benches__/` +use `@cipherstash/stack`'s `Encryption` client for real encryption. + +## Why this exists + +See cipherstash/stack issues #420, #421, #422. diff --git a/packages/bench/__benches__/drizzle/operators.bench.ts b/packages/bench/__benches__/drizzle/operators.bench.ts new file mode 100644 index 00000000..7c931bc6 --- /dev/null +++ b/packages/bench/__benches__/drizzle/operators.bench.ts @@ -0,0 +1,69 @@ +import { createEncryptionOperators } from '@cipherstash/stack/drizzle' +import type { SQL } from 'drizzle-orm' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { + type BenchHandle, + benchTable, + buildBench, + teardownBench, +} from '../../src/drizzle/setup.js' +import { applySchema } from '../../src/harness/db.js' +import { seed } from '../../src/harness/seed.js' + +let handle: BenchHandle +let ops: ReturnType + +beforeAll(async () => { + handle = await buildBench() + await applySchema(handle.pgClient) + await seed(handle) + ops = createEncryptionOperators(handle.encryptionClient) +}) + +afterAll(async () => { + if (handle) await teardownBench(handle) +}) + +/** + * Encryption cost is paid inside each iteration too — folding it into the + * timed loop reflects what customer code actually does, and the index + * engagement signal still dominates the differential between operators. + */ +describe('drizzle', () => { + bench('eq (string match)', async () => { + const where = (await ops.eq(benchTable.encText, 'value-0000042')) as SQL + await handle.db.select().from(benchTable).where(where) + }) + + bench('inArray (3 string matches)', async () => { + const where = await ops.inArray(benchTable.encText, [ + 'value-0000042', + 'value-0000123', + 'value-0000999', + ]) + await handle.db.select().from(benchTable).where(where) + }) + + bench('like (prefix)', async () => { + const where = (await ops.like(benchTable.encText, '%value-00000%')) as SQL + await handle.db.select().from(benchTable).where(where) + }) + + bench('gt (int)', async () => { + const where = (await ops.gt(benchTable.encInt, 9990)) as SQL + await handle.db.select().from(benchTable).where(where) + }) + + bench('between (int)', async () => { + const where = (await ops.between(benchTable.encInt, 4000, 4100)) as SQL + await handle.db.select().from(benchTable).where(where) + }) + + bench('orderBy desc + limit 10', async () => { + await handle.db + .select() + .from(benchTable) + .orderBy(ops.desc(benchTable.encInt)) + .limit(10) + }) +}) diff --git a/packages/bench/__tests__/db-only.test.ts b/packages/bench/__tests__/db-only.test.ts new file mode 100644 index 00000000..ee03cad1 --- /dev/null +++ b/packages/bench/__tests__/db-only.test.ts @@ -0,0 +1,56 @@ +/** + * DB-only smoke tests — exercise the schema/mode/EXPLAIN path against the + * existing local-postgres container without requiring CipherStash credentials. + * The seed/encryption path is covered separately by `harness.test.ts`, which + * does require credentials. + */ +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { applySchema, connect, countBenchRows } from '../src/harness/db.js' +import { explain, hasNodeType, summarize } from '../src/harness/explain.js' +import type pg from 'pg' + +let client: pg.Client + +beforeAll(async () => { + client = await connect() + await applySchema(client) +}) + +afterAll(async () => { + if (client) await client.end() +}) + +describe('db-only harness', () => { + it('schema applied (bench table exists, count is 0)', async () => { + const rows = await countBenchRows(client) + expect(rows).toBe(0) + }) + + it('EXPLAIN parses a trivial plan', async () => { + const plan = await explain(client, 'SELECT id FROM bench LIMIT 1', [], { + analyze: false, + }) + expect(plan['Node Type']).toBeTruthy() + expect(typeof summarize(plan)).toBe('string') + }) + + it('functional indexes exist after schema apply', async () => { + const res = await client.query<{ indexname: string }>( + `SELECT indexname FROM pg_indexes WHERE tablename = 'bench' ORDER BY indexname`, + ) + const names = res.rows.map((r) => r.indexname) + expect(names).toContain('bench_text_hmac_idx') + expect(names).toContain('bench_text_bloom_idx') + expect(names).toContain('bench_jsonb_stevec_idx') + }) + + it('plan walker traverses nested Plans nodes', async () => { + const plan = await explain( + client, + 'SELECT b1.id FROM bench b1 JOIN bench b2 ON b1.id = b2.id LIMIT 1', + [], + { analyze: false }, + ) + expect(hasNodeType(plan, 'Limit')).toBe(true) + }) +}) diff --git a/packages/bench/__tests__/drizzle/operators.explain.test.ts b/packages/bench/__tests__/drizzle/operators.explain.test.ts new file mode 100644 index 00000000..66392f9b --- /dev/null +++ b/packages/bench/__tests__/drizzle/operators.explain.test.ts @@ -0,0 +1,222 @@ +import { writeFileSync, mkdirSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { createEncryptionOperators } from '@cipherstash/stack/drizzle' +import type { SQL } from 'drizzle-orm' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { + type BenchHandle, + benchTable, + buildBench, + teardownBench, +} from '../../src/drizzle/setup.js' +import { + type PlanNode, + explain, + hasSeqScan, + summarize, + topScan, +} from '../../src/harness/explain.js' +import { applySchema } from '../../src/harness/db.js' +import { seed } from '../../src/harness/seed.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const resultsDir = resolve(__dirname, '..', '..', 'results') + +let handle: BenchHandle +let ops: ReturnType +const investigationLog: Record = { observations: {} } + +beforeAll(async () => { + handle = await buildBench() + await applySchema(handle.pgClient) + await seed(handle) + ops = createEncryptionOperators(handle.encryptionClient) +}) + +afterAll(async () => { + if (handle) await teardownBench(handle) + + // Persist #422 investigation outputs as a JSON artifact regardless of pass/fail. + try { + mkdirSync(resultsDir, { recursive: true }) + writeFileSync( + resolve(resultsDir, 'explain-shape.json'), + `${JSON.stringify(investigationLog, null, 2)}\n`, + ) + } catch (err) { + console.warn('[bench] failed to persist investigation log:', err) + } +}) + +/** + * Compile a Drizzle WHERE expression to SQL+params and run EXPLAIN against it. + * Wraps in a SELECT that touches the bench table so the planner has to make + * a decision on the encrypted column. + */ +async function explainWhere(where: SQL): Promise { + const query = handle.db.select().from(benchTable).where(where) + const compiled = query.toSQL() + return explain(handle.pgClient, compiled.sql, compiled.params as unknown[]) +} + +async function explainOrderBy(orderBy: SQL): Promise { + const query = handle.db.select().from(benchTable).orderBy(orderBy).limit(10) + const compiled = query.toSQL() + return explain(handle.pgClient, compiled.sql, compiled.params as unknown[]) +} + +function recordObservation(name: string, plan: PlanNode): void { + const scan = topScan(plan) + investigationLog.observations = { + ...(investigationLog.observations as Record), + [name]: { + summary: summarize(plan), + nodeType: scan?.['Node Type'], + indexName: scan?.['Index Name'] ?? null, + }, + } +} + +function recordError(name: string, err: unknown): void { + investigationLog.observations = { + ...(investigationLog.observations as Record), + [name]: { + error: err instanceof Error ? err.message : String(err), + }, + } +} + +/** + * Run a Drizzle WHERE-shaped expression through EXPLAIN, but if compiling or + * planning the query fails (e.g. the operator returns a non-boolean type), log + * the error to the investigation artifact instead of bubbling it. #422 tests + * must never block CI — they're observational. + */ +async function tryExplainWhere(name: string, where: SQL): Promise { + try { + const plan = await explainWhere(where) + recordObservation(name, plan) + } catch (err) { + recordError(name, err) + } +} + +// --- #421: equality + array operators ------------------------------------- +// +// `bench_text_hmac_idx` (functional hash on eql_v2.hmac_256) is the expected +// fast path. Pre-fix Drizzle emits bare `=` / `<>` / `IN (...)` which falls +// back to seq scan. Post-fix it emits `eql_v2.hmac_256(col) = +// eql_v2.hmac_256(value)` and the index scan kicks in. +// +// `eq` and `inArray` are naturally high-selectivity (only a few rows match), +// so the planner should pick the hmac index — assertion enforces it. +// +// `ne` and `notInArray` are naturally low-selectivity (almost all rows match); +// even with the hmac index available the planner correctly chooses a seq +// scan because it would re-touch nearly every row. We record their plans for +// the investigation log but don't assert — the SQL shape is what matters, +// and that's covered by the unit tests under packages/stack. +describe('#421: equality and array operators', () => { + it('eq engages the hmac functional index', async () => { + const plan = await explainWhere( + (await ops.eq(benchTable.encText, 'value-0000042')) as SQL, + ) + recordObservation('eq', plan) + expect(hasSeqScan(plan), summarize(plan)).toBe(false) + }) + + it('inArray engages the hmac functional index', async () => { + const plan = await explainWhere( + await ops.inArray(benchTable.encText, [ + 'value-0000042', + 'value-0000123', + 'value-0000999', + ]), + ) + recordObservation('inArray', plan) + expect(hasSeqScan(plan), summarize(plan)).toBe(false) + }) + + it('records ne plan shape (low-selectivity, not asserted)', async () => { + const plan = await explainWhere( + (await ops.ne(benchTable.encText, 'value-0000042')) as SQL, + ) + recordObservation('ne', plan) + }) + + it('records notInArray plan shape (low-selectivity, not asserted)', async () => { + const plan = await explainWhere( + await ops.notInArray(benchTable.encText, [ + 'value-0000042', + 'value-0000123', + ]), + ) + recordObservation('notInArray', plan) + }) +}) + +// --- #422: investigation operators ---------------------------------------- +// +// We don't yet know which call-shaped forms the planner inlines. Record plan +// shape; assertions land in a follow-up once #422 closes. +describe('#422: call-shaped operators (recorded, not asserted)', () => { + it('records like / ilike plan shapes', async () => { + await tryExplainWhere( + 'like', + (await ops.like(benchTable.encText, '%value-00000%')) as SQL, + ) + await tryExplainWhere( + 'ilike', + (await ops.ilike(benchTable.encText, '%VALUE-00000%')) as SQL, + ) + }) + + it('records gt / gte / lt / lte plan shapes', async () => { + for (const [name, build] of [ + ['gt', () => ops.gt(benchTable.encInt, 5000)], + ['gte', () => ops.gte(benchTable.encInt, 5000)], + ['lt', () => ops.lt(benchTable.encInt, 5000)], + ['lte', () => ops.lte(benchTable.encInt, 5000)], + ] as const) { + await tryExplainWhere(name, (await build()) as SQL) + } + }) + + it('records between plan shape', async () => { + await tryExplainWhere( + 'between', + (await ops.between(benchTable.encInt, 2500, 7500)) as SQL, + ) + }) + + it('records jsonb operator plan shapes', async () => { + for (const [name, build] of [ + [ + 'jsonbPathQueryFirst', + () => ops.jsonbPathQueryFirst(benchTable.encJsonb, '$.idx'), + ], + ['jsonbGet', () => ops.jsonbGet(benchTable.encJsonb, '$.idx')], + [ + 'jsonbPathExists', + () => ops.jsonbPathExists(benchTable.encJsonb, '$.idx'), + ], + ] as const) { + await tryExplainWhere(name, await build()) + } + }) + + it('records ORDER BY plan shape (asc / desc)', async () => { + for (const [name, build] of [ + ['asc', () => ops.asc(benchTable.encInt)], + ['desc', () => ops.desc(benchTable.encInt)], + ] as const) { + try { + const plan = await explainOrderBy(build()) + recordObservation(`orderBy_${name}`, plan) + } catch (err) { + recordError(`orderBy_${name}`, err) + } + } + }) +}) diff --git a/packages/bench/__tests__/harness.test.ts b/packages/bench/__tests__/harness.test.ts new file mode 100644 index 00000000..f7a67f13 --- /dev/null +++ b/packages/bench/__tests__/harness.test.ts @@ -0,0 +1,36 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { buildBench, teardownBench } from '../src/drizzle/setup.js' +import type { BenchHandle } from '../src/drizzle/setup.js' +import { applySchema, countBenchRows } from '../src/harness/db.js' +import { explain, summarize } from '../src/harness/explain.js' +import { getTargetRows, seed } from '../src/harness/seed.js' + +let handle: BenchHandle + +beforeAll(async () => { + handle = await buildBench() + await applySchema(handle.pgClient) + await seed(handle) +}) + +afterAll(async () => { + if (handle) await teardownBench(handle) +}) + +describe('bench harness smoke', () => { + it('applied schema and seeded the target row count', async () => { + const rows = await countBenchRows(handle.pgClient) + expect(rows).toBeGreaterThanOrEqual(getTargetRows()) + }) + + it('EXPLAIN parses a trivial plan', async () => { + const plan = await explain( + handle.pgClient, + 'SELECT id FROM bench LIMIT 1', + [], + { analyze: false }, + ) + expect(plan['Node Type']).toBeTruthy() + expect(typeof summarize(plan)).toBe('string') + }) +}) diff --git a/packages/bench/package.json b/packages/bench/package.json new file mode 100644 index 00000000..d51f9573 --- /dev/null +++ b/packages/bench/package.json @@ -0,0 +1,25 @@ +{ + "name": "@cipherstash/bench", + "version": "0.0.0", + "private": true, + "description": "Performance / index-engagement benchmarks for stack integrations (Drizzle, encryptedSupabase, Prisma).", + "type": "module", + "scripts": { + "db:setup": "tsx src/cli/setup.ts", + "db:reset": "tsx src/cli/reset.ts", + "test:local": "vitest run", + "bench:local": "vitest bench --run" + }, + "dependencies": { + "@cipherstash/stack": "workspace:*", + "drizzle-orm": "0.45.2", + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/node": "^22.15.12", + "@types/pg": "^8.15.0", + "tsx": "catalog:repo", + "typescript": "catalog:repo", + "vitest": "catalog:repo" + } +} diff --git a/packages/bench/sql/schema.sql b/packages/bench/sql/schema.sql new file mode 100644 index 00000000..cf861d2b --- /dev/null +++ b/packages/bench/sql/schema.sql @@ -0,0 +1,30 @@ +-- Bench fixture schema. +-- Single bench table covering text / int / jsonb encrypted columns plus the +-- three canonical EQL functional indexes: hmac_256 (hash), bloom_filter (GIN), +-- ste_vec (GIN). +-- +-- We deliberately do NOT create the `eql_v2.encrypted_operator_class` btree +-- indexes that ore-benches uses. Encrypted composites for full-feature columns +-- (equality + match + ORE) blow past the 2704-byte btree page-size limit, and +-- those indexes don't exist on Supabase anyway — the bench's whole job is to +-- validate that the functional-index path works. + +DROP TABLE IF EXISTS bench; + +CREATE TABLE bench ( + id SERIAL PRIMARY KEY, + enc_text eql_v2_encrypted NOT NULL, + enc_int eql_v2_encrypted NOT NULL, + enc_jsonb eql_v2_encrypted NOT NULL +); + +CREATE INDEX bench_text_hmac_idx + ON bench USING hash (eql_v2.hmac_256(enc_text)); + +CREATE INDEX bench_text_bloom_idx + ON bench USING gin (eql_v2.bloom_filter(enc_text)); + +CREATE INDEX bench_jsonb_stevec_idx + ON bench USING gin (eql_v2.ste_vec(enc_jsonb)); + +ANALYZE bench; diff --git a/packages/bench/src/cli/reset.ts b/packages/bench/src/cli/reset.ts new file mode 100644 index 00000000..ca96f921 --- /dev/null +++ b/packages/bench/src/cli/reset.ts @@ -0,0 +1,16 @@ +import { connect } from '../harness/db.js' + +async function main() { + const client = await connect() + try { + await client.query('DROP TABLE IF EXISTS bench') + console.log('[bench:reset] dropped bench table') + } finally { + await client.end() + } +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/packages/bench/src/cli/setup.ts b/packages/bench/src/cli/setup.ts new file mode 100644 index 00000000..2e5aadb0 --- /dev/null +++ b/packages/bench/src/cli/setup.ts @@ -0,0 +1,22 @@ +import { applySchema } from '../harness/db.js' +import { seed } from '../harness/seed.js' +import { buildBench, teardownBench } from '../drizzle/setup.js' + +async function main() { + const handle = await buildBench() + try { + console.log('[bench:setup] applying schema') + await applySchema(handle.pgClient) + + console.log('[bench:setup] seeding') + const result = await seed(handle) + console.log(`[bench:setup] done: ${JSON.stringify(result)}`) + } finally { + await teardownBench(handle) + } +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/packages/bench/src/drizzle/setup.ts b/packages/bench/src/drizzle/setup.ts new file mode 100644 index 00000000..8143ede5 --- /dev/null +++ b/packages/bench/src/drizzle/setup.ts @@ -0,0 +1,80 @@ +import { Encryption } from '@cipherstash/stack' +import type { EncryptionClient } from '@cipherstash/stack/encryption' +import { + encryptedType, + extractEncryptionSchema, +} from '@cipherstash/stack/drizzle' +import { drizzle } from 'drizzle-orm/node-postgres' +import { pgTable, serial } from 'drizzle-orm/pg-core' +import pg from 'pg' +import { getDatabaseUrl } from '../harness/db.js' + +/** + * Drizzle schema for the bench table. Mirrors `sql/schema.sql`. + * + * `id` is `serial`; the encrypted columns are `eql_v2_encrypted` composites + * driven by `@cipherstash/stack/drizzle`'s `encryptedType`. + * + * Index config flags (`equality`, `freeTextSearch`, `orderAndRange`, + * `searchableJson`) are deliberately all on — the bench needs to exercise + * every query family that lands on the table. + */ +export const benchTable = pgTable('bench', { + id: serial('id').primaryKey(), + encText: encryptedType('enc_text', { + equality: true, + freeTextSearch: true, + orderAndRange: true, + }), + encInt: encryptedType('enc_int', { + dataType: 'number', + equality: true, + orderAndRange: true, + }), + encJsonb: encryptedType<{ idx: number; group: number }>('enc_jsonb', { + dataType: 'json', + searchableJson: true, + }), +}) + +/** + * Encryption schema for the stack `Encryption()` client. Derived from the + * Drizzle table above so the two can't drift apart. + */ +export const encryptionBenchTable = extractEncryptionSchema(benchTable) + +export type BenchPlaintextRow = { + enc_text: string + enc_int: number + enc_jsonb: { idx: number; group: number } +} + +export type BenchHandle = { + pgClient: pg.Client + pool: pg.Pool + db: ReturnType + encryptionClient: EncryptionClient +} + +/** + * Spin up a single shared pg.Pool + Drizzle handle + Encryption client for + * the bench. Reuses one connection for EXPLAIN (so prepared-statement state + * is stable) and a pool for inserts. + */ +export async function buildBench(): Promise { + const connectionString = getDatabaseUrl() + const pool = new pg.Pool({ connectionString, max: 4 }) + const pgClient = new pg.Client({ connectionString }) + await pgClient.connect() + + const db = drizzle(pool) + + const encryptionClient = await Encryption({ schemas: [encryptionBenchTable] }) + + return { pgClient, pool, db, encryptionClient } +} + +export async function teardownBench(h: BenchHandle): Promise { + await h.pgClient.end() + await h.pool.end() +} diff --git a/packages/bench/src/harness/db.ts b/packages/bench/src/harness/db.ts new file mode 100644 index 00000000..a84a64e0 --- /dev/null +++ b/packages/bench/src/harness/db.ts @@ -0,0 +1,34 @@ +import { readFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import pg from 'pg' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) +const sqlDir = resolve(__dirname, '..', '..', 'sql') + +export const DEFAULT_DATABASE_URL = + 'postgres://cipherstash:password@localhost:5432/cipherstash' + +export function getDatabaseUrl(): string { + return process.env.DATABASE_URL ?? DEFAULT_DATABASE_URL +} + +export async function connect(): Promise { + const client = new pg.Client({ connectionString: getDatabaseUrl() }) + await client.connect() + return client +} + +export async function applySchema(client: pg.Client): Promise { + const path = resolve(sqlDir, 'schema.sql') + const sql = await readFile(path, 'utf8') + await client.query(sql) +} + +export async function countBenchRows(client: pg.Client): Promise { + const res = await client.query<{ count: string }>( + 'SELECT count(*)::text FROM bench', + ) + return Number(res.rows[0].count) +} diff --git a/packages/bench/src/harness/explain.ts b/packages/bench/src/harness/explain.ts new file mode 100644 index 00000000..d3387d26 --- /dev/null +++ b/packages/bench/src/harness/explain.ts @@ -0,0 +1,104 @@ +import type pg from 'pg' + +export type PlanNode = { + 'Node Type': string + 'Index Name'?: string + 'Relation Name'?: string + 'Actual Total Time'?: number + 'Total Cost'?: number + Plans?: PlanNode[] +} & Record + +type ExplainRow = { 'QUERY PLAN': [{ Plan: PlanNode }] } + +export type ExplainOptions = { + analyze?: boolean + buffers?: boolean +} + +/** + * Run EXPLAIN with FORMAT JSON and return the top-level plan node. + * Pass `analyze: false` for cheaper plan-shape checks that don't actually + * execute the query. + */ +export async function explain( + client: pg.Client, + sql: string, + params: unknown[] = [], + options: ExplainOptions = {}, +): Promise { + const { analyze = true, buffers = true } = options + const flags = [ + analyze ? 'ANALYZE' : null, + buffers && analyze ? 'BUFFERS' : null, + 'FORMAT JSON', + ] + .filter(Boolean) + .join(', ') + + const res = await client.query( + `EXPLAIN (${flags}) ${sql}`, + params as never[], + ) + return res.rows[0]['QUERY PLAN'][0].Plan +} + +export function walk(plan: PlanNode): PlanNode[] { + const out: PlanNode[] = [] + const stack: PlanNode[] = [plan] + while (stack.length > 0) { + const next = stack.pop() + if (!next) break + out.push(next) + if (next.Plans) stack.push(...next.Plans) + } + return out +} + +export function findNode( + plan: PlanNode, + predicate: (n: PlanNode) => boolean, +): PlanNode | null { + for (const node of walk(plan)) { + if (predicate(node)) return node + } + return null +} + +export function hasNodeType(plan: PlanNode, nodeType: string): boolean { + return findNode(plan, (n) => n['Node Type'] === nodeType) !== null +} + +export function hasSeqScan(plan: PlanNode): boolean { + return walk(plan).some( + (n) => + (n['Node Type'] === 'Seq Scan' || + n['Node Type'] === 'Parallel Seq Scan') && + n['Relation Name'] === 'bench', + ) +} + +export function usesIndex(plan: PlanNode, indexName: string): boolean { + return findNode(plan, (n) => n['Index Name'] === indexName) !== null +} + +/** + * Returns the first scan node touching the bench table — useful for printing + * a one-line plan summary in #422 investigation tests. + */ +export function topScan(plan: PlanNode): PlanNode | null { + return findNode( + plan, + (n) => + typeof n['Node Type'] === 'string' && + /Scan/.test(n['Node Type']) && + n['Relation Name'] === 'bench', + ) +} + +export function summarize(plan: PlanNode): string { + const scan = topScan(plan) + if (!scan) return plan['Node Type'] + const idx = scan['Index Name'] ? ` on ${scan['Index Name']}` : '' + return `${scan['Node Type']}${idx}` +} diff --git a/packages/bench/src/harness/seed.ts b/packages/bench/src/harness/seed.ts new file mode 100644 index 00000000..16172048 --- /dev/null +++ b/packages/bench/src/harness/seed.ts @@ -0,0 +1,84 @@ +import { + type BenchHandle, + type BenchPlaintextRow, + benchTable, + encryptionBenchTable, +} from '../drizzle/setup.js' +import { countBenchRows } from './db.js' + +export const DEFAULT_TARGET_ROWS = 10_000 +const INSERT_BATCH = 250 + +export type SeedResult = { + rowsBefore: number + rowsAfter: number + inserted: number + skipped: boolean +} + +export function getTargetRows(): number { + const raw = process.env.BENCH_ROWS + if (!raw) return DEFAULT_TARGET_ROWS + const n = Number.parseInt(raw, 10) + if (!Number.isFinite(n) || n <= 0) return DEFAULT_TARGET_ROWS + return n +} + +function makePlaintextRow(idx: number): BenchPlaintextRow { + return { + enc_text: `value-${String(idx).padStart(7, '0')}`, + enc_int: idx, + enc_jsonb: { idx, group: idx % 100 }, + } +} + +/** + * Idempotent seed. If `bench` already has >= target rows, returns without + * doing any work. Otherwise generates the deficit, encrypts in bulk via + * `protectClient.bulkEncryptModels`, and inserts in chunks. + */ +export async function seed( + h: BenchHandle, + targetRows: number = getTargetRows(), +): Promise { + const rowsBefore = await countBenchRows(h.pgClient) + + if (rowsBefore >= targetRows) { + return { rowsBefore, rowsAfter: rowsBefore, inserted: 0, skipped: true } + } + + const toInsert = targetRows - rowsBefore + const plaintexts: BenchPlaintextRow[] = [] + for (let i = 0; i < toInsert; i++) { + plaintexts.push(makePlaintextRow(rowsBefore + i)) + } + + const encResult = await h.encryptionClient.bulkEncryptModels( + plaintexts, + encryptionBenchTable, + ) + if (encResult.failure) { + throw new Error( + `[bench:seed] bulkEncryptModels failed: ${encResult.failure.message}`, + ) + } + + // bulkEncryptModels returns rows keyed by the encryptedTable column names + // (snake_case here). Drizzle's `benchTable` uses camelCase TS field names — + // remap before insert. + const encRows = encResult.data.map((r) => ({ + encText: r.enc_text as unknown as string, + encInt: r.enc_int as unknown as number, + encJsonb: r.enc_jsonb as unknown as { idx: number; group: number }, + })) + + for (let i = 0; i < encRows.length; i += INSERT_BATCH) { + const batch = encRows.slice(i, i + INSERT_BATCH) + await h.db.insert(benchTable).values(batch) + } + + await h.pgClient.query('ANALYZE bench') + + const rowsAfter = await countBenchRows(h.pgClient) + return { rowsBefore, rowsAfter, inserted: toInsert, skipped: false } +} diff --git a/packages/bench/tsconfig.json b/packages/bench/tsconfig.json new file mode 100644 index 00000000..6c5abb8d --- /dev/null +++ b/packages/bench/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": false, + "noEmit": true, + "isolatedModules": true, + "verbatimModuleSyntax": false + }, + "include": ["src/**/*", "__tests__/**/*", "__benches__/**/*"] +} diff --git a/packages/bench/vitest.config.ts b/packages/bench/vitest.config.ts new file mode 100644 index 00000000..c0f631c0 --- /dev/null +++ b/packages/bench/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['__tests__/**/*.test.ts'], + testTimeout: 300_000, + hookTimeout: 300_000, + pool: 'forks', + poolOptions: { + forks: { singleFork: true }, + }, + fileParallelism: false, + }, + benchmark: { + include: ['__benches__/**/*.bench.ts'], + outputJson: 'results/bench-results.json', + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7d9eea4..7484a9c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,6 +93,34 @@ importers: specifier: catalog:repo version: 5.6.3 + packages/bench: + dependencies: + '@cipherstash/stack': + specifier: workspace:* + version: link:../stack + drizzle-orm: + specifier: 0.45.2 + version: 0.45.2(@types/pg@8.16.0)(gel@2.2.0)(mysql2@3.16.0)(pg@8.16.3)(postgres@3.4.9) + pg: + specifier: ^8.16.3 + version: 8.16.3 + devDependencies: + '@types/node': + specifier: ^22.15.12 + version: 22.19.3 + '@types/pg': + specifier: ^8.15.0 + version: 8.16.0 + tsx: + specifier: catalog:repo + version: 4.19.3 + typescript: + specifier: catalog:repo + version: 5.6.3 + vitest: + specifier: catalog:repo + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.4) + packages/cli: dependencies: '@cipherstash/auth': @@ -137,7 +165,7 @@ importers: version: 7.2.0 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) tsx: specifier: catalog:repo version: 4.19.3 @@ -180,7 +208,7 @@ importers: version: 4.4.0 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) typescript: specifier: catalog:repo version: 5.6.3 @@ -208,7 +236,7 @@ importers: version: 8.13.1 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) typescript: specifier: catalog:repo version: 5.6.3 @@ -223,27 +251,27 @@ importers: version: 5.10.0 next: specifier: ^14 || ^15 - version: 15.5.15(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 15.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + optionalDependencies: + '@rollup/rollup-linux-x64-gnu': + specifier: 4.24.0 + version: 4.24.0 devDependencies: '@clerk/nextjs': specifier: catalog:security - version: 6.39.3(next@15.5.15(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 6.39.3(next@15.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) dotenv: specifier: ^16.4.7 version: 16.6.1 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) typescript: specifier: catalog:repo version: 5.6.3 vitest: specifier: catalog:repo version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.4) - optionalDependencies: - '@rollup/rollup-linux-x64-gnu': - specifier: 4.24.0 - version: 4.24.0 packages/protect: dependencies: @@ -265,6 +293,10 @@ importers: zod: specifier: ^3.24.2 version: 3.24.2 + optionalDependencies: + '@rollup/rollup-linux-x64-gnu': + specifier: 4.24.0 + version: 4.24.0 devDependencies: '@supabase/supabase-js': specifier: ^2.47.10 @@ -280,7 +312,7 @@ importers: version: 3.4.9 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) tsx: specifier: catalog:repo version: 4.19.3 @@ -290,10 +322,6 @@ importers: vitest: specifier: catalog:repo version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.4) - optionalDependencies: - '@rollup/rollup-linux-x64-gnu': - specifier: 4.24.0 - version: 4.24.0 packages/protect-dynamodb: dependencies: @@ -309,7 +337,7 @@ importers: version: 16.6.1 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) tsx: specifier: catalog:repo version: 4.19.3 @@ -328,7 +356,7 @@ importers: devDependencies: tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) typescript: specifier: catalog:repo version: 5.6.3 @@ -380,7 +408,7 @@ importers: version: 3.4.9 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) tsx: specifier: catalog:repo version: 4.19.3 @@ -423,7 +451,7 @@ importers: version: 8.16.0 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4) tsx: specifier: catalog:repo version: 4.19.3 @@ -481,28 +509,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@1.9.4': resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@1.9.4': resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@1.9.4': resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@1.9.4': resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} @@ -588,19 +612,16 @@ packages: resolution: {integrity: sha512-PDpm1EHC1XzVtEDGzcyr0UXNca8IFkfPusqqVJ5CSpzCtlYipIClYui197zQ4NGMHIAQD168IEFOK2TROyb4Tw==} cpu: [arm64] os: [linux] - libc: [glibc] '@cipherstash/auth-linux-x64-gnu@0.36.0': resolution: {integrity: sha512-Gm20ezVlGmNrkMH4s+I+JT13hDRD6vEX3fu3VDQQhWUiYCdgbdVsNJQgOr6QMY1cJkkmGyNlQKfiCPn4zlqtMg==} cpu: [x64] os: [linux] - libc: [glibc] '@cipherstash/auth-linux-x64-musl@0.36.0': resolution: {integrity: sha512-RUQeLc19JnURAMEoemP3+2DyptK+pqNFrVGgiKKOMVql0SZDVMlN2IyFrTKJ2emv1yuf4Gr1+E4jIdKPR0Oh+g==} cpu: [x64] os: [linux] - libc: [musl] '@cipherstash/auth-win32-x64-msvc@0.36.0': resolution: {integrity: sha512-1mQ8E6YFy7frHkvrDmSixpy47EakGPRh4qgoXPgk9lqZnlbMECYZhoKWQEs5wa3tLGgiX5G6jKC3NQZsOOqEfQ==} @@ -1124,8 +1145,8 @@ packages: cpu: [x64] os: [win32] - '@hono/node-server@1.19.13': - resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} + '@hono/node-server@1.19.12': + resolution: {integrity: sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 @@ -1160,105 +1181,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1330,57 +1335,53 @@ packages: '@neon-rs/load@0.1.82': resolution: {integrity: sha512-H4Gu2o5kPp+JOEhRrOQCnJnf7X6sv9FBLttM/wSbb4efsgFWeHzfU/ItZ01E5qqEk+U6QGdeVO7lxXIAtYHr5A==} - '@next/env@15.5.15': - resolution: {integrity: sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==} + '@next/env@15.5.10': + resolution: {integrity: sha512-plg+9A/KoZcTS26fe15LHg+QxReTazrIOoKKUC3Uz4leGGeNPgLHdevVraAAOX0snnUs3WkRx3eUQpj9mreG6A==} - '@next/swc-darwin-arm64@15.5.15': - resolution: {integrity: sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==} + '@next/swc-darwin-arm64@15.5.7': + resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.15': - resolution: {integrity: sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==} + '@next/swc-darwin-x64@15.5.7': + resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.15': - resolution: {integrity: sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==} + '@next/swc-linux-arm64-gnu@15.5.7': + resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] - '@next/swc-linux-arm64-musl@15.5.15': - resolution: {integrity: sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==} + '@next/swc-linux-arm64-musl@15.5.7': + resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] - '@next/swc-linux-x64-gnu@15.5.15': - resolution: {integrity: sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==} + '@next/swc-linux-x64-gnu@15.5.7': + resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] - '@next/swc-linux-x64-musl@15.5.15': - resolution: {integrity: sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==} + '@next/swc-linux-x64-musl@15.5.7': + resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] - '@next/swc-win32-arm64-msvc@15.5.15': - resolution: {integrity: sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==} + '@next/swc-win32-arm64-msvc@15.5.7': + resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.15': - resolution: {integrity: sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==} + '@next/swc-win32-x64-msvc@15.5.7': + resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1437,85 +1438,71 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.24.0': resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -2189,8 +2176,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hono@4.12.14: - resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} + hono@4.12.9: + resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} engines: {node: '>=16.9.0'} http-errors@2.0.1: @@ -2352,28 +2339,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -2412,8 +2395,8 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -2498,8 +2481,8 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - next@15.5.15: - resolution: {integrity: sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==} + next@15.5.10: + resolution: {integrity: sha512-r0X65PNwyDDyOrWNKpQoZvOatw7BcsTPRKdwEqtc9cj3wv7mbBIk9tKed4klRaFXJdX0rugpuMTHslDrAU1bBg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -2695,8 +2678,12 @@ packages: yaml: optional: true - postcss@8.5.10: - resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -3571,13 +3558,13 @@ snapshots: react-dom: 19.2.3(react@19.2.3) tslib: 2.8.1 - '@clerk/nextjs@6.39.3(next@15.5.15(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/nextjs@6.39.3(next@15.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@clerk/backend': 2.33.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@clerk/clerk-react': 5.61.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@clerk/shared': 3.47.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@clerk/types': 4.101.23(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - next: 15.5.15(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 15.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) server-only: 0.0.1 @@ -3832,9 +3819,9 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@hono/node-server@1.19.13(hono@4.12.14)': + '@hono/node-server@1.19.12(hono@4.12.9)': dependencies: - hono: 4.12.14 + hono: 4.12.9 '@img/colour@1.0.0': optional: true @@ -3980,7 +3967,7 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': dependencies: - '@hono/node-server': 1.19.13(hono@4.12.14) + '@hono/node-server': 1.19.12(hono@4.12.9) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -3990,7 +3977,7 @@ snapshots: eventsource-parser: 3.0.6 express: 5.2.1 express-rate-limit: 8.3.2(express@5.2.1) - hono: 4.12.14 + hono: 4.12.9 jose: 6.2.2 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -4002,30 +3989,30 @@ snapshots: '@neon-rs/load@0.1.82': {} - '@next/env@15.5.15': {} + '@next/env@15.5.10': {} - '@next/swc-darwin-arm64@15.5.15': + '@next/swc-darwin-arm64@15.5.7': optional: true - '@next/swc-darwin-x64@15.5.15': + '@next/swc-darwin-x64@15.5.7': optional: true - '@next/swc-linux-arm64-gnu@15.5.15': + '@next/swc-linux-arm64-gnu@15.5.7': optional: true - '@next/swc-linux-arm64-musl@15.5.15': + '@next/swc-linux-arm64-musl@15.5.7': optional: true - '@next/swc-linux-x64-gnu@15.5.15': + '@next/swc-linux-x64-gnu@15.5.7': optional: true - '@next/swc-linux-x64-musl@15.5.15': + '@next/swc-linux-x64-musl@15.5.7': optional: true - '@next/swc-win32-arm64-msvc@15.5.15': + '@next/swc-win32-arm64-msvc@15.5.7': optional: true - '@next/swc-win32-x64-msvc@15.5.15': + '@next/swc-win32-x64-msvc@15.5.7': optional: true '@nodelib/fs.scandir@2.1.5': @@ -4771,7 +4758,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hono@4.12.14: {} + hono@4.12.9: {} http-errors@2.0.1: dependencies: @@ -4859,7 +4846,7 @@ snapshots: '@types/lodash': 4.17.21 is-glob: 4.0.3 js-yaml: 4.1.1 - lodash: 4.18.1 + lodash: 4.17.23 minimist: 1.2.8 prettier: 3.7.4 tinyglobby: 0.2.15 @@ -4936,7 +4923,7 @@ snapshots: lodash.startcase@4.4.0: {} - lodash@4.18.1: {} + lodash@4.17.23: {} long@5.3.2: optional: true @@ -5011,24 +4998,24 @@ snapshots: negotiator@1.0.0: {} - next@15.5.15(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@15.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@next/env': 15.5.15 + '@next/env': 15.5.10 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001760 - postcss: 8.5.10 + postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) styled-jsx: 5.1.6(react@19.2.3) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.15 - '@next/swc-darwin-x64': 15.5.15 - '@next/swc-linux-arm64-gnu': 15.5.15 - '@next/swc-linux-arm64-musl': 15.5.15 - '@next/swc-linux-x64-gnu': 15.5.15 - '@next/swc-linux-x64-musl': 15.5.15 - '@next/swc-win32-arm64-msvc': 15.5.15 - '@next/swc-win32-x64-msvc': 15.5.15 + '@next/swc-darwin-arm64': 15.5.7 + '@next/swc-darwin-x64': 15.5.7 + '@next/swc-linux-arm64-gnu': 15.5.7 + '@next/swc-linux-arm64-musl': 15.5.7 + '@next/swc-linux-x64-gnu': 15.5.7 + '@next/swc-linux-x64-musl': 15.5.7 + '@next/swc-win32-arm64-msvc': 15.5.7 + '@next/swc-win32-x64-msvc': 15.5.7 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -5118,7 +5105,6 @@ snapshots: pg-pool@3.13.0(pg@8.16.3): dependencies: pg: 8.16.3 - optional: true pg-protocol@1.13.0: {} @@ -5149,7 +5135,6 @@ snapshots: pgpass: 1.0.5 optionalDependencies: pg-cloudflare: 1.3.0 - optional: true pgpass@1.0.5: dependencies: @@ -5167,16 +5152,22 @@ snapshots: pkce-challenge@5.0.1: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(yaml@2.8.4): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(yaml@2.8.4): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 - postcss: 8.5.10 + postcss: 8.5.6 tsx: 4.19.3 yaml: 2.8.4 - postcss@8.5.10: + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -5542,7 +5533,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.4.0(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4): + tsup@8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.4): dependencies: bundle-require: 5.1.0(esbuild@0.25.12) cac: 6.7.14 @@ -5552,7 +5543,7 @@ snapshots: esbuild: 0.25.12 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.19.3)(yaml@2.8.4) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(yaml@2.8.4) resolve-from: 5.0.0 rollup: 4.59.0 source-map: 0.8.0-beta.0 @@ -5561,7 +5552,7 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.10 + postcss: 8.5.6 typescript: 5.6.3 transitivePeerDependencies: - jiti @@ -5653,7 +5644,7 @@ snapshots: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.10 + postcss: 8.5.6 rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: