From b1f02d650cfa1ab106421e0e3ea23656ccb460ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:18:54 +0000 Subject: [PATCH 1/4] Initial plan From 0a3ed03b3d646f8129b4c24631c92c344f3279de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:42:59 +0000 Subject: [PATCH 2/4] feat: add ottasearch package, admin search UI, and spotlight integration Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com> --- apps/ottabase-template-app-tanstack/README.md | 8 + .../ottabase/db/schema.ts | 3 + .../ottabase/models/SearchDocument.ts | 8 + .../ottabase/models/SearchableModel.ts | 12 + .../ottabase/ottabase.config.ts | 10 +- .../package.json | 1 + .../src/pages/admin/AdminIndexPage.tsx | 8 + .../src/pages/admin/AdminSearchPage.tsx | 218 +++++++++ .../src/providers/Providers.tsx | 31 +- .../src/router.tsx | 11 + .../worker/lib/db-utils.ts | 4 +- .../worker/routes/ottasearch.ts | 460 ++++++++++++++++++ .../worker/routes/router.ts | 32 ++ packages/ottasearch/README.md | 29 ++ packages/ottasearch/package.json | 24 + .../ottasearch/src/__tests__/search.test.ts | 50 ++ packages/ottasearch/src/index.ts | 15 + packages/ottasearch/src/schema.ts | 25 + packages/ottasearch/src/search.ts | 69 +++ packages/ottasearch/src/types.ts | 17 + packages/ottasearch/tsconfig.json | 13 + packages/ottasearch/vitest.config.ts | 15 + pnpm-lock.yaml | 271 +++++++---- 23 files changed, 1240 insertions(+), 94 deletions(-) create mode 100644 apps/ottabase-template-app-tanstack/ottabase/models/SearchDocument.ts create mode 100644 apps/ottabase-template-app-tanstack/ottabase/models/SearchableModel.ts create mode 100644 apps/ottabase-template-app-tanstack/src/pages/admin/AdminSearchPage.tsx create mode 100644 apps/ottabase-template-app-tanstack/worker/routes/ottasearch.ts create mode 100644 packages/ottasearch/README.md create mode 100644 packages/ottasearch/package.json create mode 100644 packages/ottasearch/src/__tests__/search.test.ts create mode 100644 packages/ottasearch/src/index.ts create mode 100644 packages/ottasearch/src/schema.ts create mode 100644 packages/ottasearch/src/search.ts create mode 100644 packages/ottasearch/src/types.ts create mode 100644 packages/ottasearch/tsconfig.json create mode 100644 packages/ottasearch/vitest.config.ts diff --git a/apps/ottabase-template-app-tanstack/README.md b/apps/ottabase-template-app-tanstack/README.md index e1a088179..bc55725ce 100644 --- a/apps/ottabase-template-app-tanstack/README.md +++ b/apps/ottabase-template-app-tanstack/README.md @@ -10,6 +10,7 @@ TanStack Router + Query template with automated OttaORM migrations and Cloudflar - **Auth.js** - OAuth, Magic Link, and Credentials authentication - **Vite** - Fast development server and optimized builds - **Cloudflare Workers** - D1, KV, R2, Queues, Rate Limiting, Durable Objects +- **In-house Search** - `@ottabase/ottasearch` with D1 FTS + optional Vectorize semantic ranking - **Mantine + shadcn/ui** - Flexible UI component libraries - **Jotai** - Global state management @@ -31,6 +32,13 @@ curl -X POST http://localhost:3004/api/ottaorm/init # Done! Visit http://localhost:3003 (frontend) or http://localhost:3004 (when using dev:worker only) ``` +## Search setup (admin) + +- Open **`/admin/search`** to configure searchable models and index fields. +- Run **Reindex** once after migrations to initialize FTS and build search documents. +- Global spotlight (`/` shortcut) queries `/api/ottasearch/spotlight`. +- If Vectorize binding is missing, admin shows pending setup and search falls back to D1 FTS only. + ## Configuration ### Config Files diff --git a/apps/ottabase-template-app-tanstack/ottabase/db/schema.ts b/apps/ottabase-template-app-tanstack/ottabase/db/schema.ts index aa2aefc8d..1afe35338 100644 --- a/apps/ottabase-template-app-tanstack/ottabase/db/schema.ts +++ b/apps/ottabase-template-app-tanstack/ottabase/db/schema.ts @@ -33,6 +33,7 @@ import { postsTable, seriesTable, } from '@ottabase/ottablog'; +import { searchDocumentsTable, searchableModelsTable } from '@ottabase/ottasearch'; import { referralTrackingTable } from '@ottabase/referrals'; import { shortlinksTable } from '@ottabase/shortlinks'; @@ -54,6 +55,8 @@ export { postTagsTable, postVersionsTable, postsTable, + searchDocumentsTable, + searchableModelsTable, seriesTable, referralTrackingTable, shortlinksTable, diff --git a/apps/ottabase-template-app-tanstack/ottabase/models/SearchDocument.ts b/apps/ottabase-template-app-tanstack/ottabase/models/SearchDocument.ts new file mode 100644 index 000000000..48a04072e --- /dev/null +++ b/apps/ottabase-template-app-tanstack/ottabase/models/SearchDocument.ts @@ -0,0 +1,8 @@ +import { searchDocumentsTable } from '@ottabase/ottasearch'; +import { BaseModel } from '@ottabase/ottaorm'; + +export class SearchDocument extends BaseModel { + static entity = 'search_documents'; + static table = searchDocumentsTable; + static primaryKey = 'id'; +} diff --git a/apps/ottabase-template-app-tanstack/ottabase/models/SearchableModel.ts b/apps/ottabase-template-app-tanstack/ottabase/models/SearchableModel.ts new file mode 100644 index 000000000..6ab4e6732 --- /dev/null +++ b/apps/ottabase-template-app-tanstack/ottabase/models/SearchableModel.ts @@ -0,0 +1,12 @@ +import { searchableModelsTable } from '@ottabase/ottasearch'; +import { BaseModel } from '@ottabase/ottaorm'; + +export class SearchableModel extends BaseModel { + static entity = 'searchable_models'; + static table = searchableModelsTable; + static primaryKey = 'entityName'; + + static casts = { + enabled: 'boolean' as const, + }; +} diff --git a/apps/ottabase-template-app-tanstack/ottabase/ottabase.config.ts b/apps/ottabase-template-app-tanstack/ottabase/ottabase.config.ts index 0ac4bded6..9e3b44b75 100644 --- a/apps/ottabase-template-app-tanstack/ottabase/ottabase.config.ts +++ b/apps/ottabase-template-app-tanstack/ottabase/ottabase.config.ts @@ -12,6 +12,7 @@ // ============================================================ import { defineOttabaseConfig } from '@ottabase/config'; +import { searchDocumentsTable, searchableModelsTable } from '@ottabase/ottasearch'; export default defineOttabaseConfig({ // ── App Identity ────────────────────────────────────────── @@ -52,7 +53,14 @@ export default defineOttabaseConfig({ // customPackages: { // myPremiumFeature: { tables: { premiumTable } }, // }, - customPackages: {}, + customPackages: { + ottasearch: { + tables: { + searchableModelsTable, + searchDocumentsTable, + }, + }, + }, // ── Feature Configuration ───────────────────────────────── features: { diff --git a/apps/ottabase-template-app-tanstack/package.json b/apps/ottabase-template-app-tanstack/package.json index 853521291..d8477a07c 100644 --- a/apps/ottabase-template-app-tanstack/package.json +++ b/apps/ottabase-template-app-tanstack/package.json @@ -49,6 +49,7 @@ "@ottabase/ottamenu": "workspace:*", "@ottabase/ottaorm": "workspace:*", "@ottabase/ottarenderer": "workspace:*", + "@ottabase/ottasearch": "workspace:*", "@ottabase/ottaselect": "workspace:*", "@ottabase/ottaupload": "workspace:*", "@ottabase/queue": "workspace:*", diff --git a/apps/ottabase-template-app-tanstack/src/pages/admin/AdminIndexPage.tsx b/apps/ottabase-template-app-tanstack/src/pages/admin/AdminIndexPage.tsx index c91716a0c..14bdbe0bc 100644 --- a/apps/ottabase-template-app-tanstack/src/pages/admin/AdminIndexPage.tsx +++ b/apps/ottabase-template-app-tanstack/src/pages/admin/AdminIndexPage.tsx @@ -14,6 +14,7 @@ import { Palette, Shield, ShieldCheck, + Search, UserPlus, Users, Power, @@ -148,6 +149,13 @@ export function AdminIndexPage() { icon: Power, disabled: false, }, + { + title: 'Search Admin', + description: 'Manage searchable models, run D1 FTS indexing, and monitor semantic setup.', + href: '/admin/search', + icon: Search, + disabled: false, + }, { title: 'System Health', description: 'View system health metrics and API status.', diff --git a/apps/ottabase-template-app-tanstack/src/pages/admin/AdminSearchPage.tsx b/apps/ottabase-template-app-tanstack/src/pages/admin/AdminSearchPage.tsx new file mode 100644 index 000000000..c8ab55998 --- /dev/null +++ b/apps/ottabase-template-app-tanstack/src/pages/admin/AdminSearchPage.tsx @@ -0,0 +1,218 @@ +import { api } from '@/lib/api'; +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, + Switch, +} from '@ottabase/ui-shadcn'; +import { useEffect, useMemo, useState } from 'react'; +import { toast } from 'sonner'; + +type SearchModelConfig = { + entityName: string; + modelName: string; + tableName: string; + enabled: boolean; + fields: string[]; + lastIndexedAt: number | null; +}; + +type SearchStatus = { + ftsReady: boolean; + indexedDocuments: number; + enabledModels: number; + hasVectorize: boolean; + pending: string[]; +}; + +async function loadSearchConfig() { + return api<{ models: SearchModelConfig[] }>('/api/ottasearch/config'); +} + +async function loadSearchStatus() { + return api('/api/ottasearch/status'); +} + +export function AdminSearchPage() { + const [models, setModels] = useState([]); + const [status, setStatus] = useState(null); + const [busyEntity, setBusyEntity] = useState(null); + const [reindexing, setReindexing] = useState(false); + + const refresh = async () => { + const [configData, statusData] = await Promise.all([loadSearchConfig(), loadSearchStatus()]); + setModels(configData.models); + setStatus(statusData); + }; + + useEffect(() => { + void refresh(); + }, []); + + const pending = useMemo(() => status?.pending ?? [], [status]); + + const toggleModel = async (model: SearchModelConfig, enabled: boolean) => { + setBusyEntity(model.entityName); + try { + await api('/api/ottasearch/config', { + method: 'PUT', + body: { + entityName: model.entityName, + enabled, + fields: model.fields, + }, + }); + await refresh(); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Failed to update searchable model'); + } finally { + setBusyEntity(null); + } + }; + + const updateFields = async (model: SearchModelConfig, rawFields: string) => { + const fields = rawFields + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + + await api('/api/ottasearch/config', { + method: 'PUT', + body: { + entityName: model.entityName, + enabled: model.enabled, + fields, + }, + }); + await refresh(); + }; + + const runReindex = async (entityName?: string) => { + setReindexing(true); + try { + await api('/api/ottasearch/reindex', { + method: 'POST', + body: { + entityName, + }, + }); + await refresh(); + toast.success(entityName ? `Reindexed ${entityName}` : 'Search index refreshed'); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Reindex failed'); + } finally { + setReindexing(false); + } + }; + + return ( +
+
+

Search Admin

+

+ Configure searchable models and run in-house indexing (D1 FTS + optional Vectorize semantic + ranking). +

+
+ +
+ + + FTS Index + {status?.ftsReady ? 'Ready' : 'Pending setup'} + + + + + Indexed Documents + {status?.indexedDocuments ?? 0} + + + + + Enabled Models + {status?.enabledModels ?? 0} + + + + + Semantic (Vectorize) + {status?.hasVectorize ? 'Enabled' : 'Not configured'} + + +
+ + {pending.length > 0 && ( + + + Pending setup + Complete these from admin to fully activate in-house search. + + + {pending.map((item) => ( +
+ • {item} +
+ ))} +
+
+ )} + + + +
+ Searchable models + Choose models and fields to index into D1 FTS. +
+ +
+ + {models.map((model) => ( +
+
+
+
+ {model.entityName} + {model.tableName} +
+
{model.modelName}
+
+ toggleModel(model, checked)} + /> +
+ updateFields(model, event.target.value)} + /> +
+ + Last indexed:{' '} + {model.lastIndexedAt ? new Date(model.lastIndexedAt).toLocaleString() : 'Never'} + + +
+
+ ))} +
+
+
+ ); +} diff --git a/apps/ottabase-template-app-tanstack/src/providers/Providers.tsx b/apps/ottabase-template-app-tanstack/src/providers/Providers.tsx index 8212b9a2e..e4f5d6cf8 100644 --- a/apps/ottabase-template-app-tanstack/src/providers/Providers.tsx +++ b/apps/ottabase-template-app-tanstack/src/providers/Providers.tsx @@ -22,7 +22,7 @@ import { ApiError } from '@ottabase/api'; import { BrandProvider } from '@ottabase/brand-engine-react'; import { I18nProvider } from '@ottabase/i18n/react'; import { OttaQueryProvider } from '@ottabase/ottaorm/client'; -import { SpotlightProvider } from '@ottabase/spotlight'; +import { createApiSearchHandler, SpotlightProvider } from '@ottabase/spotlight'; import { ProviderState } from '@ottabase/state'; import { ProviderUIBase } from '@ottabase/ui-base'; import { ShadcnProviders } from '@ottabase/ui-shadcn/providers'; @@ -132,6 +132,32 @@ function ProvidersCore({ fontFamilies: { primary: string; heading: string; monospace: string }; queryConfig: { defaultOptions: { queries: { retry: (n: number, err: unknown) => boolean } } }; }) { + const spotlightSearch = React.useMemo( + () => + createApiSearchHandler<{ + id: string; + label: string; + description?: string; + href?: string; + keywords?: string[]; + }>({ + api, + endpoint: '/api/ottasearch/spotlight', + transform: (item) => ({ + id: item.id, + label: item.label, + description: item.description, + keywords: item.keywords, + onSelect: () => { + if (item.href && typeof window !== 'undefined') { + window.location.href = item.href; + } + }, + }), + }), + [api], + ); + return ( {children} diff --git a/apps/ottabase-template-app-tanstack/src/router.tsx b/apps/ottabase-template-app-tanstack/src/router.tsx index d8ef7b155..6c536eafa 100644 --- a/apps/ottabase-template-app-tanstack/src/router.tsx +++ b/apps/ottabase-template-app-tanstack/src/router.tsx @@ -712,6 +712,16 @@ const adminNotificationsRoute = new Route({ ), }); +const adminSearchRoute = new Route({ + getParentRoute: () => rootRoute, + path: '/admin/search', + component: lazyRouteComponent(() => + import('@/pages/admin/AdminSearchPage').then((m) => ({ + default: () => renderAdminRoute(), + })), + ), +}); + // Admin Blog routes const adminBlogRoute = new Route({ getParentRoute: () => rootRoute, @@ -999,6 +1009,7 @@ const coreRoutes = [ adminQueueRoute, adminCronRoute, adminNotificationsRoute, + adminSearchRoute, adminDbRoute, adminRBACRoute, adminRBACRolesRoute, diff --git a/apps/ottabase-template-app-tanstack/worker/lib/db-utils.ts b/apps/ottabase-template-app-tanstack/worker/lib/db-utils.ts index 7376f8502..25c850e45 100644 --- a/apps/ottabase-template-app-tanstack/worker/lib/db-utils.ts +++ b/apps/ottabase-template-app-tanstack/worker/lib/db-utils.ts @@ -27,6 +27,8 @@ import { ReferralTracking } from '@ottabase/referrals'; import { Shortlink } from '@ottabase/shortlinks'; import { errorResponse } from '@ottabase/utils/http-errors'; import { getOttabaseConfig } from '../../ottabase/config.loader'; +import { SearchDocument } from '../../ottabase/models/SearchDocument'; +import { SearchableModel } from '../../ottabase/models/SearchableModel'; import { Todo } from '../../ottabase/models/Todo'; import type { CloudflareEnv } from '../cloudflare-env'; import { readJson } from './utils'; @@ -98,7 +100,7 @@ export function initDbConnection(env: CloudflareEnv): void { ]; // Menu, MenuItem: use /api/brand/menus (cache-invalidating CRUD), not OttaORM const brandModels = [BrandKit, LayoutTemplate, LayoutRouteMapping, MenuSlotAssignment]; - const appModels = [Todo]; + const appModels = [Todo, SearchableModel, SearchDocument]; registerModels([...coreModels, ...ottablogModels, ...packageModels, ...brandModels, ...appModels]); diff --git a/apps/ottabase-template-app-tanstack/worker/routes/ottasearch.ts b/apps/ottabase-template-app-tanstack/worker/routes/ottasearch.ts new file mode 100644 index 000000000..96251b47c --- /dev/null +++ b/apps/ottabase-template-app-tanstack/worker/routes/ottasearch.ts @@ -0,0 +1,460 @@ +import { + collectDocumentText, + ensureFtsTable, + mergeHybridResults, + OTTASEARCH_FTS_TABLE, + parseJsonStringArray, +} from '@ottabase/ottasearch'; +import { getAllModelsMetadata } from '@ottabase/ottaorm'; +import { errorResponse } from '@ottabase/utils/http-errors'; +import { jsonResponse } from '@ottabase/utils/http-response'; +import { requireAdminAccess } from '../lib/admin-guard'; +import type { ApiRouteContext } from './router'; + +const DEFAULT_SEARCH_FIELDS = ['title', 'name', 'label', 'description', 'content', 'body', 'summary', 'slug']; +const EMBEDDING_MODEL = '@cf/baai/bge-base-en-v1.5'; + +function getModelMetadata() { + return Array.from(getAllModelsMetadata().entries()).map(([entityName, entry]) => ({ + entityName, + modelName: entry.metadata.modelName, + tableName: entry.metadata.tableName, + model: entry.model, + })); +} + +function buildSpotlightHref(entityName: string, recordId: string, row: Record): string { + if (entityName === 'posts') { + const slug = typeof row.slug === 'string' ? row.slug : recordId; + return `/blog/${slug}`; + } + if (entityName === 'shortlinks') return '/shortlinks'; + if (entityName === 'users') return '/admin/users'; + if (entityName.startsWith('referral')) return '/admin/referrals'; + return `/admin/db?table=${encodeURIComponent(entityName)}`; +} + +async function tableExists(context: ApiRouteContext, tableName: string): Promise { + const row = await context.env.OBCF_D1?.prepare( + `SELECT 1 as ok FROM sqlite_schema WHERE type='table' AND name = ? LIMIT 1`, + ) + .bind(tableName) + .first<{ ok: number }>(); + return Boolean(row?.ok); +} + +async function getVectorEmbedding(context: ApiRouteContext, text: string): Promise { + const ai = (context.env as { OBCF_AI?: { run?: (model: string, payload: unknown) => Promise } }).OBCF_AI; + if (!ai?.run) return null; + + try { + const response = await ai.run(EMBEDDING_MODEL, { text: [text] }); + const vector = + (response as { data?: unknown[] } | null)?.data?.[0] ?? + (response as { result?: { data?: unknown[] } } | null)?.result?.data?.[0]; + if (Array.isArray(vector) && vector.every((v) => typeof v === 'number')) { + return vector; + } + } catch { + return null; + } + + return null; +} + +export async function handleOttaSearchStatus(context: ApiRouteContext): Promise { + const auth = await requireAdminAccess(context, { scope: 'system' }); + if (auth instanceof Response) return auth; + + const { env } = context; + if (!env.OBCF_D1) { + return errorResponse('D1 database binding not configured', 500, { code: 'CONFIG_ERROR' }); + } + + const ftsExists = await env.OBCF_D1.prepare( + `SELECT 1 as ok FROM sqlite_schema WHERE type='table' AND name = ? LIMIT 1`, + ) + .bind(OTTASEARCH_FTS_TABLE) + .first<{ ok: number }>(); + const hasConfigTable = await tableExists(context, 'searchable_models'); + const hasDocumentsTable = await tableExists(context, 'search_documents'); + const docsCount = hasDocumentsTable + ? await env.OBCF_D1.prepare('SELECT count(*) as total FROM search_documents').first<{ total: number }>() + : { total: 0 }; + const enabledModels = hasConfigTable + ? await env.OBCF_D1.prepare('SELECT count(*) as total FROM searchable_models WHERE enabled = 1').first<{ + total: number; + }>() + : { total: 0 }; + + const hasVectorize = Boolean((env as { OBCF_VECTORIZE?: unknown }).OBCF_VECTORIZE); + + const pending: string[] = []; + if (!hasConfigTable || !hasDocumentsTable) { + pending.push('Search tables not initialized. Run /api/ottaorm/init then reindex.'); + } + if (!ftsExists?.ok) pending.push('FTS index not initialized. Run Reindex once from admin.'); + if (!enabledModels?.total) pending.push('No searchable models enabled. Configure models first.'); + if (!hasVectorize) pending.push('Semantic search unavailable: bind OBCF_VECTORIZE to enable vector ranking.'); + + return jsonResponse({ + ftsReady: Boolean(ftsExists?.ok), + indexedDocuments: docsCount?.total ?? 0, + enabledModels: enabledModels?.total ?? 0, + hasVectorize, + pending, + }); +} + +export async function handleOttaSearchConfig(context: ApiRouteContext): Promise { + const auth = await requireAdminAccess(context, { scope: 'system' }); + if (auth instanceof Response) return auth; + + if (!context.env.OBCF_D1) { + return errorResponse('D1 database binding not configured', 500, { code: 'CONFIG_ERROR' }); + } + if (!(await tableExists(context, 'searchable_models'))) { + return jsonResponse({ models: [] }); + } + + const metadata = getModelMetadata(); + const configsResult = await context.env.OBCF_D1.prepare( + 'SELECT entity_name as entityName, enabled, fields_json as fieldsJson, last_indexed_at as lastIndexedAt FROM searchable_models ORDER BY entity_name', + ).all<{ entityName: string; enabled: number; fieldsJson: string; lastIndexedAt: number | null }>(); + + const configsByEntity = new Map< + string, + { entityName: string; enabled: number; fields: string[]; lastIndexedAt: number | null } + >( + configsResult.results.map( + (row: { entityName: string; enabled: number; fieldsJson: string; lastIndexedAt: number | null }) => [ + row.entityName, + { + entityName: row.entityName, + enabled: row.enabled, + fields: parseJsonStringArray(row.fieldsJson), + lastIndexedAt: row.lastIndexedAt, + }, + ], + ), + ); + + return jsonResponse({ + models: metadata.map((model) => ({ + entityName: model.entityName, + modelName: model.modelName, + tableName: model.tableName, + enabled: Boolean(configsByEntity.get(model.entityName)?.enabled), + fields: configsByEntity.get(model.entityName)?.fields ?? DEFAULT_SEARCH_FIELDS, + lastIndexedAt: configsByEntity.get(model.entityName)?.lastIndexedAt ?? null, + })), + }); +} + +export async function handleOttaSearchConfigUpsert(context: ApiRouteContext): Promise { + const auth = await requireAdminAccess(context, { scope: 'system' }); + if (auth instanceof Response) return auth; + + if (!context.env.OBCF_D1) { + return errorResponse('D1 database binding not configured', 500, { code: 'CONFIG_ERROR' }); + } + if (!(await tableExists(context, 'searchable_models'))) { + return errorResponse('searchable_models table not found. Run /api/ottaorm/init first.', 400, { + code: 'SETUP_REQUIRED', + }); + } + + const body = (await context.request.json().catch(() => null)) as { + entityName?: string; + enabled?: boolean; + fields?: string[]; + } | null; + + const entityName = body?.entityName?.trim(); + if (!entityName) return errorResponse('entityName is required', 400, { code: 'VALIDATION_ERROR' }); + + const fields = (body?.fields ?? DEFAULT_SEARCH_FIELDS).filter((field) => typeof field === 'string' && field.trim()); + const enabled = body?.enabled !== false; + const now = Date.now(); + + await context.env.OBCF_D1.prepare( + `INSERT INTO searchable_models(entity_name, enabled, fields_json, created_at, updated_at) + VALUES(?, ?, ?, ?, ?) + ON CONFLICT(entity_name) DO UPDATE SET enabled = excluded.enabled, fields_json = excluded.fields_json, updated_at = excluded.updated_at`, + ) + .bind(entityName, enabled ? 1 : 0, JSON.stringify(fields), now, now) + .run(); + + return jsonResponse({ success: true, entityName, enabled, fields }); +} + +export async function handleOttaSearchReindex(context: ApiRouteContext): Promise { + const auth = await requireAdminAccess(context, { scope: 'system' }); + if (auth instanceof Response) return auth; + + const { env } = context; + if (!env.OBCF_D1) { + return errorResponse('D1 database binding not configured', 500, { code: 'CONFIG_ERROR' }); + } + + await ensureFtsTable(env.OBCF_D1); + if (!(await tableExists(context, 'searchable_models')) || !(await tableExists(context, 'search_documents'))) { + return errorResponse('Search tables not found. Run /api/ottaorm/init first.', 400, { code: 'SETUP_REQUIRED' }); + } + + const body = (await context.request.json().catch(() => ({}))) as { entityName?: string }; + const targetEntity = body.entityName?.trim(); + + const configsResult = await env.OBCF_D1.prepare( + 'SELECT entity_name as entityName, fields_json as fieldsJson FROM searchable_models WHERE enabled = 1', + ).all<{ entityName: string; fieldsJson: string }>(); + + const configs = configsResult.results.filter( + (row: { entityName: string; fieldsJson: string }) => !targetEntity || row.entityName === targetEntity, + ); + const metadataByEntity = new Map(getModelMetadata().map((item) => [item.entityName, item])); + + let indexedCount = 0; + + for (const config of configs) { + const meta = metadataByEntity.get(config.entityName); + if (!meta) continue; + + const ModelClass = meta.model as { + all: (options?: { limit?: number }) => Promise Record }>>; + primaryKey?: string; + }; + + const rows = await ModelClass.all({ limit: 200 }); + const fields = parseJsonStringArray(config.fieldsJson).length + ? parseJsonStringArray(config.fieldsJson) + : DEFAULT_SEARCH_FIELDS; + + const existingIds = await env.OBCF_D1.prepare('SELECT id FROM search_documents WHERE entity_name = ?') + .bind(config.entityName) + .all<{ id: string }>(); + for (const row of existingIds.results) { + await env.OBCF_D1.prepare(`DELETE FROM ${OTTASEARCH_FTS_TABLE} WHERE id = ?`).bind(row.id).run(); + } + await env.OBCF_D1.prepare('DELETE FROM search_documents WHERE entity_name = ?').bind(config.entityName).run(); + + for (const rowModel of rows) { + const row = rowModel.toJson(); + const primaryKey = ModelClass.primaryKey ?? 'id'; + const recordIdRaw = row[primaryKey] ?? row.id; + const recordId = recordIdRaw ? String(recordIdRaw) : ''; + if (!recordId) continue; + + const content = collectDocumentText(row, fields); + if (!content) continue; + + const title = + (typeof row.title === 'string' && row.title) || + (typeof row.name === 'string' && row.name) || + (typeof row.label === 'string' && row.label) || + `${config.entityName}:${recordId}`; + + const keywords = fields + .filter((field) => typeof row[field] === 'string') + .map((field) => String(row[field])); + const id = `${config.entityName}:${recordId}`; + const now = Date.now(); + + const embedding = await getVectorEmbedding(context, `${title}\n${content.slice(0, 2000)}`); + + await env.OBCF_D1.prepare( + `INSERT INTO search_documents(id, entity_name, record_id, title, content, keywords_json, embedding_json, created_at, updated_at) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET title = excluded.title, content = excluded.content, keywords_json = excluded.keywords_json, embedding_json = excluded.embedding_json, updated_at = excluded.updated_at`, + ) + .bind( + id, + config.entityName, + recordId, + title, + content, + JSON.stringify(keywords), + embedding ? JSON.stringify(embedding) : null, + now, + now, + ) + .run(); + + await env.OBCF_D1.prepare(`DELETE FROM ${OTTASEARCH_FTS_TABLE} WHERE id = ?`).bind(id).run(); + await env.OBCF_D1.prepare( + `INSERT INTO ${OTTASEARCH_FTS_TABLE}(id, title, content, keywords) VALUES(?, ?, ?, ?)`, + ) + .bind(id, title, content, keywords.join(' ')) + .run(); + + const vectorize = ( + env as { + OBCF_VECTORIZE?: { + upsert?: (vectors: Array<{ id: string; values: number[]; metadata: unknown }>) => Promise; + }; + } + ).OBCF_VECTORIZE; + if (vectorize?.upsert && embedding) { + await vectorize.upsert([ + { + id, + values: embedding, + metadata: { entityName: config.entityName, recordId, title }, + }, + ]); + } + + indexedCount += 1; + } + + await env.OBCF_D1.prepare( + 'UPDATE searchable_models SET last_indexed_at = ?, updated_at = ? WHERE entity_name = ?', + ) + .bind(Date.now(), Date.now(), config.entityName) + .run(); + } + + return jsonResponse({ success: true, indexedCount, models: configs.length }); +} + +export async function handleOttaSearchQuery(context: ApiRouteContext): Promise { + const { env, url } = context; + if (!env.OBCF_D1) { + return errorResponse('D1 database binding not configured', 500, { code: 'CONFIG_ERROR' }); + } + + const query = url.searchParams.get('q')?.trim() ?? ''; + if (!query) return jsonResponse({ results: [] }); + if (!(await tableExists(context, 'search_documents'))) { + return jsonResponse({ results: [] }); + } + + const limit = Math.min(Number(url.searchParams.get('limit') || 10), 25); + await ensureFtsTable(env.OBCF_D1); + + const ftsResult = await env.OBCF_D1.prepare( + `SELECT sd.id, sd.entity_name as entityName, sd.record_id as recordId, sd.title, sd.content, sd.keywords_json as keywordsJson, + (1.0 / (1 + abs(bm25(${OTTASEARCH_FTS_TABLE})))) as score + FROM ${OTTASEARCH_FTS_TABLE} + JOIN search_documents sd ON sd.id = ${OTTASEARCH_FTS_TABLE}.id + WHERE ${OTTASEARCH_FTS_TABLE} MATCH ? + ORDER BY score DESC + LIMIT ?`, + ) + .bind(query, limit) + .all<{ + id: string; + entityName: string; + recordId: string; + title: string; + content: string; + keywordsJson: string; + score: number; + }>(); + + const ftsDocs = ftsResult.results.map( + (row: { + id: string; + entityName: string; + recordId: string; + title: string; + content: string; + keywordsJson: string; + score: number; + }) => ({ + id: row.id, + entityName: row.entityName, + recordId: row.recordId, + title: row.title, + content: row.content, + keywords: parseJsonStringArray(row.keywordsJson), + score: row.score, + }), + ); + + let semanticDocs: Array<{ + id: string; + entityName: string; + recordId: string; + title: string; + content: string; + keywords: string[]; + score: number; + }> = []; + + const vectorize = ( + env as { + OBCF_VECTORIZE?: { + query?: ( + vector: number[], + options: { topK: number }, + ) => Promise<{ matches?: Array<{ id: string; score: number }> }>; + }; + } + ).OBCF_VECTORIZE; + + if (vectorize?.query) { + const embedding = await getVectorEmbedding(context, query); + if (embedding) { + const vectorResult = await vectorize.query(embedding, { topK: limit }); + const vectorMatches = vectorResult.matches ?? []; + + for (const match of vectorMatches) { + const doc = await env.OBCF_D1.prepare( + 'SELECT id, entity_name as entityName, record_id as recordId, title, content, keywords_json as keywordsJson FROM search_documents WHERE id = ? LIMIT 1', + ) + .bind(match.id) + .first<{ + id: string; + entityName: string; + recordId: string; + title: string; + content: string; + keywordsJson: string; + }>(); + + if (doc) { + semanticDocs.push({ + id: doc.id, + entityName: doc.entityName, + recordId: doc.recordId, + title: doc.title, + content: doc.content, + keywords: parseJsonStringArray(doc.keywordsJson), + score: match.score, + }); + } + } + } + } + + const merged = mergeHybridResults(ftsDocs, semanticDocs).slice(0, limit); + + return jsonResponse({ + results: merged.map((row) => ({ + id: row.id, + entityName: row.entityName, + recordId: row.recordId, + title: row.title, + description: row.content.slice(0, 180), + keywords: row.keywords, + score: row.score ?? 0, + href: buildSpotlightHref(row.entityName, row.recordId, row as unknown as Record), + })), + }); +} + +export async function handleOttaSearchSpotlight(context: ApiRouteContext): Promise { + const result = await handleOttaSearchQuery(context); + const json = (await result.json()) as { results?: Array> }; + + return jsonResponse( + (json.results ?? []).map((row) => ({ + id: row.id, + label: row.title, + description: row.description, + keywords: row.keywords, + href: row.href, + })), + ); +} diff --git a/apps/ottabase-template-app-tanstack/worker/routes/router.ts b/apps/ottabase-template-app-tanstack/worker/routes/router.ts index 09bc276d6..1aa65ec10 100644 --- a/apps/ottabase-template-app-tanstack/worker/routes/router.ts +++ b/apps/ottabase-template-app-tanstack/worker/routes/router.ts @@ -71,6 +71,14 @@ import { import { handleCoreAnalytics } from './core-analytics'; import { handleAuditLogs, handleDemo, handleDemoError } from './demo'; import { handleEmailProviders, handleEmailTest } from './email'; +import { + handleOttaSearchConfig, + handleOttaSearchConfigUpsert, + handleOttaSearchQuery, + handleOttaSearchReindex, + handleOttaSearchSpotlight, + handleOttaSearchStatus, +} from './ottasearch'; import { handleOttaormCrud } from './ottaorm-crud'; import { handleModelsMetadata, handleOttaormInit } from './ottaorm-init'; import { @@ -299,6 +307,22 @@ async function handleGetRoutes(context: ApiRouteContext): Promise { + it('collects text from primitive and structured fields', () => { + const text = collectDocumentText( + { + title: 'Hello', + tags: ['one', 'two'], + meta: { status: 'published' }, + }, + ['title', 'tags', 'meta'], + ); + + expect(text).toContain('Hello'); + expect(text).toContain('one'); + expect(text).toContain('published'); + }); + + it('parses JSON string arrays safely', () => { + expect(parseJsonStringArray('["a","b"]')).toEqual(['a', 'b']); + expect(parseJsonStringArray('not-json')).toEqual([]); + }); + + it('merges fts and semantic scores by document id', () => { + const merged = mergeHybridResults( + [{ id: 'a', entityName: 'posts', recordId: '1', title: 'A', content: 'A', keywords: [], score: 0.5 }], + [{ id: 'a', entityName: 'posts', recordId: '1', title: 'A', content: 'A', keywords: [], score: 0.4 }], + ); + + expect(merged).toHaveLength(1); + expect(merged[0].score).toBeGreaterThan(1); + }); + + it('creates fts table using expected SQL', async () => { + const run = vi.fn(async () => ({})); + const prepare = vi.fn(() => ({ run })); + + await ensureFtsTable({ prepare } as any); + + expect(prepare).toHaveBeenCalledWith( + expect.stringContaining(`CREATE VIRTUAL TABLE IF NOT EXISTS ${OTTASEARCH_FTS_TABLE}`), + ); + }); +}); diff --git a/packages/ottasearch/src/index.ts b/packages/ottasearch/src/index.ts new file mode 100644 index 000000000..3a6ac4179 --- /dev/null +++ b/packages/ottasearch/src/index.ts @@ -0,0 +1,15 @@ +export { + searchDocumentsTable, + searchableModelsTable, + type SearchDocumentRecord, + type SearchableModelRecord, +} from './schema'; +export { + collectDocumentText, + ensureFtsTable, + mergeHybridResults, + OTTASEARCH_FTS_TABLE, + parseJsonStringArray, + type D1DatabaseLike, +} from './search'; +export type { IndexedSearchDocument, SearchableModelConfig } from './types'; diff --git a/packages/ottasearch/src/schema.ts b/packages/ottasearch/src/schema.ts new file mode 100644 index 000000000..8fe486d41 --- /dev/null +++ b/packages/ottasearch/src/schema.ts @@ -0,0 +1,25 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +export const searchableModelsTable = sqliteTable('searchable_models', { + entityName: text('entity_name').primaryKey(), + enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true), + fieldsJson: text('fields_json').notNull().default('[]'), + createdAt: integer('created_at').notNull(), + updatedAt: integer('updated_at').notNull(), + lastIndexedAt: integer('last_indexed_at'), +}); + +export const searchDocumentsTable = sqliteTable('search_documents', { + id: text('id').primaryKey(), + entityName: text('entity_name').notNull(), + recordId: text('record_id').notNull(), + title: text('title').notNull(), + content: text('content').notNull(), + keywordsJson: text('keywords_json').notNull().default('[]'), + embeddingJson: text('embedding_json'), + createdAt: integer('created_at').notNull(), + updatedAt: integer('updated_at').notNull(), +}); + +export type SearchableModelRecord = typeof searchableModelsTable.$inferSelect; +export type SearchDocumentRecord = typeof searchDocumentsTable.$inferSelect; diff --git a/packages/ottasearch/src/search.ts b/packages/ottasearch/src/search.ts new file mode 100644 index 000000000..37d434273 --- /dev/null +++ b/packages/ottasearch/src/search.ts @@ -0,0 +1,69 @@ +import type { IndexedSearchDocument } from './types'; + +export const OTTASEARCH_FTS_TABLE = 'search_documents_fts'; + +export function collectDocumentText(record: Record, fields: string[]): string { + const parts = fields + .map((field) => record[field]) + .flatMap((value) => { + if (value === null || value === undefined) return []; + if (Array.isArray(value)) return value.map((item) => String(item)); + if (typeof value === 'object') return [JSON.stringify(value)]; + return [String(value)]; + }) + .map((part) => part.trim()) + .filter(Boolean); + + return parts.join(' '); +} + +export function parseJsonStringArray(value: unknown): string[] { + if (typeof value !== 'string' || !value.trim()) return []; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : []; + } catch { + return []; + } +} + +export function mergeHybridResults( + ftsResults: IndexedSearchDocument[], + semanticResults: IndexedSearchDocument[], +): IndexedSearchDocument[] { + const byId = new Map(); + + for (const result of ftsResults) { + byId.set(result.id, { ...result, score: (result.score ?? 0) + 1 }); + } + + for (const result of semanticResults) { + const existing = byId.get(result.id); + if (existing) { + existing.score = (existing.score ?? 0) + (result.score ?? 0.6); + } else { + byId.set(result.id, { ...result, score: result.score ?? 0.6 }); + } + } + + return [...byId.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)); +} + +export async function ensureFtsTable(db: D1DatabaseLike): Promise { + await db + .prepare( + `CREATE VIRTUAL TABLE IF NOT EXISTS ${OTTASEARCH_FTS_TABLE} USING fts5(id UNINDEXED, title, content, keywords)`, + ) + .run(); +} + +export interface D1PreparedStatementLike { + bind(...values: unknown[]): D1PreparedStatementLike; + run(): Promise; + all(): Promise<{ results: T[] }>; + first(): Promise; +} + +export interface D1DatabaseLike { + prepare(query: string): D1PreparedStatementLike; +} diff --git a/packages/ottasearch/src/types.ts b/packages/ottasearch/src/types.ts new file mode 100644 index 000000000..2fd7d63ce --- /dev/null +++ b/packages/ottasearch/src/types.ts @@ -0,0 +1,17 @@ +export interface SearchableModelConfig { + entityName: string; + enabled: boolean; + fields: string[]; + lastIndexedAt?: number | null; +} + +export interface IndexedSearchDocument { + id: string; + entityName: string; + recordId: string; + title: string; + content: string; + keywords: string[]; + score?: number; + href?: string; +} diff --git a/packages/ottasearch/tsconfig.json b/packages/ottasearch/tsconfig.json new file mode 100644 index 000000000..e3f0e2933 --- /dev/null +++ b/packages/ottasearch/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "baseUrl": ".", + "paths": {}, + "skipLibCheck": true, + "lib": ["esnext", "dom", "dom.iterable"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ottasearch/vitest.config.ts b/packages/ottasearch/vitest.config.ts new file mode 100644 index 000000000..bd8da3bbf --- /dev/null +++ b/packages/ottasearch/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + include: ['src/**/*.{test,spec}.{ts,tsx}', '__tests__/**/*.{test,spec}.{ts,tsx}'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a99b630ca..a6219fc6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,7 +256,7 @@ importers: version: 7.28.5(@babel/core@7.28.6) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.2.3(@types/react@19.2.7)(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) + version: 10.2.3(@types/react@19.2.7)(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) '@storybook/addon-links': specifier: 'catalog:' version: 10.2.3(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) @@ -274,13 +274,13 @@ importers: version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@typescript-eslint/eslint-plugin': specifier: 'catalog:' - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@vitejs/plugin-react': specifier: 'catalog:' - version: 4.7.0(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.18 version: 4.0.18(vitest@4.0.18) @@ -295,7 +295,7 @@ importers: version: 7.1.3(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) eslint-plugin-storybook: specifier: 'catalog:' - version: 10.2.3(eslint@9.39.2(jiti@2.6.1))(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + version: 10.2.3(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) happy-dom: specifier: ^20.4.0 version: 20.4.0 @@ -337,13 +337,13 @@ importers: version: 1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) tsup: specifier: 'catalog:' - version: 8.5.1(@swc/core@1.13.5)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.13.5)(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) turbo: specifier: ^2.8.0 version: 2.8.0 vitest: specifier: 'catalog:' - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(happy-dom@20.4.0)(jiti@1.21.7)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) webpack: specifier: ^5.104.1 version: 5.104.1(@swc/core@1.13.5)(esbuild@0.27.2) @@ -431,13 +431,13 @@ importers: version: 10.4.21(postcss@8.5.6) eslint: specifier: 'catalog:' - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) eslint-config-next: specifier: ^16.1.1 - version: 16.1.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + version: 16.1.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) localflare: specifier: 'catalog:' - version: 0.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(vite@7.3.1(@types/node@20.19.28)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(vite@7.3.1(@types/node@20.19.28)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) postcss: specifier: 'catalog:' version: 8.5.6 @@ -546,6 +546,9 @@ importers: '@ottabase/ottarenderer': specifier: workspace:* version: link:../../packages/ottarenderer + '@ottabase/ottasearch': + specifier: workspace:* + version: link:../../packages/ottasearch '@ottabase/ottaselect': specifier: workspace:* version: link:../../packages/ottaselect @@ -1559,6 +1562,22 @@ importers: specifier: 'catalog:' version: 5.9.3 + packages/ottasearch: + dependencies: + '@ottabase/ottaorm': + specifier: workspace:* + version: link:../ottaorm + drizzle-orm: + specifier: 'catalog:' + version: 0.38.4(@cloudflare/workers-types@4.20251225.0)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/pg@8.15.6)(@types/react@19.2.7)(prisma@5.22.0)(react@19.2.4) + devDependencies: + typescript: + specifier: 'catalog:' + version: 5.9.3 + vitest: + specifier: 'catalog:' + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + packages/ottaselect: dependencies: '@ottabase/config': @@ -17014,10 +17033,10 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.2.3(@types/react@19.2.7)(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': + '@storybook/addon-docs@10.2.3(@types/react@19.2.7)(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.4) - '@storybook/csf-plugin': 10.2.3(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) + '@storybook/csf-plugin': 10.2.3(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@storybook/react-dom-shim': 10.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 @@ -17075,14 +17094,14 @@ snapshots: storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - '@storybook/csf-plugin@10.2.3(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': + '@storybook/csf-plugin@10.2.3(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': dependencies: storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.2 rollup: 4.57.1 - vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) webpack: 5.104.1(@swc/core@1.13.5)(esbuild@0.27.2) '@storybook/global@5.0.0': {} @@ -17310,12 +17329,12 @@ snapshots: tailwindcss: 4.1.18 vite: 5.4.21(@types/node@20.19.28)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0) - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@20.19.28)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@20.19.28)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@20.19.28)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@20.19.28)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) '@tanstack/history@1.145.7': {} @@ -17555,15 +17574,15 @@ snapshots: dependencies: '@types/node': 25.1.0 - '@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/type-utils': 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.52.0 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -17571,15 +17590,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.54.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -17587,26 +17606,26 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.52.0 '@typescript-eslint/types': 8.52.0 '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.52.0 debug: 4.4.3 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.54.0 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -17647,25 +17666,25 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.52.0 '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@1.21.7) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -17705,24 +17724,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.52.0 '@typescript-eslint/types': 8.52.0 '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7)) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -17820,7 +17839,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -17828,7 +17847,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -17849,13 +17868,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -17891,7 +17910,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(happy-dom@20.4.0)(jiti@1.21.7)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/utils@3.2.4': dependencies: @@ -19101,18 +19120,18 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.1.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + eslint-config-next@16.1.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 16.1.1 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) globals: 16.4.0 - typescript-eslint: 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + typescript-eslint: 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -19129,33 +19148,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) get-tsconfig: 4.13.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.2(jiti@1.21.7) + '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -19164,9 +19183,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -19178,13 +19197,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@1.21.7)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -19194,7 +19213,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -19203,18 +19222,18 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): dependencies: '@babel/core': 7.28.6 '@babel/parser': 7.28.6 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) hermes-parser: 0.25.1 zod: 3.25.76 zod-validation-error: 4.0.2(zod@3.25.76) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@1.21.7)): + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -19222,7 +19241,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.2(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -19236,10 +19255,10 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-storybook@10.2.3(eslint@9.39.2(jiti@2.6.1))(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): + eslint-plugin-storybook@10.2.3(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - supports-color @@ -20387,7 +20406,7 @@ snapshots: - '@types/react-dom' - vite - localflare-dashboard@0.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(vite@7.3.1(@types/node@20.19.28)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + localflare-dashboard@0.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(vite@7.3.1(@types/node@20.19.28)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@base-ui/react': 1.0.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@fontsource-variable/figtree': 5.2.10 @@ -20398,7 +20417,7 @@ snapshots: '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.4) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tailwindcss/vite': 4.1.18(vite@7.3.1(@types/node@20.19.28)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@tailwindcss/vite': 4.1.18(vite@7.3.1(@types/node@20.19.28)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-query': 5.90.16(react@19.2.4) '@tanstack/react-table': 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) class-variance-authority: 0.7.1 @@ -20438,11 +20457,11 @@ snapshots: - utf-8-validate - vite - localflare@0.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(vite@7.3.1(@types/node@20.19.28)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + localflare@0.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(vite@7.3.1(@types/node@20.19.28)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: cac: 6.7.14 localflare-core: 0.1.2 - localflare-dashboard: 0.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(vite@7.3.1(@types/node@20.19.28)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + localflare-dashboard: 0.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(vite@7.3.1(@types/node@20.19.28)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) localflare-server: 0.1.2 picocolors: 1.1.1 transitivePeerDependencies: @@ -22294,6 +22313,35 @@ snapshots: tslib@2.8.1: {} + tsup@8.5.1(@swc/core@1.13.5)(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.2 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + resolve-from: 5.0.0 + rollup: 4.57.1 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.13.5 + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsup@8.5.1(@swc/core@1.13.5)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.2) @@ -22405,13 +22453,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.2(jiti@1.21.7) + '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -22592,7 +22640,7 @@ snapshots: sugarss: 5.0.1(postcss@8.5.6) terser: 5.46.0 - vite@7.3.1(@types/node@20.19.28)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@20.19.28)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -22603,14 +22651,14 @@ snapshots: optionalDependencies: '@types/node': 20.19.28 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 lightningcss: 1.30.2 sugarss: 5.0.1(postcss@8.5.6) terser: 5.46.0 tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.28)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -22619,9 +22667,9 @@ snapshots: rollup: 4.57.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 20.19.28 + '@types/node': 25.1.0 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 lightningcss: 1.30.2 sugarss: 5.0.1(postcss@8.5.6) terser: 5.46.0 @@ -22649,7 +22697,7 @@ snapshots: vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.28)(@vitest/ui@4.0.18)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -22687,10 +22735,51 @@ snapshots: - tsx - yaml + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(happy-dom@20.4.0)(jiti@1.21.7)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 25.1.0 + '@vitest/ui': 4.0.18(vitest@4.0.18) + happy-dom: 20.4.0 + jsdom: 27.4.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 From 67439ec29f55d65b8bb3fe3cee083e24bf6a9d53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:48:53 +0000 Subject: [PATCH 3/4] chore: patch storybook websocket hijacking vulnerability Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com> --- pnpm-lock.yaml | 158 ++++++++++++++++++++++---------------------- pnpm-workspace.yaml | 10 +-- 2 files changed, 84 insertions(+), 84 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6219fc6a..1093fbbed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,17 +46,17 @@ catalogs: specifier: ^5.22.0 version: 5.22.0 '@storybook/addon-docs': - specifier: ^10.2.3 - version: 10.2.3 + specifier: ^10.2.10 + version: 10.2.13 '@storybook/addon-links': - specifier: ^10.2.3 - version: 10.2.3 + specifier: ^10.2.10 + version: 10.2.13 '@storybook/addon-styling-webpack': specifier: ^3.0.0 version: 3.0.0 '@storybook/react-webpack5': - specifier: ^10.2.3 - version: 10.2.3 + specifier: ^10.2.10 + version: 10.2.13 '@tabler/icons-react': specifier: ^3.35.0 version: 3.35.0 @@ -130,8 +130,8 @@ catalogs: specifier: ^9.39.2 version: 9.39.2 eslint-plugin-storybook: - specifier: ^10.2.3 - version: 10.2.3 + specifier: ^10.2.10 + version: 10.2.13 handlebars: specifier: ^4.7.8 version: 4.7.8 @@ -199,8 +199,8 @@ catalogs: specifier: ^1.5.1 version: 1.7.4 storybook: - specifier: ^10.2.3 - version: 10.2.3 + specifier: ^10.2.10 + version: 10.2.13 style-loader: specifier: ^4.0.0 version: 4.0.0 @@ -256,16 +256,16 @@ importers: version: 7.28.5(@babel/core@7.28.6) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.2.3(@types/react@19.2.7)(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) + version: 10.2.13(@types/react@19.2.7)(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) '@storybook/addon-links': specifier: 'catalog:' - version: 10.2.3(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.2.13(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/addon-styling-webpack': specifier: 'catalog:' - version: 3.0.0(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) + version: 3.0.0(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) '@storybook/react-webpack5': specifier: 'catalog:' - version: 10.2.3(@swc/core@1.13.5)(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + version: 10.2.13(@swc/core@1.13.5)(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -295,7 +295,7 @@ importers: version: 7.1.3(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) eslint-plugin-storybook: specifier: 'catalog:' - version: 10.2.3(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + version: 10.2.13(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) happy-dom: specifier: ^20.4.0 version: 20.4.0 @@ -325,7 +325,7 @@ importers: version: 6.1.2 storybook: specifier: 'catalog:' - version: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) style-loader: specifier: 'catalog:' version: 4.0.0(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) @@ -6563,16 +6563,16 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-docs@10.2.3': - resolution: {integrity: sha512-IPprt2qp4HN1uyE1Ki1sH0ZOE5B6z5sKzEMfrKMGokYKYk/AAJVfSiVIKju3q525GrBFlNhRW2+fB4pQfklv2w==} + '@storybook/addon-docs@10.2.13': + resolution: {integrity: sha512-puMxpJbt/CuodLIbKDxWrW1ZgADYomfNHWEKp2d2l2eJjp17rADx0h3PABuNbX+YHbJwYcDdqluSnQwMysFEOA==} peerDependencies: - storybook: ^10.2.3 + storybook: ^10.2.13 - '@storybook/addon-links@10.2.3': - resolution: {integrity: sha512-ewOUga9zhcGQRGTTl7PyaV8kwLL4Jj1oeXWF2fq4fx+Fhzcn+d99gu3uV+zrGZa1gueBIRwf+p6NJTO//xSVUw==} + '@storybook/addon-links@10.2.13': + resolution: {integrity: sha512-8wnAomGiHaUpNIc+lOzmazTrebxa64z9rihIbM/Q59vkOImHQNkGp7KP/qNgJA4GPTFtu8+fLjX2qCoAQPM0jQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.3 + storybook: ^10.2.13 peerDependenciesMeta: react: optional: true @@ -6583,26 +6583,26 @@ packages: storybook: ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 webpack: ^5.0.0 - '@storybook/builder-webpack5@10.2.3': - resolution: {integrity: sha512-H6lp7Mc5aTZjVefXfPSxI4ZuimnOEwui/DfEvKC7Rhlig21O501F9+tHl+zHIhrrcXheYxqZCmARJ/hAYwO/0g==} + '@storybook/builder-webpack5@10.2.13': + resolution: {integrity: sha512-LVQPuCiadOvCgyhkF2Y70D5bxdzsD7Ib8YXrGoiLYRGtv9m5ZTh91ky5cEJEXxOvkD19acLZ4TfRc2KwERDaqQ==} peerDependencies: - storybook: ^10.2.3 + storybook: ^10.2.13 typescript: '*' peerDependenciesMeta: typescript: optional: true - '@storybook/core-webpack@10.2.3': - resolution: {integrity: sha512-pqlPj9mSv0rtTFgz+ok3YT2LPIi/CPn9p0XEUhdE8a/vP80ne8CpkAS1jZ3ceY5awfwXCTaz5ROKBx7tc+Q1Kw==} + '@storybook/core-webpack@10.2.13': + resolution: {integrity: sha512-xGud9eeRe3hMNdS3yO2UFHTMMu80rNKpHdejBH6J6+5HGq2BSegaXpmGwRim+SF7BkXN70P8RQrKnWOgwMx9ug==} peerDependencies: - storybook: ^10.2.3 + storybook: ^10.2.13 - '@storybook/csf-plugin@10.2.3': - resolution: {integrity: sha512-/b/C8C40ukzXs3Xauud2+yOJqwBdOkADfRtJ9O4TzrhftzkEdqsNI03xXZySeh7eXW8eI3Vq4t75Ljuj27Xytw==} + '@storybook/csf-plugin@10.2.13': + resolution: {integrity: sha512-gUCR7PmyrWYj3dIJJgxOm25dcXFolPIUPmug3z90Aaon7YPXw3pUN+dNDx8KqDJqRK1WDIB4HaefgYZIm5V7iA==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.2.3 + storybook: ^10.2.13 vite: '*' webpack: '*' peerDependenciesMeta: @@ -6624,12 +6624,12 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/preset-react-webpack@10.2.3': - resolution: {integrity: sha512-q49cmtDwb/6EdnwA0+jSYFi2V+ki0imiBA5SnCO3EEJK8dIXxevC0elt6PG5QPJtKSZWBclrdR2iAo95ALu7hA==} + '@storybook/preset-react-webpack@10.2.13': + resolution: {integrity: sha512-i3BhcnAGzSif7E/Jl4bAF0rdU9UfMafHohUXZlHvbTqV+SdLkev6DZDIwJbehZu6pBLJatn5TGNmbvvl/Dlskw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.3 + storybook: ^10.2.13 typescript: '*' peerDependenciesMeta: typescript: @@ -6641,30 +6641,30 @@ packages: typescript: '>= 4.x' webpack: '>= 4' - '@storybook/react-dom-shim@10.2.3': - resolution: {integrity: sha512-xMZXvjfQCsmzOTqFCRQ1/gxs//jDGLlnmBCikH4NSGPPogRPaNUkxgdNjOResd6pB+G3ZYAOspJkmGEEbq8dVw==} + '@storybook/react-dom-shim@10.2.13': + resolution: {integrity: sha512-ZSduoB10qTI0V9z22qeULmQLsvTs8d/rtJi03qbVxpPiMRor86AmyAaBrfhGGmWBxWQZpOGQQm6yIT2YLoPs7w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.3 + storybook: ^10.2.13 - '@storybook/react-webpack5@10.2.3': - resolution: {integrity: sha512-FhG4lJgX4WExF7/QzZIsoOb9smIQiuvn6yUzLPn3VhtxQVs34ZOM+6XC5/tiVX9XtCLLWDai1O1kxbS1QCHEJA==} + '@storybook/react-webpack5@10.2.13': + resolution: {integrity: sha512-3gBm7ZgDQ869R/lD5GsHf9GwZO6cMDi86Tu36XhZkrVmvyeh9zFvMDHwkOqEWe5qU+tLUCDWF+UPTDAPyvyujQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.3 + storybook: ^10.2.13 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: optional: true - '@storybook/react@10.2.3': - resolution: {integrity: sha512-M67G7IY9TcLQQJ/9mHPItIiNvZFyuXf5r/wBY03YGquwCqo4GtLdp9uyGg3uCc2i0dS5VV5OQenisldmdjWFWQ==} + '@storybook/react@10.2.13': + resolution: {integrity: sha512-gavZbGMkrjR53a6gSaBJPCelXQf8Rumpej9Jm6HdrAYlEJgFssPah5Frbar9yVCZiXiZkFLfAu7RkZzZhnGyZg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.3 + storybook: ^10.2.13 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -8511,11 +8511,11 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-storybook@10.2.3: - resolution: {integrity: sha512-5wy+OKe6VexZecAedroKv+GR+agciZqK/Su7cdo6b1mICWaWwejU/XjjTLL9zr6wiEjCN/0mhYg7yz70DoaMQQ==} + eslint-plugin-storybook@10.2.13: + resolution: {integrity: sha512-ftNfZVL5zXhGMPEy/7PTCEriVH0zCBI89uiYYgSSTtM1b4l++VP+/MzJ17U1R1/jgENsp9LJm+jwRJnViv79RQ==} peerDependencies: eslint: '>=8' - storybook: ^10.2.3 + storybook: ^10.2.13 eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} @@ -10878,8 +10878,8 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} - storybook@10.2.3: - resolution: {integrity: sha512-kjsJ0hctkTO0ipHiyv1MY39wP4tAyVM7rPQGyVMU1iQ7NYHxthiiCHhFB/szmVjXdJa58fu3ZH5cwENMn8Y5eA==} + storybook@10.2.13: + resolution: {integrity: sha512-heMfJjOfbHvL+wlCAwFZlSxcakyJ5yQDam6e9k2RRArB1veJhRnsjO6lO1hOXjJYrqxfHA/ldIugbBVlCDqfvQ==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -17033,15 +17033,15 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.2.3(@types/react@19.2.7)(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': + '@storybook/addon-docs@10.2.13(@types/react@19.2.7)(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.4) - '@storybook/csf-plugin': 10.2.3(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) + '@storybook/csf-plugin': 10.2.13(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-dom-shim': 10.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/react-dom-shim': 10.2.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -17050,21 +17050,21 @@ snapshots: - vite - webpack - '@storybook/addon-links@10.2.3(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-links@10.2.13(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: react: 19.2.4 - '@storybook/addon-styling-webpack@3.0.0(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': + '@storybook/addon-styling-webpack@3.0.0(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': dependencies: - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) webpack: 5.104.1(@swc/core@1.13.5)(esbuild@0.27.2) - '@storybook/builder-webpack5@10.2.3(@swc/core@1.13.5)(esbuild@0.27.2)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@storybook/builder-webpack5@10.2.13(@swc/core@1.13.5)(esbuild@0.27.2)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': dependencies: - '@storybook/core-webpack': 10.2.3(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/core-webpack': 10.2.13(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 css-loader: 7.1.3(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) @@ -17072,7 +17072,7 @@ snapshots: fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) html-webpack-plugin: 5.6.6(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) magic-string: 0.30.21 - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) style-loader: 4.0.0(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) terser-webpack-plugin: 5.3.16(@swc/core@1.13.5)(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) ts-dedent: 2.2.0 @@ -17089,14 +17089,14 @@ snapshots: - uglify-js - webpack-cli - '@storybook/core-webpack@10.2.3(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/core-webpack@10.2.13(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - '@storybook/csf-plugin@10.2.3(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': + '@storybook/csf-plugin@10.2.13(esbuild@0.27.2)(rollup@4.57.1)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2))': dependencies: - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.2 @@ -17111,9 +17111,9 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@storybook/preset-react-webpack@10.2.3(@swc/core@1.13.5)(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@storybook/preset-react-webpack@10.2.13(@swc/core@1.13.5)(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': dependencies: - '@storybook/core-webpack': 10.2.3(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/core-webpack': 10.2.13(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.13.5)(esbuild@0.27.2)) '@types/semver': 7.7.1 magic-string: 0.30.21 @@ -17122,7 +17122,7 @@ snapshots: react-dom: 19.2.4(react@19.2.4) resolve: 1.22.11 semver: 7.7.3 - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tsconfig-paths: 4.2.0 webpack: 5.104.1(@swc/core@1.13.5)(esbuild@0.27.2) optionalDependencies: @@ -17148,20 +17148,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-dom-shim@10.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/react-dom-shim@10.2.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-webpack5@10.2.3(@swc/core@1.13.5)(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@storybook/react-webpack5@10.2.13(@swc/core@1.13.5)(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': dependencies: - '@storybook/builder-webpack5': 10.2.3(@swc/core@1.13.5)(esbuild@0.27.2)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) - '@storybook/preset-react-webpack': 10.2.3(@swc/core@1.13.5)(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) - '@storybook/react': 10.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@storybook/builder-webpack5': 10.2.13(@swc/core@1.13.5)(esbuild@0.27.2)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@storybook/preset-react-webpack': 10.2.13(@swc/core@1.13.5)(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@storybook/react': 10.2.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -17172,14 +17172,14 @@ snapshots: - uglify-js - webpack-cli - '@storybook/react@10.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@storybook/react@10.2.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/react-dom-shim': 10.2.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 react-docgen: 8.0.2 react-dom: 19.2.4(react@19.2.4) - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -19255,11 +19255,11 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-storybook@10.2.3(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): + eslint-plugin-storybook@10.2.13(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) - storybook: 10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - supports-color - typescript @@ -21953,7 +21953,7 @@ snapshots: stoppable@1.1.0: {} - storybook@10.2.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + storybook@10.2.13(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6e79b322f..5e37a19ab 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -17,10 +17,10 @@ catalog: "@mantine/notifications": ^8.3.15 "@prisma/adapter-d1": ^5.22.0 "@prisma/client": ^5.22.0 - "@storybook/addon-docs": ^10.2.3 - "@storybook/addon-links": ^10.2.3 + "@storybook/addon-docs": ^10.2.10 + "@storybook/addon-links": ^10.2.10 "@storybook/addon-styling-webpack": ^3.0.0 - "@storybook/react-webpack5": ^10.2.3 + "@storybook/react-webpack5": ^10.2.10 "@storybook/test": ^9.1.15 "@tabler/icons-react": ^3.35.0 "@tailwindcss/forms": ^0.5.10 @@ -47,7 +47,7 @@ catalog: drizzle-kit: ^0.31.8 drizzle-orm: ^0.38.3 eslint: ^9.39.2 - eslint-plugin-storybook: ^10.2.3 + eslint-plugin-storybook: ^10.2.10 handlebars: ^4.7.8 highlight.js: ^11.11.1 husky: ^9.1.7 @@ -70,7 +70,7 @@ catalog: react-hook-form: ^7.54.2 rimraf: ^6.1.2 sonner: ^1.5.1 - storybook: ^10.2.3 + storybook: ^10.2.10 style-loader: ^4.0.0 tailwind-merge: ^2.6.0 tailwindcss: ^3.4.19 From a90d4f62f16e928fe8ee481c8bd49d77f3e96b23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:46:17 +0000 Subject: [PATCH 4/4] fix: harden ottasearch queries and reindex all records Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com> --- .../worker/routes/ottasearch.ts | 21 ++++++++++++++++--- .../ottasearch/src/__tests__/search.test.ts | 9 ++++++++ packages/ottasearch/src/index.ts | 1 + packages/ottasearch/src/search.ts | 12 +++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/ottabase-template-app-tanstack/worker/routes/ottasearch.ts b/apps/ottabase-template-app-tanstack/worker/routes/ottasearch.ts index 96251b47c..3a5235b9b 100644 --- a/apps/ottabase-template-app-tanstack/worker/routes/ottasearch.ts +++ b/apps/ottabase-template-app-tanstack/worker/routes/ottasearch.ts @@ -2,6 +2,7 @@ import { collectDocumentText, ensureFtsTable, mergeHybridResults, + normalizeFtsQuery, OTTASEARCH_FTS_TABLE, parseJsonStringArray, } from '@ottabase/ottasearch'; @@ -221,11 +222,22 @@ export async function handleOttaSearchReindex(context: ApiRouteContext): Promise if (!meta) continue; const ModelClass = meta.model as { - all: (options?: { limit?: number }) => Promise Record }>>; + all: (options?: { + limit?: number; + offset?: number; + }) => Promise Record }>>; primaryKey?: string; }; - const rows = await ModelClass.all({ limit: 200 }); + const rows: Array<{ toJson: () => Record }> = []; + const pageSize = 200; + let offset = 0; + while (true) { + const page = await ModelClass.all({ limit: pageSize, offset }); + rows.push(...page); + if (page.length < pageSize) break; + offset += pageSize; + } const fields = parseJsonStringArray(config.fieldsJson).length ? parseJsonStringArray(config.fieldsJson) : DEFAULT_SEARCH_FIELDS; @@ -325,6 +337,9 @@ export async function handleOttaSearchQuery(context: ApiRouteContext): Promise { expect.stringContaining(`CREATE VIRTUAL TABLE IF NOT EXISTS ${OTTASEARCH_FTS_TABLE}`), ); }); + + it('normalizes user query into safe FTS terms', () => { + expect(normalizeFtsQuery('hello "world" test*')).toBe('hello* OR world* OR test*'); + expect(normalizeFtsQuery('***')).toBe(''); + expect(normalizeFtsQuery(' ')).toBe(''); + expect(normalizeFtsQuery('naïve café')).toBe('naïve* OR café*'); + expect(normalizeFtsQuery('a b c d e f g h i j')).toBe('a* OR b* OR c* OR d* OR e* OR f* OR g* OR h*'); + }); }); diff --git a/packages/ottasearch/src/index.ts b/packages/ottasearch/src/index.ts index 3a6ac4179..b79134f5d 100644 --- a/packages/ottasearch/src/index.ts +++ b/packages/ottasearch/src/index.ts @@ -8,6 +8,7 @@ export { collectDocumentText, ensureFtsTable, mergeHybridResults, + normalizeFtsQuery, OTTASEARCH_FTS_TABLE, parseJsonStringArray, type D1DatabaseLike, diff --git a/packages/ottasearch/src/search.ts b/packages/ottasearch/src/search.ts index 37d434273..957c324c6 100644 --- a/packages/ottasearch/src/search.ts +++ b/packages/ottasearch/src/search.ts @@ -1,6 +1,8 @@ import type { IndexedSearchDocument } from './types'; export const OTTASEARCH_FTS_TABLE = 'search_documents_fts'; +// Keep FTS queries bounded to avoid expensive broad MATCH scans from long user input. +const MAX_FTS_TERMS = 8; export function collectDocumentText(record: Record, fields: string[]): string { const parts = fields @@ -27,6 +29,16 @@ export function parseJsonStringArray(value: unknown): string[] { } } +export function normalizeFtsQuery(query: string): string { + const terms = query + .split(/\s+/) + .map((term) => term.trim().replace(/["'*]|[^\p{L}\p{N}_-]/gu, '')) + .filter(Boolean) + .slice(0, MAX_FTS_TERMS); + + return terms.map((term) => `${term}*`).join(' OR '); +} + export function mergeHybridResults( ftsResults: IndexedSearchDocument[], semanticResults: IndexedSearchDocument[],