From 219f40b09f7b33873956f24533a2b54407685cd8 Mon Sep 17 00:00:00 2001 From: vincenzodomina Date: Sun, 15 Mar 2026 21:39:55 +0100 Subject: [PATCH 1/7] feat: add supabase state adapter --- .changeset/add-state-supabase-adapter.md | 5 + packages/state-supabase/CHANGELOG.md | 30 ++ packages/state-supabase/README.md | 175 ++++++++ packages/state-supabase/package.json | 58 +++ packages/state-supabase/sql/chat_state.sql | 477 +++++++++++++++++++++ packages/state-supabase/src/index.test.ts | 422 ++++++++++++++++++ packages/state-supabase/src/index.ts | 298 +++++++++++++ packages/state-supabase/tsconfig.json | 10 + packages/state-supabase/tsup.config.ts | 10 + packages/state-supabase/vitest.config.ts | 14 + pnpm-lock.yaml | 98 +++++ 11 files changed, 1597 insertions(+) create mode 100644 .changeset/add-state-supabase-adapter.md create mode 100644 packages/state-supabase/CHANGELOG.md create mode 100644 packages/state-supabase/README.md create mode 100644 packages/state-supabase/package.json create mode 100644 packages/state-supabase/sql/chat_state.sql create mode 100644 packages/state-supabase/src/index.test.ts create mode 100644 packages/state-supabase/src/index.ts create mode 100644 packages/state-supabase/tsconfig.json create mode 100644 packages/state-supabase/tsup.config.ts create mode 100644 packages/state-supabase/vitest.config.ts diff --git a/.changeset/add-state-supabase-adapter.md b/.changeset/add-state-supabase-adapter.md new file mode 100644 index 00000000..6d32cb6e --- /dev/null +++ b/.changeset/add-state-supabase-adapter.md @@ -0,0 +1,5 @@ +--- +"@chat-adapter/state-supabase": minor +--- + +Add a Supabase-backed Chat SDK state adapter that uses RPCs for durable locks, cache, subscriptions, and list state, and ship a copy-paste SQL migration for declarative schema workflows. diff --git a/packages/state-supabase/CHANGELOG.md b/packages/state-supabase/CHANGELOG.md new file mode 100644 index 00000000..fd2c9caa --- /dev/null +++ b/packages/state-supabase/CHANGELOG.md @@ -0,0 +1,30 @@ +# @chat-adapter/state-supabase + +## 4.20.1 + +### Minor Changes + +- Add a Supabase-backed state adapter that stores Chat SDK state in Postgres via Supabase RPCs and ships a copy-paste SQL migration for declarative schema workflows. + +### Patch Changes + +- Updated dependencies + - chat@4.20.1 + +## 4.20.0 + +### Patch Changes + +- chat@4.20.0 + +## 4.19.0 + +### Patch Changes + +- chat@4.19.0 + +## 4.18.0 + +### Patch Changes + +- chat@4.18.0 diff --git a/packages/state-supabase/README.md b/packages/state-supabase/README.md new file mode 100644 index 00000000..59fa3f57 --- /dev/null +++ b/packages/state-supabase/README.md @@ -0,0 +1,175 @@ +# @chat-adapter/state-supabase + +[![npm version](https://img.shields.io/npm/v/@chat-adapter/state-supabase)](https://www.npmjs.com/package/@chat-adapter/state-supabase) +[![npm downloads](https://img.shields.io/npm/dm/@chat-adapter/state-supabase)](https://www.npmjs.com/package/@chat-adapter/state-supabase) + +Production Supabase state adapter for [Chat SDK](https://chat-sdk.dev). It keeps Chat SDK state inside Postgres via Supabase RPCs, so you get durable subscriptions, distributed locks, cache TTLs, and list storage without adding Redis. + +This package is intended for server-side usage. In most production deployments you should pass a service-role Supabase client. + +## Installation + +```bash +pnpm add @chat-adapter/state-supabase +``` + +## Quick start + +1. Copy `sql/chat_state.sql` into your declarative schema folder. +2. Add `chat_state` to your Supabase API exposed schemas. +3. Create a server-side Supabase client and pass it to `createSupabaseState()`. + +```typescript +import { createClient } from "@supabase/supabase-js"; +import { Chat } from "chat"; +import { createSupabaseState } from "@chat-adapter/state-supabase"; + +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + } +); + +const bot = new Chat({ + userName: "mybot", + adapters: { /* ... */ }, + state: createSupabaseState({ client: supabase }), +}); +``` + +## Usage examples + +### Using an existing server-side helper + +```typescript +import { createAdminClient } from "@/lib/supabase/admin"; +import { createSupabaseState } from "@chat-adapter/state-supabase"; + +const state = createSupabaseState({ + client: createAdminClient(), +}); +``` + +### Custom schema and key prefix + +```typescript +const state = createSupabaseState({ + client: supabase, + schema: "bot_state", + keyPrefix: "app-name-prod", +}); +``` + +If you change the schema, replace `chat_state` throughout the provided SQL file and expose that schema in Supabase. + +### Triggering cleanup from application code + +```typescript +await supabase + .schema("chat_state") + .rpc("chat_state_cleanup_expired", { p_key_prefix: "app-name-prod" }); +``` + +### Copying the migration into a declarative schema repo + +```bash +cp node_modules/@chat-adapter/state-supabase/sql/chat_state.sql \ + supabase/schemas/11_chat_state.sql +``` + +## Configuration + +| Option | Required | Description | +|--------|----------|-------------| +| `client` | Yes | Existing `SupabaseClient` instance | +| `schema` | No | Schema containing the RPC functions (default: `"chat_state"`) | +| `keyPrefix` | No | Prefix for all state rows (default: `"chat-sdk"`) | +| `logger` | No | Logger instance (defaults to `ConsoleLogger("info").child("supabase")`) | + +## Migration file + +The package ships a copy-paste migration at `sql/chat_state.sql`. + +It creates: + +```sql +chat_state.chat_state_subscriptions +chat_state.chat_state_locks +chat_state.chat_state_cache +chat_state.chat_state_lists +``` + +and the RPC functions the adapter calls internally. + +The migration intentionally: + +- uses `jsonb` for cache and list values +- fixes the common expired-key bug in `setIfNotExists()` by allowing expired rows to be replaced atomically +- clears or refreshes list TTLs consistently across all rows in a list +- grants RPC execution only to `service_role` by default + +## Why RPCs instead of direct table APIs? + +Supabase table APIs are enough for some simple operations, but a production-grade state adapter still needs server-side atomicity for: + +- lock acquisition with "take over only if expired" semantics +- `setIfNotExists()` that can replace expired keys +- list append + trim + TTL update in a single transaction + +Using RPCs for all state operations keeps the permission model narrower, avoids exposing raw table writes as the primary API, and keeps behavior consistent across operations. + +## Features + +| Feature | Supported | +|---------|-----------| +| Persistence | Yes | +| Multi-instance | Yes | +| Subscriptions | Yes | +| Distributed locking | Yes | +| Key-value caching | Yes (with TTL) | +| List storage | Yes | +| Automatic schema creation | No | +| RPC-only API | Yes | +| Key prefix namespacing | Yes | + +## Locking considerations + +This adapter preserves Chat SDK's existing lock semantics. Lock acquisition is atomic in Postgres, but the higher-level behavior is still bounded by how Chat SDK uses locks. If your handlers can run longer than the configured lock TTL, increase the TTL or use Chat SDK's interruption/conflict patterns as appropriate. + +For extremely high-contention distributed locking, a dedicated Redis-based adapter may still be a better fit. + +## Cleanup behavior + +The adapter does opportunistic cleanup during normal reads and writes: + +- expired locks can be replaced during `acquireLock()` +- expired cache rows are deleted during `get()` +- expired list rows are deleted during `appendToList()` and `getList()` + +For high-throughput deployments, run periodic cleanup as well: + +```sql +select chat_state.chat_state_cleanup_expired(); +``` + +or, for a single namespace: + +```sql +select chat_state.chat_state_cleanup_expired('app-name-prod'); +``` + +## Security notes + +- Prefer a service-role client for server-side bots and background workers. +- Do not use this adapter from browser clients. +- The migration revokes direct table access and exposes RPC execution only to `service_role` by default. +- If you intentionally loosen those grants, do so with a clear threat model. + +## License + +MIT diff --git a/packages/state-supabase/package.json b/packages/state-supabase/package.json new file mode 100644 index 00000000..6844e642 --- /dev/null +++ b/packages/state-supabase/package.json @@ -0,0 +1,58 @@ +{ + "name": "@chat-adapter/state-supabase", + "version": "4.20.1", + "description": "Supabase state adapter for chat (production)", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "sql" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run --coverage", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": { + "@supabase/supabase-js": "^2.99.1", + "chat": "workspace:*" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vercel/chat.git", + "directory": "packages/state-supabase" + }, + "homepage": "https://github.com/vercel/chat#readme", + "bugs": { + "url": "https://github.com/vercel/chat/issues" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/node": "^25.3.2", + "@vitest/coverage-v8": "^4.0.18", + "tsup": "^8.3.5", + "typescript": "^5.7.2", + "vitest": "^4.0.18" + }, + "keywords": [ + "chat", + "state", + "supabase", + "postgres", + "production" + ], + "license": "MIT" +} diff --git a/packages/state-supabase/sql/chat_state.sql b/packages/state-supabase/sql/chat_state.sql new file mode 100644 index 00000000..aa132273 --- /dev/null +++ b/packages/state-supabase/sql/chat_state.sql @@ -0,0 +1,477 @@ +-- Copy this file into your declarative schema folder. +-- +-- By default it creates a `chat_state` schema that exposes RPC functions only. +-- If you want a different schema name, replace `chat_state` throughout this file +-- and pass the same schema to `createSupabaseState({ schema: "..." })`. +-- +-- Important: +-- 1. Add the schema to your Supabase API exposed schemas. +-- 2. The grants below intentionally allow only `service_role` to execute the RPCs. +-- If you want to use another PostgREST role, adjust the grants explicitly. + +create schema if not exists chat_state; + +revoke all on schema chat_state from public, anon, authenticated; +grant usage on schema chat_state to service_role; + +create table if not exists chat_state.chat_state_subscriptions ( + key_prefix text not null, + thread_id text not null, + created_at timestamptz not null default now(), + primary key (key_prefix, thread_id) +); + +create table if not exists chat_state.chat_state_locks ( + key_prefix text not null, + thread_id text not null, + token text not null, + expires_at timestamptz not null, + updated_at timestamptz not null default now(), + primary key (key_prefix, thread_id) +); + +create index if not exists chat_state_locks_expires_idx + on chat_state.chat_state_locks (expires_at); + +create table if not exists chat_state.chat_state_cache ( + key_prefix text not null, + cache_key text not null, + value jsonb not null, + expires_at timestamptz, + updated_at timestamptz not null default now(), + primary key (key_prefix, cache_key) +); + +create index if not exists chat_state_cache_expires_idx + on chat_state.chat_state_cache (expires_at); + +create table if not exists chat_state.chat_state_lists ( + key_prefix text not null, + list_key text not null, + seq bigint generated always as identity, + value jsonb not null, + expires_at timestamptz, + primary key (key_prefix, list_key, seq) +); + +create index if not exists chat_state_lists_expires_idx + on chat_state.chat_state_lists (expires_at); + +alter table chat_state.chat_state_subscriptions enable row level security; +alter table chat_state.chat_state_locks enable row level security; +alter table chat_state.chat_state_cache enable row level security; +alter table chat_state.chat_state_lists enable row level security; + +create or replace function chat_state.chat_state_connect() +returns boolean +language sql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ + select true; +$$; + +create or replace function chat_state.chat_state_subscribe( + p_key_prefix text, + p_thread_id text +) +returns boolean +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +begin + insert into chat_state_subscriptions (key_prefix, thread_id) + values (p_key_prefix, p_thread_id) + on conflict do nothing; + + return true; +end; +$$; + +create or replace function chat_state.chat_state_unsubscribe( + p_key_prefix text, + p_thread_id text +) +returns boolean +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +begin + delete from chat_state_subscriptions + where key_prefix = p_key_prefix + and thread_id = p_thread_id; + + return found; +end; +$$; + +create or replace function chat_state.chat_state_is_subscribed( + p_key_prefix text, + p_thread_id text +) +returns boolean +language sql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ + select exists( + select 1 + from chat_state_subscriptions + where key_prefix = p_key_prefix + and thread_id = p_thread_id + ); +$$; + +create or replace function chat_state.chat_state_acquire_lock( + p_key_prefix text, + p_thread_id text, + p_token text, + p_ttl_ms bigint +) +returns jsonb +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +declare + v_expires_at timestamptz; + v_lock chat_state_locks%rowtype; +begin + v_expires_at := now() + (p_ttl_ms * interval '1 millisecond'); + + insert into chat_state_locks (key_prefix, thread_id, token, expires_at) + values (p_key_prefix, p_thread_id, p_token, v_expires_at) + on conflict (key_prefix, thread_id) do update + set token = excluded.token, + expires_at = excluded.expires_at, + updated_at = now() + where chat_state_locks.expires_at <= now() + returning * into v_lock; + + if not found then + return null; + end if; + + return jsonb_build_object( + 'threadId', v_lock.thread_id, + 'token', v_lock.token, + 'expiresAt', floor(extract(epoch from v_lock.expires_at) * 1000)::bigint + ); +end; +$$; + +create or replace function chat_state.chat_state_force_release_lock( + p_key_prefix text, + p_thread_id text +) +returns boolean +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +begin + delete from chat_state_locks + where key_prefix = p_key_prefix + and thread_id = p_thread_id; + + return found; +end; +$$; + +create or replace function chat_state.chat_state_release_lock( + p_key_prefix text, + p_thread_id text, + p_token text +) +returns boolean +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +begin + delete from chat_state_locks + where key_prefix = p_key_prefix + and thread_id = p_thread_id + and token = p_token; + + return found; +end; +$$; + +create or replace function chat_state.chat_state_extend_lock( + p_key_prefix text, + p_thread_id text, + p_token text, + p_ttl_ms bigint +) +returns boolean +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +begin + update chat_state_locks + set expires_at = now() + (p_ttl_ms * interval '1 millisecond'), + updated_at = now() + where key_prefix = p_key_prefix + and thread_id = p_thread_id + and token = p_token + and expires_at > now(); + + return found; +end; +$$; + +create or replace function chat_state.chat_state_get( + p_key_prefix text, + p_cache_key text +) +returns jsonb +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +declare + v_value jsonb; +begin + select value + into v_value + from chat_state_cache + where key_prefix = p_key_prefix + and cache_key = p_cache_key + and (expires_at is null or expires_at > now()) + limit 1; + + if found then + return v_value; + end if; + + delete from chat_state_cache + where key_prefix = p_key_prefix + and cache_key = p_cache_key + and expires_at is not null + and expires_at <= now(); + + return null; +end; +$$; + +create or replace function chat_state.chat_state_set( + p_key_prefix text, + p_cache_key text, + p_value jsonb, + p_ttl_ms bigint default null +) +returns boolean +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +declare + v_expires_at timestamptz; +begin + v_expires_at := case + when p_ttl_ms is null then null + else now() + (p_ttl_ms * interval '1 millisecond') + end; + + insert into chat_state_cache (key_prefix, cache_key, value, expires_at) + values (p_key_prefix, p_cache_key, p_value, v_expires_at) + on conflict (key_prefix, cache_key) do update + set value = excluded.value, + expires_at = excluded.expires_at, + updated_at = now(); + + return true; +end; +$$; + +create or replace function chat_state.chat_state_set_if_not_exists( + p_key_prefix text, + p_cache_key text, + p_value jsonb, + p_ttl_ms bigint default null +) +returns boolean +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +declare + v_cache_key text; + v_expires_at timestamptz; +begin + v_expires_at := case + when p_ttl_ms is null then null + else now() + (p_ttl_ms * interval '1 millisecond') + end; + + insert into chat_state_cache (key_prefix, cache_key, value, expires_at) + values (p_key_prefix, p_cache_key, p_value, v_expires_at) + on conflict (key_prefix, cache_key) do update + set value = excluded.value, + expires_at = excluded.expires_at, + updated_at = now() + where chat_state_cache.expires_at is not null + and chat_state_cache.expires_at <= now() + returning cache_key into v_cache_key; + + return v_cache_key is not null; +end; +$$; + +create or replace function chat_state.chat_state_delete( + p_key_prefix text, + p_cache_key text +) +returns boolean +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +begin + delete from chat_state_cache + where key_prefix = p_key_prefix + and cache_key = p_cache_key; + + return found; +end; +$$; + +create or replace function chat_state.chat_state_append_to_list( + p_key_prefix text, + p_list_key text, + p_value jsonb, + p_max_length integer default null, + p_ttl_ms bigint default null +) +returns boolean +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +declare + v_expires_at timestamptz; +begin + v_expires_at := case + when p_ttl_ms is null then null + else now() + (p_ttl_ms * interval '1 millisecond') + end; + + delete from chat_state_lists + where key_prefix = p_key_prefix + and list_key = p_list_key + and expires_at is not null + and expires_at <= now(); + + insert into chat_state_lists (key_prefix, list_key, value, expires_at) + values (p_key_prefix, p_list_key, p_value, v_expires_at); + + if p_max_length is not null then + delete from chat_state_lists + where key_prefix = p_key_prefix + and list_key = p_list_key + and seq in ( + select seq + from chat_state_lists + where key_prefix = p_key_prefix + and list_key = p_list_key + order by seq desc + offset greatest(p_max_length, 0) + ); + end if; + + update chat_state_lists + set expires_at = v_expires_at + where key_prefix = p_key_prefix + and list_key = p_list_key; + + return true; +end; +$$; + +create or replace function chat_state.chat_state_get_list( + p_key_prefix text, + p_list_key text +) +returns jsonb +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +declare + v_values jsonb; +begin + delete from chat_state_lists + where key_prefix = p_key_prefix + and list_key = p_list_key + and expires_at is not null + and expires_at <= now(); + + select coalesce(jsonb_agg(value order by seq), '[]'::jsonb) + into v_values + from chat_state_lists + where key_prefix = p_key_prefix + and list_key = p_list_key; + + return v_values; +end; +$$; + +create or replace function chat_state.chat_state_cleanup_expired( + p_key_prefix text default null +) +returns jsonb +language plpgsql +security definer +set search_path = pg_catalog, chat_state, pg_temp +as $$ +declare + v_deleted_cache integer := 0; + v_deleted_lists integer := 0; + v_deleted_locks integer := 0; +begin + delete from chat_state_locks + where expires_at <= now() + and (p_key_prefix is null or key_prefix = p_key_prefix); + get diagnostics v_deleted_locks = row_count; + + delete from chat_state_cache + where expires_at is not null + and expires_at <= now() + and (p_key_prefix is null or key_prefix = p_key_prefix); + get diagnostics v_deleted_cache = row_count; + + delete from chat_state_lists + where expires_at is not null + and expires_at <= now() + and (p_key_prefix is null or key_prefix = p_key_prefix); + get diagnostics v_deleted_lists = row_count; + + return jsonb_build_object( + 'cache', v_deleted_cache, + 'lists', v_deleted_lists, + 'locks', v_deleted_locks + ); +end; +$$; + +revoke all on all tables in schema chat_state from public, anon, authenticated, service_role; +revoke all on all sequences in schema chat_state from public, anon, authenticated, service_role; +revoke all on all functions in schema chat_state from public, anon, authenticated; + +grant execute on all functions in schema chat_state to service_role; + +alter default privileges in schema chat_state + revoke all on tables from public, anon, authenticated, service_role; + +alter default privileges in schema chat_state + revoke all on sequences from public, anon, authenticated, service_role; + +alter default privileges in schema chat_state + revoke execute on functions from public, anon, authenticated; + +alter default privileges in schema chat_state + grant execute on functions to service_role; diff --git a/packages/state-supabase/src/index.test.ts b/packages/state-supabase/src/index.test.ts new file mode 100644 index 00000000..cf0cb5ec --- /dev/null +++ b/packages/state-supabase/src/index.test.ts @@ -0,0 +1,422 @@ +import type { SupabaseClient } from "@supabase/supabase-js"; +import type { Lock, Logger } from "chat"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { createSupabaseState, SupabaseStateAdapter } = await import("./index"); + +const mockLogger: Logger = { + child: () => mockLogger, + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), +}; + +type RpcArgs = Record | undefined; +type RpcCall = { args?: RpcArgs; fn: string; schema: string }; +type RpcResponse = { data: unknown; error: Error | null }; + +function createDeferred() { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + return { promise, reject, resolve }; +} + +function createMockSupabaseClient( + handler?: (schemaName: string, fn: string, args?: RpcArgs) => RpcResponse | Promise +) { + const calls: RpcCall[] = []; + const resolvedHandler = + handler ?? (() => ({ data: null, error: null } satisfies RpcResponse)); + + const schema = vi.fn().mockImplementation((schemaName: string) => ({ + rpc: vi.fn().mockImplementation((fn: string, args?: RpcArgs) => { + calls.push({ args, fn, schema: schemaName }); + return Promise.resolve(resolvedHandler(schemaName, fn, args)); + }), + })); + + return { + calls, + client: { schema } as unknown as SupabaseClient, + schema, + }; +} + +describe("SupabaseStateAdapter", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("should export createSupabaseState function", () => { + expect(typeof createSupabaseState).toBe("function"); + }); + + it("should export SupabaseStateAdapter class", () => { + expect(typeof SupabaseStateAdapter).toBe("function"); + }); + + describe("createSupabaseState", () => { + it("should create an adapter with an existing client", () => { + const { client } = createMockSupabaseClient(); + const adapter = createSupabaseState({ client, logger: mockLogger }); + expect(adapter).toBeInstanceOf(SupabaseStateAdapter); + }); + + it("should create an adapter with custom schema and keyPrefix", () => { + const { client } = createMockSupabaseClient(); + const adapter = createSupabaseState({ + client, + keyPrefix: "custom-prefix", + logger: mockLogger, + schema: "custom_state", + }); + expect(adapter).toBeInstanceOf(SupabaseStateAdapter); + }); + + it("should use default logger when none provided", () => { + const { client } = createMockSupabaseClient(); + const adapter = createSupabaseState({ client }); + expect(adapter).toBeInstanceOf(SupabaseStateAdapter); + }); + + it("should throw when no client is provided", () => { + expect(() => + createSupabaseState({} as Parameters[0]) + ).toThrow("Supabase client is required"); + }); + }); + + describe("ensureConnected", () => { + function createUnconnectedAdapter() { + const { client } = createMockSupabaseClient(); + return new SupabaseStateAdapter({ client, logger: mockLogger }); + } + + it("should throw when calling subscribe before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.subscribe("thread1")).rejects.toThrow("not connected"); + }); + + it("should throw when calling acquireLock before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.acquireLock("thread1", 5000)).rejects.toThrow( + "not connected" + ); + }); + + it("should throw when calling releaseLock before connect", async () => { + const adapter = createUnconnectedAdapter(); + const lock: Lock = { + expiresAt: Date.now() + 5000, + threadId: "thread1", + token: "tok", + }; + await expect(adapter.releaseLock(lock)).rejects.toThrow("not connected"); + }); + + it("should throw when calling get before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.get("key")).rejects.toThrow("not connected"); + }); + + it("should throw when calling appendToList before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.appendToList("key", "value")).rejects.toThrow( + "not connected" + ); + }); + }); + + describe("with mock client", () => { + let adapter: InstanceType; + let calls: RpcCall[]; + let response: RpcResponse; + + beforeEach(async () => { + response = { data: true, error: null }; + const mock = createMockSupabaseClient(() => response); + calls = mock.calls; + adapter = new SupabaseStateAdapter({ + client: mock.client, + logger: mockLogger, + }); + await adapter.connect(); + calls.length = 0; + }); + + afterEach(async () => { + await adapter.disconnect(); + }); + + describe("connect / disconnect", () => { + it("should be idempotent on connect", async () => { + await adapter.connect(); + await adapter.connect(); + expect(calls).toEqual([]); + }); + + it("should deduplicate concurrent connect calls", async () => { + const deferred = createDeferred(); + const mock = createMockSupabaseClient(() => deferred.promise); + const concurrentAdapter = new SupabaseStateAdapter({ + client: mock.client, + logger: mockLogger, + }); + + const pending = Promise.all([ + concurrentAdapter.connect(), + concurrentAdapter.connect(), + ]); + + expect(mock.calls).toHaveLength(1); + expect(mock.calls[0]).toMatchObject({ + args: {}, + fn: "chat_state_connect", + schema: "chat_state", + }); + + deferred.resolve({ data: true, error: null }); + await pending; + }); + + it("should be idempotent on disconnect", async () => { + await adapter.disconnect(); + await adapter.disconnect(); + }); + + it("should handle connect failure and allow retry", async () => { + const error = new Error("migration missing"); + const mock = createMockSupabaseClient(() => ({ data: null, error })); + const failingAdapter = new SupabaseStateAdapter({ + client: mock.client, + logger: mockLogger, + }); + + await expect(failingAdapter.connect()).rejects.toThrow("migration missing"); + expect(mockLogger.error).toHaveBeenCalled(); + + await expect(failingAdapter.connect()).rejects.toThrow("migration missing"); + expect(mock.calls).toHaveLength(2); + }); + }); + + describe("subscriptions", () => { + it("should subscribe using the expected RPC and arguments", async () => { + await adapter.subscribe("slack:C123:1234.5678"); + expect(calls[0]).toEqual({ + args: { + p_key_prefix: "chat-sdk", + p_thread_id: "slack:C123:1234.5678", + }, + fn: "chat_state_subscribe", + schema: "chat_state", + }); + }); + + it("should unsubscribe using the expected RPC and arguments", async () => { + await adapter.unsubscribe("slack:C123:1234.5678"); + expect(calls[0]).toEqual({ + args: { + p_key_prefix: "chat-sdk", + p_thread_id: "slack:C123:1234.5678", + }, + fn: "chat_state_unsubscribe", + schema: "chat_state", + }); + }); + + it("should return true when subscribed", async () => { + response = { data: true, error: null }; + const result = await adapter.isSubscribed("thread1"); + expect(result).toBe(true); + }); + + it("should return false when not subscribed", async () => { + response = { data: false, error: null }; + const result = await adapter.isSubscribed("thread1"); + expect(result).toBe(false); + }); + }); + + describe("locking", () => { + it("should acquire a lock when lock data is returned", async () => { + response = { + data: { + expiresAt: Date.now() + 5000, + threadId: "thread1", + token: "sb_test-token", + }, + error: null, + }; + + const lock = await adapter.acquireLock("thread1", 5000); + expect(lock).not.toBeNull(); + expect(lock?.threadId).toBe("thread1"); + expect(lock?.token).toBe("sb_test-token"); + }); + + it("should return null when lock is already held", async () => { + response = { data: null, error: null }; + const lock = await adapter.acquireLock("thread1", 5000); + expect(lock).toBeNull(); + }); + + it("should release a lock with the expected RPC arguments", async () => { + const lock: Lock = { + expiresAt: Date.now() + 5000, + threadId: "thread1", + token: "sb_test-token", + }; + + await adapter.releaseLock(lock); + expect(calls[0]).toEqual({ + args: { + p_key_prefix: "chat-sdk", + p_thread_id: "thread1", + p_token: "sb_test-token", + }, + fn: "chat_state_release_lock", + schema: "chat_state", + }); + }); + + it("should return true when lock extension succeeds", async () => { + response = { data: true, error: null }; + const lock: Lock = { + expiresAt: Date.now() + 5000, + threadId: "thread1", + token: "sb_test-token", + }; + + const result = await adapter.extendLock(lock, 5000); + expect(result).toBe(true); + }); + + it("should force-release a lock with the expected RPC arguments", async () => { + await adapter.forceReleaseLock("thread1"); + expect(calls[0]).toEqual({ + args: { + p_key_prefix: "chat-sdk", + p_thread_id: "thread1", + }, + fn: "chat_state_force_release_lock", + schema: "chat_state", + }); + }); + }); + + describe("cache", () => { + it("should return JSON data on cache hit", async () => { + response = { data: { foo: "bar" }, error: null }; + const result = await adapter.get("key"); + expect(result).toEqual({ foo: "bar" }); + }); + + it("should return string data on cache hit", async () => { + response = { data: "plain-text", error: null }; + const result = await adapter.get("key"); + expect(result).toBe("plain-text"); + }); + + it("should return null on cache miss", async () => { + response = { data: null, error: null }; + const result = await adapter.get("key"); + expect(result).toBeNull(); + }); + + it("should set a value with the expected RPC arguments", async () => { + await adapter.set("key", { foo: "bar" }, 5000); + expect(calls[0]).toEqual({ + args: { + p_cache_key: "key", + p_key_prefix: "chat-sdk", + p_ttl_ms: 5000, + p_value: { foo: "bar" }, + }, + fn: "chat_state_set", + schema: "chat_state", + }); + }); + + it("should return true when setIfNotExists stores a value", async () => { + response = { data: true, error: null }; + const result = await adapter.setIfNotExists("key", "value", 5000); + expect(result).toBe(true); + }); + + it("should delete a key with the expected RPC arguments", async () => { + await adapter.delete("key"); + expect(calls[0]).toEqual({ + args: { + p_cache_key: "key", + p_key_prefix: "chat-sdk", + }, + fn: "chat_state_delete", + schema: "chat_state", + }); + }); + }); + + describe("appendToList / getList", () => { + it("should append to a list with the expected RPC arguments", async () => { + await adapter.appendToList("mylist", { foo: "bar" }, { maxLength: 10, ttlMs: 60000 }); + expect(calls[0]).toEqual({ + args: { + p_key_prefix: "chat-sdk", + p_list_key: "mylist", + p_max_length: 10, + p_ttl_ms: 60000, + p_value: { foo: "bar" }, + }, + fn: "chat_state_append_to_list", + schema: "chat_state", + }); + }); + + it("should return parsed list items from getList", async () => { + response = { data: [{ id: 1 }, { id: 2 }], error: null }; + const result = await adapter.getList("mylist"); + expect(result).toEqual([{ id: 1 }, { id: 2 }]); + }); + + it("should return empty array when getList returns null", async () => { + response = { data: null, error: null }; + const result = await adapter.getList("mylist"); + expect(result).toEqual([]); + }); + }); + + describe("getClient", () => { + it("should return the underlying client", () => { + const client = adapter.getClient(); + expect(client).toBeDefined(); + }); + }); + + it("should use a custom schema for RPC calls", async () => { + const mock = createMockSupabaseClient(() => ({ data: true, error: null })); + const customSchemaAdapter = new SupabaseStateAdapter({ + client: mock.client, + logger: mockLogger, + schema: "bot_state", + }); + + await customSchemaAdapter.connect(); + await customSchemaAdapter.subscribe("thread1"); + + expect(mock.calls[0]?.schema).toBe("bot_state"); + expect(mock.calls[1]?.schema).toBe("bot_state"); + }); + }); +}); diff --git a/packages/state-supabase/src/index.ts b/packages/state-supabase/src/index.ts new file mode 100644 index 00000000..4de087ae --- /dev/null +++ b/packages/state-supabase/src/index.ts @@ -0,0 +1,298 @@ +import { randomUUID } from "node:crypto"; +import type { SupabaseClient } from "@supabase/supabase-js"; +import type { Lock, Logger, StateAdapter } from "chat"; +import { ConsoleLogger } from "chat"; + +const DEFAULT_KEY_PREFIX = "chat-sdk"; +const DEFAULT_SCHEMA = "chat_state"; + +const RPC = { + acquireLock: "chat_state_acquire_lock", + appendToList: "chat_state_append_to_list", + connect: "chat_state_connect", + delete: "chat_state_delete", + extendLock: "chat_state_extend_lock", + forceReleaseLock: "chat_state_force_release_lock", + get: "chat_state_get", + getList: "chat_state_get_list", + isSubscribed: "chat_state_is_subscribed", + releaseLock: "chat_state_release_lock", + set: "chat_state_set", + setIfNotExists: "chat_state_set_if_not_exists", + subscribe: "chat_state_subscribe", + unsubscribe: "chat_state_unsubscribe", +} as const; + +type AnySupabaseClient = SupabaseClient; +type RpcArgs = Record; + +interface StoredLock { + expiresAt: number; + threadId: string; + token: string; +} + +export interface CreateSupabaseStateOptions { + /** Existing Supabase client instance. Prefer a server-side service-role client. */ + client: AnySupabaseClient; + /** Key prefix for all state rows (default: "chat-sdk") */ + keyPrefix?: string; + /** Logger instance for error reporting */ + logger?: Logger; + /** Schema containing the state RPC functions (default: "chat_state") */ + schema?: string; +} + +export class SupabaseStateAdapter implements StateAdapter { + private readonly client: AnySupabaseClient; + private readonly keyPrefix: string; + private readonly logger: Logger; + private readonly schemaName: string; + private connected = false; + private connectPromise: Promise | null = null; + + constructor(options: CreateSupabaseStateOptions) { + this.client = options.client; + this.keyPrefix = options.keyPrefix || DEFAULT_KEY_PREFIX; + this.logger = options.logger ?? new ConsoleLogger("info").child("supabase"); + this.schemaName = options.schema || DEFAULT_SCHEMA; + } + + async connect(): Promise { + if (this.connected) { + return; + } + + if (!this.connectPromise) { + this.connectPromise = (async () => { + try { + await this.callRpc(RPC.connect); + this.connected = true; + } catch (error) { + this.connectPromise = null; + this.logger.error("Supabase connect failed", { error }); + throw error; + } + })(); + } + + await this.connectPromise; + } + + async disconnect(): Promise { + if (!this.connected) { + return; + } + + this.connected = false; + this.connectPromise = null; + } + + async subscribe(threadId: string): Promise { + this.ensureConnected(); + + await this.callRpc(RPC.subscribe, { + p_key_prefix: this.keyPrefix, + p_thread_id: threadId, + }); + } + + async unsubscribe(threadId: string): Promise { + this.ensureConnected(); + + await this.callRpc(RPC.unsubscribe, { + p_key_prefix: this.keyPrefix, + p_thread_id: threadId, + }); + } + + async isSubscribed(threadId: string): Promise { + this.ensureConnected(); + + const result = await this.callRpc(RPC.isSubscribed, { + p_key_prefix: this.keyPrefix, + p_thread_id: threadId, + }); + + return Boolean(result); + } + + async acquireLock(threadId: string, ttlMs: number): Promise { + this.ensureConnected(); + + const result = await this.callRpc(RPC.acquireLock, { + p_key_prefix: this.keyPrefix, + p_thread_id: threadId, + p_ttl_ms: ttlMs, + p_token: generateToken(), + }); + + return normalizeLock(result); + } + + async forceReleaseLock(threadId: string): Promise { + this.ensureConnected(); + + await this.callRpc(RPC.forceReleaseLock, { + p_key_prefix: this.keyPrefix, + p_thread_id: threadId, + }); + } + + async releaseLock(lock: Lock): Promise { + this.ensureConnected(); + + await this.callRpc(RPC.releaseLock, { + p_key_prefix: this.keyPrefix, + p_thread_id: lock.threadId, + p_token: lock.token, + }); + } + + async extendLock(lock: Lock, ttlMs: number): Promise { + this.ensureConnected(); + + const result = await this.callRpc(RPC.extendLock, { + p_key_prefix: this.keyPrefix, + p_thread_id: lock.threadId, + p_ttl_ms: ttlMs, + p_token: lock.token, + }); + + return Boolean(result); + } + + async get(key: string): Promise { + this.ensureConnected(); + + const result = await this.callRpc(RPC.get, { + p_cache_key: key, + p_key_prefix: this.keyPrefix, + }); + + return result ?? null; + } + + async set(key: string, value: T, ttlMs?: number): Promise { + this.ensureConnected(); + + await this.callRpc(RPC.set, { + p_cache_key: key, + p_key_prefix: this.keyPrefix, + p_ttl_ms: ttlMs ?? null, + p_value: value, + }); + } + + async setIfNotExists( + key: string, + value: unknown, + ttlMs?: number + ): Promise { + this.ensureConnected(); + + const result = await this.callRpc(RPC.setIfNotExists, { + p_cache_key: key, + p_key_prefix: this.keyPrefix, + p_ttl_ms: ttlMs ?? null, + p_value: value, + }); + + return Boolean(result); + } + + async delete(key: string): Promise { + this.ensureConnected(); + + await this.callRpc(RPC.delete, { + p_cache_key: key, + p_key_prefix: this.keyPrefix, + }); + } + + async appendToList( + key: string, + value: unknown, + options?: { maxLength?: number; ttlMs?: number } + ): Promise { + this.ensureConnected(); + + await this.callRpc(RPC.appendToList, { + p_key_prefix: this.keyPrefix, + p_list_key: key, + p_max_length: options?.maxLength ?? null, + p_ttl_ms: options?.ttlMs ?? null, + p_value: value, + }); + } + + async getList(key: string): Promise { + this.ensureConnected(); + + const result = await this.callRpc(RPC.getList, { + p_key_prefix: this.keyPrefix, + p_list_key: key, + }); + + return Array.isArray(result) ? result : []; + } + + getClient(): AnySupabaseClient { + return this.client; + } + + private async callRpc(fn: string, args: RpcArgs = {}): Promise { + const { data, error } = await this.client + .schema(this.schemaName) + .rpc(fn, args); + + if (error) { + throw error; + } + + return data as T; + } + + private ensureConnected(): void { + if (!this.connected) { + throw new Error( + "SupabaseStateAdapter is not connected. Call connect() first." + ); + } + } +} + +function generateToken(): string { + return `sb_${randomUUID()}`; +} + +function normalizeLock(lock: StoredLock | null): Lock | null { + if (!lock) { + return null; + } + + const expiresAt = + typeof lock.expiresAt === "number" ? lock.expiresAt : Number(lock.expiresAt); + + if (!Number.isFinite(expiresAt)) { + return null; + } + + return { + expiresAt, + threadId: lock.threadId, + token: lock.token, + }; +} + +export function createSupabaseState( + options: CreateSupabaseStateOptions +): SupabaseStateAdapter { + if (!options?.client) { + throw new Error( + "Supabase client is required. Create a server-side Supabase client and pass it in options." + ); + } + + return new SupabaseStateAdapter(options); +} diff --git a/packages/state-supabase/tsconfig.json b/packages/state-supabase/tsconfig.json new file mode 100644 index 00000000..8768f5bd --- /dev/null +++ b/packages/state-supabase/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "strictNullChecks": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/state-supabase/tsup.config.ts b/packages/state-supabase/tsup.config.ts new file mode 100644 index 00000000..0313af1c --- /dev/null +++ b/packages/state-supabase/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + clean: true, + sourcemap: true, + external: ["@supabase/supabase-js"], +}); diff --git a/packages/state-supabase/vitest.config.ts b/packages/state-supabase/vitest.config.ts new file mode 100644 index 00000000..edc2d946 --- /dev/null +++ b/packages/state-supabase/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineProject } from "vitest/config"; + +export default defineProject({ + test: { + globals: true, + environment: "node", + coverage: { + provider: "v8", + reporter: ["text", "json-summary"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.test.ts"], + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3a1f680..d1fadeab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -650,6 +650,31 @@ importers: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + packages/state-supabase: + dependencies: + '@supabase/supabase-js': + specifier: ^2.99.1 + version: 2.99.1 + chat: + specifier: workspace:* + version: link:../chat + devDependencies: + '@types/node': + specifier: ^25.3.2 + version: 25.3.2 + '@vitest/coverage-v8': + specifier: ^4.0.18 + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + packages: '@ai-sdk/gateway@2.0.39': @@ -2611,6 +2636,30 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + '@supabase/auth-js@2.99.1': + resolution: {integrity: sha512-x7lKKTvKjABJt/FYcRSPiTT01Xhm2FF8RhfL8+RHMkmlwmRQ88/lREupIHKwFPW0W6pTCJqkZb7Yhpw/EZ+fNw==} + engines: {node: '>=20.0.0'} + + '@supabase/functions-js@2.99.1': + resolution: {integrity: sha512-WQE62W5geYImCO4jzFxCk/avnK7JmOdtqu2eiPz3zOaNiIJajNRSAwMMDgEGd2EMs+sUVYj1LfBjfmW3EzHgIA==} + engines: {node: '>=20.0.0'} + + '@supabase/postgrest-js@2.99.1': + resolution: {integrity: sha512-gtw2ibJrADvfqrpUWXGNlrYUvxttF4WVWfPpTFKOb2IRj7B6YRWMDgcrYqIuD4ZEabK4m6YKQCCGy6clgf1lPA==} + engines: {node: '>=20.0.0'} + + '@supabase/realtime-js@2.99.1': + resolution: {integrity: sha512-9EDdy/5wOseGFqxW88ShV9JMRhm7f+9JGY5x+LqT8c7R0X1CTLwg5qie8FiBWcXTZ+68yYxVWunI+7W4FhkWOg==} + engines: {node: '>=20.0.0'} + + '@supabase/storage-js@2.99.1': + resolution: {integrity: sha512-mf7zPfqofI62SOoyQJeNUVxe72E4rQsbWim6lTDPeLu3lHija/cP5utlQADGrjeTgOUN6znx/rWn7SjrETP1dw==} + engines: {node: '>=20.0.0'} + + '@supabase/supabase-js@2.99.1': + resolution: {integrity: sha512-5MRoYD9ffXq8F6a036dm65YoSHisC3by/d22mauKE99Vrwf792KxYIIr/iqCX7E4hkuugbPZ5EGYHTB7MKy6Vg==} + engines: {node: '>=20.0.0'} + '@svta/cml-608@1.0.1': resolution: {integrity: sha512-Y/Ier9VPUSOBnf0bJqdDyTlPrt4dDB+jk5mYHa1bnD2kcRl8qn7KkW3PRuj4w1aVN+BS2eHmsLxodt7P2hylUg==} engines: {node: '>=20'} @@ -2909,6 +2958,9 @@ packages: '@types/pg@8.18.0': resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==} + '@types/phoenix@1.6.7': + resolution: {integrity: sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -4131,6 +4183,10 @@ packages: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true + iceberg-js@0.8.1: + resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} + engines: {node: '>=20.0.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -7993,6 +8049,44 @@ snapshots: mermaid: 11.12.2 react: 19.2.3 + '@supabase/auth-js@2.99.1': + dependencies: + tslib: 2.8.1 + + '@supabase/functions-js@2.99.1': + dependencies: + tslib: 2.8.1 + + '@supabase/postgrest-js@2.99.1': + dependencies: + tslib: 2.8.1 + + '@supabase/realtime-js@2.99.1': + dependencies: + '@types/phoenix': 1.6.7 + '@types/ws': 8.18.1 + tslib: 2.8.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.99.1': + dependencies: + iceberg-js: 0.8.1 + tslib: 2.8.1 + + '@supabase/supabase-js@2.99.1': + dependencies: + '@supabase/auth-js': 2.99.1 + '@supabase/functions-js': 2.99.1 + '@supabase/postgrest-js': 2.99.1 + '@supabase/realtime-js': 2.99.1 + '@supabase/storage-js': 2.99.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@svta/cml-608@1.0.1': {} '@svta/cml-cmcd@1.0.1(@svta/cml-cta@1.0.1(@svta/cml-structured-field-values@1.0.1(@svta/cml-utils@1.0.1))(@svta/cml-utils@1.0.1))(@svta/cml-structured-field-values@1.0.1(@svta/cml-utils@1.0.1))(@svta/cml-utils@1.0.1)': @@ -8279,6 +8373,8 @@ snapshots: pg-protocol: 1.13.0 pg-types: 2.2.0 + '@types/phoenix@1.6.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -9737,6 +9833,8 @@ snapshots: human-id@4.1.3: {} + iceberg-js@0.8.1: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 From 68a33e19c665c255f1fad604385226354bdde849 Mon Sep 17 00:00:00 2001 From: vincenzodomina Date: Mon, 16 Mar 2026 12:57:12 +0100 Subject: [PATCH 2/7] fix: remove schema prop, minor fixes --- packages/state-supabase/PRD.md | 92 ++++++++++++++++++++++ packages/state-supabase/README.md | 6 +- packages/state-supabase/sql/chat_state.sql | 4 +- packages/state-supabase/src/index.test.ts | 26 +++--- packages/state-supabase/src/index.ts | 11 ++- 5 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 packages/state-supabase/PRD.md diff --git a/packages/state-supabase/PRD.md b/packages/state-supabase/PRD.md new file mode 100644 index 00000000..dab925dd --- /dev/null +++ b/packages/state-supabase/PRD.md @@ -0,0 +1,92 @@ +## Title +Add `@chat-adapter/state-supabase` for Supabase-native production state + +## Issue body +### Summary + +I would like a Supabase-native state adapter for Chat SDK: + +```ts +import { createSupabaseState } from "@chat-adapter/state-supabase"; +``` + +Even though Supabase uses Postgres under the hood, the existing `@chat-adapter/state-pg` adapter is not a good fit for apps that are already built around Supabase as the primary database and access layer. + +### Why this is needed + +In my app, Supabase is already the standard way all database access is handled. I want the Chat SDK state adapter to fit into that same model instead of introducing a second, parallel database access path. + +My main requirements are: + +- I already use Supabase throughout the app and want to stay within that stack. +- I want to reuse the existing Supabase client from my app instead of managing a separate direct Postgres connection URL. +- I want database access to stay centrally controlled through the same Supabase client/configuration patterns I already use. +- I want schema creation and evolution to be managed through my Supabase declarative schema/migrations, not auto-created at runtime. +- I want security, grants, roles, and schema exposure to be explicitly controlled in my Supabase setup. +- I want the adapter internals to reuse Supabase best practices and stable APIs, such as `supabase-js`, PostgREST, and RPCs where appropriate. +- I want the operational convenience of using only Supabase, with no extra direct DB connection setup and no extra cache/infra dependency. + +### Why `@chat-adapter/state-pg` is not enough + +`@chat-adapter/state-pg` is technically Postgres-based, but operationally it solves a different problem. + +For a Supabase app, the gaps are: + +- It expects a direct Postgres connection or `pg` client, not a Supabase client. +- It introduces another database access mechanism alongside the rest of the app. +- It pushes me toward managing separate raw Postgres credentials/URLs instead of reusing the centrally managed Supabase access path. +- It creates its own schema objects on `connect()`, which does not fit teams that manage DB objects declaratively through Supabase migrations. +- It does not align with Supabase-specific security and schema management workflows. +- It does not take advantage of Supabase-native patterns for RPC-based atomic operations and controlled API exposure. + +So while Supabase is "just Postgres" underneath, the developer workflow, security model, and operational model are materially different. + +### Desired developer experience + +Something like this: + +```ts +import { createClient } from "@/lib/supabase/server"; +import { createSupabaseState } from "@chat-adapter/state-supabase"; + +const supabase = await createClient(); + +const bot = new Chat({ + userName: "mybot", + adapters, + state: createSupabaseState({ client: supabase }), +}); +``` + +### Desired packaging/setup model + +- The package should ship a copy-paste SQL migration file that users can add to their own declarative schema folder. +- The migration should support using a dedicated schema, not require `public`. +- The adapter should work against that schema using Supabase APIs. +- Runtime schema creation should not be required. +- Security/grants should remain under user control. + +### Implementation direction + +A good fit would be: + +- `createSupabaseState({ client, keyPrefix?, logger? })` +- use `supabase-js` internally +- use standard Supabase APIs where possible +- use RPCs for the operations that require atomicity or better performance +- keep behavior functionally equivalent to the existing production Postgres adapter from a Chat SDK perspective + +### Acceptance criteria + +- New package: `@chat-adapter/state-supabase` +- Accepts an existing `SupabaseClient` +- Does not require direct Postgres URLs +- Ships SQL schema/migration assets for declarative setup +- Uses a dedicated non-public schema (`chat_state`) for state tables and RPCs +- Supports production state features equivalent to the Postgres adapter: + - subscriptions + - distributed locking + - key-value cache with TTL + - list operations +- Uses Supabase-native access patterns rather than raw `pg` +- Works well for apps that already standardized on Supabase as their backend platform \ No newline at end of file diff --git a/packages/state-supabase/README.md b/packages/state-supabase/README.md index 59fa3f57..286fcc7e 100644 --- a/packages/state-supabase/README.md +++ b/packages/state-supabase/README.md @@ -55,18 +55,15 @@ const state = createSupabaseState({ }); ``` -### Custom schema and key prefix +### Custom key prefix ```typescript const state = createSupabaseState({ client: supabase, - schema: "bot_state", keyPrefix: "app-name-prod", }); ``` -If you change the schema, replace `chat_state` throughout the provided SQL file and expose that schema in Supabase. - ### Triggering cleanup from application code ```typescript @@ -87,7 +84,6 @@ cp node_modules/@chat-adapter/state-supabase/sql/chat_state.sql \ | Option | Required | Description | |--------|----------|-------------| | `client` | Yes | Existing `SupabaseClient` instance | -| `schema` | No | Schema containing the RPC functions (default: `"chat_state"`) | | `keyPrefix` | No | Prefix for all state rows (default: `"chat-sdk"`) | | `logger` | No | Logger instance (defaults to `ConsoleLogger("info").child("supabase")`) | diff --git a/packages/state-supabase/sql/chat_state.sql b/packages/state-supabase/sql/chat_state.sql index aa132273..cfb0da32 100644 --- a/packages/state-supabase/sql/chat_state.sql +++ b/packages/state-supabase/sql/chat_state.sql @@ -369,7 +369,7 @@ begin insert into chat_state_lists (key_prefix, list_key, value, expires_at) values (p_key_prefix, p_list_key, p_value, v_expires_at); - if p_max_length is not null then + if p_max_length is not null and p_max_length > 0 then delete from chat_state_lists where key_prefix = p_key_prefix and list_key = p_list_key @@ -379,7 +379,7 @@ begin where key_prefix = p_key_prefix and list_key = p_list_key order by seq desc - offset greatest(p_max_length, 0) + offset p_max_length ); end if; diff --git a/packages/state-supabase/src/index.test.ts b/packages/state-supabase/src/index.test.ts index cf0cb5ec..4ed6fe9b 100644 --- a/packages/state-supabase/src/index.test.ts +++ b/packages/state-supabase/src/index.test.ts @@ -73,13 +73,12 @@ describe("SupabaseStateAdapter", () => { expect(adapter).toBeInstanceOf(SupabaseStateAdapter); }); - it("should create an adapter with custom schema and keyPrefix", () => { + it("should create an adapter with custom keyPrefix", () => { const { client } = createMockSupabaseClient(); const adapter = createSupabaseState({ client, keyPrefix: "custom-prefix", logger: mockLogger, - schema: "custom_state", }); expect(adapter).toBeInstanceOf(SupabaseStateAdapter); }); @@ -384,6 +383,14 @@ describe("SupabaseStateAdapter", () => { }); }); + it("should pass p_max_length null when maxLength is 0 or omitted (no trim)", async () => { + await adapter.appendToList("mylist", { x: 1 }, { maxLength: 0 }); + expect(calls[0].args).toMatchObject({ p_max_length: null }); + + await adapter.appendToList("mylist", { x: 2 }); + expect(calls[1].args).toMatchObject({ p_max_length: null }); + }); + it("should return parsed list items from getList", async () => { response = { data: [{ id: 1 }, { id: 2 }], error: null }; const result = await adapter.getList("mylist"); @@ -403,20 +410,5 @@ describe("SupabaseStateAdapter", () => { expect(client).toBeDefined(); }); }); - - it("should use a custom schema for RPC calls", async () => { - const mock = createMockSupabaseClient(() => ({ data: true, error: null })); - const customSchemaAdapter = new SupabaseStateAdapter({ - client: mock.client, - logger: mockLogger, - schema: "bot_state", - }); - - await customSchemaAdapter.connect(); - await customSchemaAdapter.subscribe("thread1"); - - expect(mock.calls[0]?.schema).toBe("bot_state"); - expect(mock.calls[1]?.schema).toBe("bot_state"); - }); }); }); diff --git a/packages/state-supabase/src/index.ts b/packages/state-supabase/src/index.ts index 4de087ae..21c69e6d 100644 --- a/packages/state-supabase/src/index.ts +++ b/packages/state-supabase/src/index.ts @@ -39,8 +39,6 @@ export interface CreateSupabaseStateOptions { keyPrefix?: string; /** Logger instance for error reporting */ logger?: Logger; - /** Schema containing the state RPC functions (default: "chat_state") */ - schema?: string; } export class SupabaseStateAdapter implements StateAdapter { @@ -55,7 +53,7 @@ export class SupabaseStateAdapter implements StateAdapter { this.client = options.client; this.keyPrefix = options.keyPrefix || DEFAULT_KEY_PREFIX; this.logger = options.logger ?? new ConsoleLogger("info").child("supabase"); - this.schemaName = options.schema || DEFAULT_SCHEMA; + this.schemaName = DEFAULT_SCHEMA; } async connect(): Promise { @@ -217,10 +215,15 @@ export class SupabaseStateAdapter implements StateAdapter { ): Promise { this.ensureConnected(); + const maxLength = + options?.maxLength != null && options.maxLength > 0 + ? options.maxLength + : null; + await this.callRpc(RPC.appendToList, { p_key_prefix: this.keyPrefix, p_list_key: key, - p_max_length: options?.maxLength ?? null, + p_max_length: maxLength, p_ttl_ms: options?.ttlMs ?? null, p_value: value, }); From fef7c4eb5721b6018837f6fed66ad80d1d27f44e Mon Sep 17 00:00:00 2001 From: vincenzodomina Date: Mon, 16 Mar 2026 13:25:10 +0100 Subject: [PATCH 3/7] feat: add testcontainer integration tests --- packages/state-supabase/README.md | 22 + packages/state-supabase/package.json | 4 + packages/state-supabase/src/index.test.ts | 310 ++++++- packages/state-supabase/vitest.config.ts | 4 + pnpm-lock.yaml | 998 ++++++++++++++++++++-- 5 files changed, 1287 insertions(+), 51 deletions(-) diff --git a/packages/state-supabase/README.md b/packages/state-supabase/README.md index 286fcc7e..f9565dfa 100644 --- a/packages/state-supabase/README.md +++ b/packages/state-supabase/README.md @@ -159,6 +159,28 @@ or, for a single namespace: select chat_state.chat_state_cleanup_expired('app-name-prod'); ``` +## Integration tests (Testcontainers) + +The package includes integration tests that run the migration against a real Postgres instance in Docker and assert on schema, RPCs, and return shapes. They live in `src/index.test.ts` (gated by `RUN_INTEGRATION=1`) and require **Docker**; they are skipped when you run `pnpm test`. + +```bash +# Unit tests only (default; integration block skipped) +pnpm test + +# Integration tests (requires Docker; sets RUN_INTEGRATION=1) +pnpm test:integration +``` + +Integration tests: + +- Start a Postgres 16 container via [testcontainers](https://github.com/testcontainers/testcontainers-node) +- Create roles required by the migration (`service_role`, `anon`, `authenticated`) +- Apply `sql/chat_state.sql` +- Call each RPC with real SQL and assert on results (connect, subscribe, lock, cache, list, cleanup) +- Run the adapter against a pg-backed fake Supabase client to verify the full path + +Use these to validate schema changes, grants, and jsonb behavior before releasing. + ## Security notes - Prefer a service-role client for server-side bots and background workers. diff --git a/packages/state-supabase/package.json b/packages/state-supabase/package.json index 6844e642..664b4fa5 100644 --- a/packages/state-supabase/package.json +++ b/packages/state-supabase/package.json @@ -21,6 +21,7 @@ "dev": "tsup --watch", "test": "vitest run --coverage", "test:watch": "vitest", + "test:integration": "RUN_INTEGRATION=1 vitest run", "typecheck": "tsc --noEmit", "clean": "rm -rf dist" }, @@ -42,7 +43,10 @@ }, "devDependencies": { "@types/node": "^25.3.2", + "@types/pg": "^8.18.0", "@vitest/coverage-v8": "^4.0.18", + "pg": "^8.20.0", + "testcontainers": "^11.12.0", "tsup": "^8.3.5", "typescript": "^5.7.2", "vitest": "^4.0.18" diff --git a/packages/state-supabase/src/index.test.ts b/packages/state-supabase/src/index.test.ts index 4ed6fe9b..fd11f1b1 100644 --- a/packages/state-supabase/src/index.test.ts +++ b/packages/state-supabase/src/index.test.ts @@ -1,9 +1,18 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import type { SupabaseClient } from "@supabase/supabase-js"; +import { createClient } from "@supabase/supabase-js"; import type { Lock, Logger } from "chat"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import pg from "pg"; +import { GenericContainer, Wait } from "testcontainers"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { createSupabaseState, SupabaseStateAdapter } = await import("./index"); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const KEY_PREFIX = "chat-sdk"; + const mockLogger: Logger = { child: () => mockLogger, debug: vi.fn(), @@ -260,7 +269,7 @@ describe("SupabaseStateAdapter", () => { }; const lock = await adapter.acquireLock("thread1", 5000); - expect(lock).not.toBeNull(); + expect(lock !== null).toBe(true); expect(lock?.threadId).toBe("thread1"); expect(lock?.token).toBe("sb_test-token"); }); @@ -411,4 +420,301 @@ describe("SupabaseStateAdapter", () => { }); }); }); + + // Integration: real Postgres via Testcontainers. Run with pnpm test:integration (RUN_INTEGRATION=1). + describe.skipIf(!process.env.RUN_INTEGRATION)( + "integration (Testcontainers Postgres)", + { timeout: 120_000 }, + () => { + let container: Awaited>; + let pool: pg.Pool; + let connectionString: string; + + beforeAll(async () => { + const postgres = await new GenericContainer("postgres:16-alpine") + .withEnvironment({ POSTGRES_PASSWORD: "postgres" }) + .withExposedPorts(5432) + .withWaitStrategy( + Wait.forLogMessage(/database system is ready to accept connections/, 2) + ) + .withStartupTimeout(60_000) + .start(); + + container = postgres; + const host = postgres.getHost(); + const port = postgres.getMappedPort(5432); + connectionString = `postgres://postgres:postgres@${host}:${port}/postgres`; + + pool = new pg.Pool({ connectionString }); + + await pool.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN + CREATE ROLE service_role; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN + CREATE ROLE anon; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN + CREATE ROLE authenticated; + END IF; + END $$; + `); + + const sqlPath = path.join(__dirname, "..", "sql", "chat_state.sql"); + const migrationSql = fs.readFileSync(sqlPath, "utf-8"); + await postgres.copyContentToContainer([ + { content: migrationSql, target: "/tmp/chat_state.sql" }, + ]); + const exec = await postgres.exec([ + "psql", + "-U", + "postgres", + "-d", + "postgres", + "-f", + "/tmp/chat_state.sql", + ]); + if (exec.exitCode !== 0) { + throw new Error(`Migration failed: exit ${exec.exitCode}\n${exec.output}`); + } + }, 90_000); + + afterAll(async () => { + if (pool) await pool.end(); + if (container) await container.stop(); + }); + + describe("schema and RPC exposure", () => { + it("chat_state schema exists", async () => { + const { rows } = await pool.query( + "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'chat_state'" + ); + expect(rows).toHaveLength(1); + expect(rows[0].schema_name).toBe("chat_state"); + }); + + it("chat_state_connect returns true", async () => { + const { rows } = await pool.query( + "SELECT chat_state.chat_state_connect() AS result" + ); + expect(rows).toHaveLength(1); + expect(rows[0].result).toBe(true); + }); + + it("chat_state_subscribe / is_subscribed / unsubscribe", async () => { + await pool.query("SELECT chat_state.chat_state_subscribe($1, $2)", [ + KEY_PREFIX, + "thread-1", + ]); + const { rows: sub } = await pool.query( + "SELECT chat_state.chat_state_is_subscribed($1, $2) AS result", + [KEY_PREFIX, "thread-1"] + ); + expect(sub[0].result).toBe(true); + + await pool.query("SELECT chat_state.chat_state_unsubscribe($1, $2)", [ + KEY_PREFIX, + "thread-1", + ]); + const { rows: unsub } = await pool.query( + "SELECT chat_state.chat_state_is_subscribed($1, $2) AS result", + [KEY_PREFIX, "thread-1"] + ); + expect(unsub[0].result).toBe(false); + }); + + it("chat_state_acquire_lock returns lock shape and expires when held", async () => { + const token1 = "sb_test_" + Date.now(); + const ttlMs = 30_000; + const { rows: r1 } = await pool.query( + "SELECT chat_state.chat_state_acquire_lock($1, $2, $3, $4) AS result", + [KEY_PREFIX, "thread-lock", token1, ttlMs] + ); + expect(r1).toHaveLength(1); + const lock = r1[0].result as { + threadId: string; + token: string; + expiresAt: number; + } | null; + expect(lock !== null).toBe(true); + expect(lock!.threadId).toBe("thread-lock"); + expect(lock!.token).toBe(token1); + expect(typeof lock!.expiresAt).toBe("number"); + expect(lock!.expiresAt).toBeGreaterThan(Date.now()); + + const token2 = "sb_other_" + Date.now(); + const { rows: r2 } = await pool.query( + "SELECT chat_state.chat_state_acquire_lock($1, $2, $3, $4) AS result", + [KEY_PREFIX, "thread-lock", token2, ttlMs] + ); + expect(r2[0].result).toBeNull(); + + await pool.query( + "SELECT chat_state.chat_state_release_lock($1, $2, $3)", + [KEY_PREFIX, "thread-lock", token1] + ); + }); + + it("chat_state_set / get / delete and jsonb roundtrip", async () => { + await pool.query( + "SELECT chat_state.chat_state_set($1, $2, $3, $4)", + [ + KEY_PREFIX, + "cache-key-1", + JSON.stringify({ foo: "bar", n: 42 }), + 60_000, + ] + ); + const { rows: get } = await pool.query( + "SELECT chat_state.chat_state_get($1, $2) AS result", + [KEY_PREFIX, "cache-key-1"] + ); + expect(get[0].result).toEqual({ foo: "bar", n: 42 }); + + await pool.query("SELECT chat_state.chat_state_delete($1, $2)", [ + KEY_PREFIX, + "cache-key-1", + ]); + const { rows: miss } = await pool.query( + "SELECT chat_state.chat_state_get($1, $2) AS result", + [KEY_PREFIX, "cache-key-1"] + ); + expect(miss[0].result).toBeNull(); + }); + + it("chat_state_set_if_not_exists inserts only when missing or expired", async () => { + const { rows: first } = await pool.query( + "SELECT chat_state.chat_state_set_if_not_exists($1, $2, $3, $4) AS result", + [KEY_PREFIX, "dedupe-key", JSON.stringify(true), 10_000] + ); + expect(first[0].result).toBe(true); + + const { rows: second } = await pool.query( + "SELECT chat_state.chat_state_set_if_not_exists($1, $2, $3, $4) AS result", + [KEY_PREFIX, "dedupe-key", JSON.stringify(true), 10_000] + ); + expect(second[0].result).toBe(false); + }); + + it("chat_state_append_to_list and get_list", async () => { + await pool.query( + "SELECT chat_state.chat_state_append_to_list($1, $2, $3, $4, $5)", + [KEY_PREFIX, "list-1", JSON.stringify({ id: 1 }), 10, 60_000] + ); + await pool.query( + "SELECT chat_state.chat_state_append_to_list($1, $2, $3, $4, $5)", + [KEY_PREFIX, "list-1", JSON.stringify({ id: 2 }), 10, 60_000] + ); + const { rows } = await pool.query( + "SELECT chat_state.chat_state_get_list($1, $2) AS result", + [KEY_PREFIX, "list-1"] + ); + expect(rows[0].result).toEqual([{ id: 1 }, { id: 2 }]); + }); + + it("chat_state_cleanup_expired returns counts", async () => { + const { rows } = await pool.query( + "SELECT chat_state.chat_state_cleanup_expired() AS result" + ); + const result = rows[0].result as { + cache: number; + lists: number; + locks: number; + }; + expect(typeof result.cache).toBe("number"); + expect(typeof result.lists).toBe("number"); + expect(typeof result.locks).toBe("number"); + }); + }); + + describe("adapter against real Postgres", () => { + const RPC_PARAM_ORDER: Record = { + chat_state_connect: [], + chat_state_subscribe: ["p_key_prefix", "p_thread_id"], + chat_state_unsubscribe: ["p_key_prefix", "p_thread_id"], + chat_state_is_subscribed: ["p_key_prefix", "p_thread_id"], + chat_state_acquire_lock: [ + "p_key_prefix", + "p_thread_id", + "p_token", + "p_ttl_ms", + ], + chat_state_force_release_lock: ["p_key_prefix", "p_thread_id"], + chat_state_release_lock: [ + "p_key_prefix", + "p_thread_id", + "p_token", + ], + chat_state_extend_lock: [ + "p_key_prefix", + "p_thread_id", + "p_token", + "p_ttl_ms", + ], + chat_state_get: ["p_key_prefix", "p_cache_key"], + chat_state_set: [ + "p_key_prefix", + "p_cache_key", + "p_value", + "p_ttl_ms", + ], + chat_state_set_if_not_exists: [ + "p_key_prefix", + "p_cache_key", + "p_value", + "p_ttl_ms", + ], + chat_state_delete: ["p_key_prefix", "p_cache_key"], + chat_state_append_to_list: [ + "p_key_prefix", + "p_list_key", + "p_value", + "p_max_length", + "p_ttl_ms", + ], + chat_state_get_list: ["p_key_prefix", "p_list_key"], + chat_state_cleanup_expired: ["p_key_prefix"], + }; + + it("adapter connect, subscribe, get, set with pg-backed fake client", async () => { + const poolForClient = new pg.Pool({ connectionString }); + + const fakeSupabase = { + schema: (_schemaName: string) => ({ + rpc: async (fn: string, args: Record) => { + const paramOrder = RPC_PARAM_ORDER[fn] ?? []; + const values = paramOrder.map((name) => args[name]); + const placeholders = values + .map((_, i) => `$${i + 1}`) + .join(", "); + const sql = `SELECT chat_state.${fn}(${placeholders}) AS result`; + const { rows } = await poolForClient.query( + sql, + values as unknown[] + ); + const data = rows[0]?.result ?? null; + return { data, error: null }; + }, + }), + } as unknown as ReturnType; + + const adapter = createSupabaseState({ client: fakeSupabase }); + await adapter.connect(); + await adapter.subscribe("integration-thread"); + const subscribed = await adapter.isSubscribed("integration-thread"); + expect(subscribed).toBe(true); + + await adapter.set("integration-cache-key", { x: 1 }, 5000); + const value = + await adapter.get<{ x: number }>("integration-cache-key"); + expect(value).toEqual({ x: 1 }); + + await adapter.disconnect(); + await poolForClient.end(); + }); + }); + } + ); }); diff --git a/packages/state-supabase/vitest.config.ts b/packages/state-supabase/vitest.config.ts index edc2d946..b4da363f 100644 --- a/packages/state-supabase/vitest.config.ts +++ b/packages/state-supabase/vitest.config.ts @@ -1,9 +1,13 @@ import { defineProject } from "vitest/config"; +const runIntegration = process.env.RUN_INTEGRATION === "1"; + export default defineProject({ test: { globals: true, environment: "node", + testTimeout: runIntegration ? 120_000 : 10_000, + hookTimeout: runIntegration ? 90_000 : 10_000, coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1fadeab..b2323923 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 2.29.8(@types/node@25.3.2) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) knip: specifier: ^5.85.0 version: 5.85.0(@types/node@25.3.2)(typescript@5.9.3) @@ -31,7 +31,7 @@ importers: version: 7.2.4 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) apps/docs: dependencies: @@ -88,7 +88,7 @@ importers: version: 16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) fumadocs-mdx: specifier: 14.0.4 - version: 14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) fumadocs-ui: specifier: 16.2.2 version: 16.2.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18) @@ -268,13 +268,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/adapter-gchat: dependencies: @@ -296,13 +296,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/adapter-github: dependencies: @@ -324,13 +324,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/adapter-linear: dependencies: @@ -349,13 +349,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/adapter-shared: dependencies: @@ -368,13 +368,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/adapter-slack: dependencies: @@ -393,13 +393,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/adapter-teams: dependencies: @@ -427,13 +427,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/adapter-telegram: dependencies: @@ -449,13 +449,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/adapter-whatsapp: dependencies: @@ -471,13 +471,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/chat: dependencies: @@ -511,13 +511,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/integration-tests: dependencies: @@ -557,7 +557,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/state-ioredis: dependencies: @@ -573,13 +573,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/state-memory: dependencies: @@ -592,13 +592,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/state-pg: dependencies: @@ -617,16 +617,16 @@ importers: version: 8.18.0 '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/state-redis: dependencies: @@ -642,13 +642,13 @@ importers: version: 25.3.2 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/state-supabase: dependencies: @@ -662,18 +662,27 @@ importers: '@types/node': specifier: ^25.3.2 version: 25.3.2 + '@types/pg': + specifier: ^8.18.0 + version: 8.18.0 '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) + pg: + specifier: ^8.20.0 + version: 8.20.0 + testcontainers: + specifier: ^11.12.0 + version: 11.12.0 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -803,6 +812,9 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -1170,6 +1182,20 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -1367,6 +1393,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + '@linear/sdk@76.0.0': resolution: {integrity: sha512-Xt0x5Kl6qBoWhGFypb8ykyP+c5kT7scmRPs1uJidSPOaRgkMJ/4y41QpmZCWCBUMmZtf/O0VktgQio6rLXT94w==} engines: {node: '>=18.x'} @@ -1696,6 +1728,36 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2922,6 +2984,12 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -2952,6 +3020,9 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@25.3.2': resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} @@ -2972,6 +3043,15 @@ packages: '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} + + '@types/ssh2@0.5.52': + resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} + + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -3099,6 +3179,10 @@ packages: '@workflow/serde@4.1.0-beta.2': resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3151,6 +3235,14 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -3165,6 +3257,9 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -3176,6 +3271,12 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -3185,6 +3286,14 @@ packages: axios@1.13.6: resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + b4a@1.8.0: + resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -3195,6 +3304,44 @@ packages: resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} engines: {node: 20 || >=22} + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.5.5: + resolution: {integrity: sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.8.0: + resolution: {integrity: sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.8.1: + resolution: {integrity: sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.3.2: + resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3216,6 +3363,9 @@ packages: bcp-47@2.1.0: resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} @@ -3226,6 +3376,9 @@ packages: bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + botbuilder-core@4.23.3: resolution: {integrity: sha512-48iW739I24piBH683b/Unvlu1fSzjB69ViOwZ0PbTkN2yW5cTvHJWlW7bXntO8GSqJfssgPaVthKfyaCW457ig==} @@ -3258,12 +3411,23 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -3274,6 +3438,10 @@ packages: peerDependencies: esbuild: '>=0.18' + byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -3335,6 +3503,9 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -3348,6 +3519,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + cloudflare-video-element@1.3.5: resolution: {integrity: sha512-zj9gjJa6xW8MNrfc4oKuwgGS0njRLpOlQjdifbuNxvy8k4Y3pKCyKCMG2XIsjd2iQGhgjS57b1P5VWdJlxcXBw==} @@ -3401,6 +3576,10 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} @@ -3411,12 +3590,28 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cross-fetch@4.1.0: resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} @@ -3693,6 +3888,18 @@ packages: resolution: {integrity: sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==} engines: {node: '>=18'} + docker-compose@1.3.2: + resolution: {integrity: sha512-FO/Jemn08gf9o9E6qtqOPQpyauwf2rQAzfpoUlMyqNpdaVb0ImR/wXKoutLZKp1tks58F8Z8iR7va7H1ne09cw==} + engines: {node: '>= 6.0.0'} + + docker-modem@5.0.6: + resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} + engines: {node: '>= 8.0'} + + dockerode@4.0.9: + resolution: {integrity: sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==} + engines: {node: '>= 8.0'} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -3729,6 +3936,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -3775,6 +3985,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -3808,12 +4022,23 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -3834,6 +4059,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3920,6 +4148,9 @@ packages: react-dom: optional: true + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.3.3: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} @@ -4021,6 +4252,10 @@ packages: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} @@ -4033,6 +4268,10 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -4213,6 +4452,9 @@ packages: imsc@1.1.5: resolution: {integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -4288,6 +4530,9 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -4399,6 +4644,10 @@ packages: layout-base@2.0.1: resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + lie@3.1.1: resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} @@ -4500,6 +4749,9 @@ packages: lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -4539,6 +4791,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -4812,6 +5067,10 @@ packages: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -4823,6 +5082,14 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -4862,6 +5129,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nan@2.25.0: + resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4924,6 +5194,10 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + npm-to-yarn@3.0.1: resolution: {integrity: sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4944,6 +5218,9 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -5162,15 +5439,36 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + properties-reader@3.0.1: + resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} + engines: {node: '>=18'} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -5253,6 +5551,20 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -5355,6 +5667,10 @@ packages: remend@1.2.1: resolution: {integrity: sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -5362,6 +5678,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -5398,6 +5718,9 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -5461,6 +5784,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -5496,6 +5822,9 @@ packages: spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -5506,6 +5835,13 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + ssh-remote-port-forward@1.0.4: + resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -5521,6 +5857,9 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5529,6 +5868,12 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -5597,10 +5942,32 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-fs@3.1.2: + resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.1.8: + resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + testcontainers@11.12.0: + resolution: {integrity: sha512-VWtH+UQejVYYvb53ohEZRbx2naxyDvwO9lQ6A0VgmVE2Oh8r9EF09I+BfmrXpd9N9ntpzhao9di2yNwibSz5KA==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -5633,6 +6000,10 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5728,6 +6099,9 @@ packages: tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + twitch-video-element@0.1.6: resolution: {integrity: sha512-X7l8gy+DEFKJ/EztUwaVnAYwQN9fUJxPkOVJj2sE62sGvGU4DNLyvmOsmVulM+8Plc5dMg6hYIMNRAPaH+39Uw==} @@ -5752,6 +6126,9 @@ packages: oxlint: optional: true + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -5759,6 +6136,10 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} + undici@7.24.4: + resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} + engines: {node: '>=20.18.1'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -6005,6 +6386,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -6041,9 +6425,30 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + youtube-video-element@1.8.1: resolution: {integrity: sha512-+5UuAGaj+5AnBf39huLVpy/4dLtR0rmJP1TxOHVZ81bac4ZHFpTtQ4Dz2FAn2GPnfXISezvUEaQoAdFW4hH9Xg==} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -6222,6 +6627,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@balena/dockerignore@1.0.2': {} + '@bcoe/v8-coverage@1.0.2': {} '@biomejs/biome@2.4.4': @@ -6618,6 +7025,25 @@ snapshots: dependencies: graphql: 15.10.1 + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + '@iconify/types@2.0.0': {} '@iconify/utils@3.1.0': @@ -6764,6 +7190,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@linear/sdk@76.0.0(graphql@15.10.1)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@15.10.1) @@ -7088,6 +7522,29 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -8335,6 +8792,17 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 25.3.2 + '@types/ssh2': 1.15.5 + + '@types/dockerode@4.0.1': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 25.3.2 + '@types/ssh2': 1.15.5 + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -8363,6 +8831,10 @@ snapshots: '@types/node@12.20.55': {} + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@25.3.2': dependencies: undici-types: 7.18.2 @@ -8385,6 +8857,19 @@ snapshots: '@types/retry@0.12.0': {} + '@types/ssh2-streams@0.1.13': + dependencies: + '@types/node': 25.3.2 + + '@types/ssh2@0.5.52': + dependencies: + '@types/node': 25.3.2 + '@types/ssh2-streams': 0.1.13 + + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + '@types/trusted-types@2.0.7': optional: true @@ -8429,7 +8914,7 @@ snapshots: native-promise-only: 0.8.1 weakmap-polyfill: 2.0.4 - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -8441,7 +8926,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) '@vitest/expect@4.0.18': dependencies: @@ -8452,13 +8937,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(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.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.18': dependencies: @@ -8486,6 +8971,10 @@ snapshots: '@workflow/serde@4.1.0-beta.2': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -8526,6 +9015,30 @@ snapshots: any-promise@1.3.0: {} + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.8 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -8538,6 +9051,10 @@ snapshots: array-union@2.1.0: {} + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + assertion-error@2.0.1: {} ast-v8-to-istanbul@0.3.12: @@ -8548,6 +9065,10 @@ snapshots: astring@1.9.0: {} + async-lock@1.4.1: {} + + async@3.2.6: {} + asynckit@0.4.0: {} axios@1.13.2: @@ -8566,12 +9087,47 @@ snapshots: transitivePeerDependencies: - debug + b4a@1.8.0: {} + bail@2.0.2: {} balanced-match@1.0.2: {} balanced-match@4.0.3: {} + bare-events@2.8.2: {} + + bare-fs@4.5.5: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.8.1(bare-events@2.8.2) + bare-url: 2.3.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.8.0: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.8.0 + + bare-stream@2.8.1(bare-events@2.8.2): + dependencies: + streamx: 2.23.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-url@2.3.2: + dependencies: + bare-path: 3.0.0 + base64-js@1.5.1: {} base64url@3.0.1: {} @@ -8591,6 +9147,10 @@ snapshots: is-alphanumerical: 2.0.1 is-decimal: 2.0.1 + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + before-after-hook@4.0.0: {} better-path-resolve@1.0.0: @@ -8599,6 +9159,12 @@ snapshots: bignumber.js@9.3.1: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + botbuilder-core@4.23.3: dependencies: botbuilder-dialogs-adaptive-runtime-core: 4.23.3-preview @@ -8700,13 +9266,23 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffer@6.0.3: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + buildcheck@0.0.7: + optional: true + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -8716,6 +9292,8 @@ snapshots: esbuild: 0.27.2 load-tsconfig: 0.2.5 + byline@5.0.0: {} + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -8774,6 +9352,8 @@ snapshots: dependencies: readdirp: 5.0.0 + chownr@1.1.4: {} + ci-info@3.9.0: {} citty@0.2.1: {} @@ -8784,6 +9364,12 @@ snapshots: client-only@0.0.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cloudflare-video-element@1.3.5: {} clsx@2.1.1: {} @@ -8826,12 +9412,22 @@ snapshots: commander@8.3.0: {} + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + compute-scroll-into-view@3.1.1: {} confbox@0.1.8: {} consola@3.4.2: {} + core-util-is@1.0.3: {} + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -8840,6 +9436,19 @@ snapshots: dependencies: layout-base: 2.0.1 + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.25.0 + optional: true + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + cross-fetch@4.1.0: dependencies: node-fetch: 2.7.0 @@ -9157,6 +9766,31 @@ snapshots: - bufferutil - utf-8-validate + docker-compose@1.3.2: + dependencies: + yaml: 2.8.2 + + docker-modem@5.0.6: + dependencies: + debug: 4.4.3 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@4.0.9: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.3 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.6 + protobufjs: 7.5.4 + tar-fs: 2.1.4 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -9197,6 +9831,10 @@ snapshots: emoji-regex@9.2.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -9271,6 +9909,8 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} + escape-string-regexp@5.0.0: {} esprima@4.0.1: {} @@ -9312,10 +9952,20 @@ snapshots: dependencies: '@types/estree': 1.0.8 + event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + eventsource-parser@3.0.6: {} expect-type@1.3.0: {} @@ -9328,6 +9978,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9410,6 +10062,8 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + fs-constants@1.0.0: {} + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 @@ -9460,7 +10114,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)): + fumadocs-mdx@14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 @@ -9484,7 +10138,7 @@ snapshots: optionalDependencies: next: 16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 - vite: 7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -9544,6 +10198,8 @@ snapshots: transitivePeerDependencies: - supports-color + get-caller-file@2.0.5: {} + get-east-asian-width@1.4.0: {} get-intrinsic@1.3.0: @@ -9561,6 +10217,8 @@ snapshots: get-nonce@1.0.1: {} + get-port@7.1.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -9855,6 +10513,8 @@ snapshots: dependencies: sax: 1.2.1 + inherits@2.0.4: {} + inline-style-parser@0.2.7: {} internmap@1.0.1: {} @@ -9918,6 +10578,8 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isarray@1.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -10040,6 +10702,10 @@ snapshots: layout-base@2.0.1: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + lie@3.1.1: dependencies: immediate: 3.0.6 @@ -10111,6 +10777,8 @@ snapshots: lodash-es@4.17.23: {} + lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} lodash.includes@4.3.0: {} @@ -10137,6 +10805,8 @@ snapshots: lodash@4.17.21: {} + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -10712,6 +11382,10 @@ snapshots: dependencies: brace-expansion: 5.0.2 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -10720,6 +11394,10 @@ snapshots: minipass@7.1.3: {} + mkdirp-classic@0.5.3: {} + + mkdirp@3.0.1: {} + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -10755,6 +11433,9 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nan@2.25.0: + optional: true + nanoid@3.3.11: {} nanoid@5.1.6: {} @@ -10805,6 +11486,8 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + normalize-path@3.0.0: {} + npm-to-yarn@3.0.1: {} nypm@0.6.5: @@ -10819,6 +11502,10 @@ snapshots: obug@2.1.1: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.4: @@ -11003,13 +11690,14 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.6 tsx: 4.21.0 + yaml: 2.8.2 postcss-selector-parser@7.1.1: dependencies: @@ -11040,16 +11728,53 @@ snapshots: prettier@2.8.8: {} + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + properties-reader@3.0.1: + dependencies: + '@kwsites/file-exists': 1.1.1 + mkdirp: 3.0.1 + transitivePeerDependencies: + - supports-color + property-information@7.1.0: {} + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.3.2 + long: 5.3.2 + proxy-from-env@1.1.0: {} + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -11190,6 +11915,34 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -11363,10 +12116,14 @@ snapshots: remend@1.2.1: {} + require-directory@2.1.1: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + retry@0.12.0: {} + retry@0.13.1: {} reusify@1.1.0: {} @@ -11422,6 +12179,8 @@ snapshots: rw@1.3.3: {} + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -11519,6 +12278,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} sisteransi@1.0.5: {} @@ -11543,12 +12304,27 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + split-ca@1.0.1: {} + split2@4.2.0: {} spotify-audio-element@1.0.4: {} sprintf-js@1.0.3: {} + ssh-remote-port-forward@1.0.4: + dependencies: + '@types/ssh2': 0.5.52 + ssh2: 1.17.0 + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.25.0 + stackback@0.0.2: {} standard-as-callback@2.1.0: {} @@ -11577,6 +12353,15 @@ snapshots: transitivePeerDependencies: - supports-color + streamx@2.23.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -11589,6 +12374,14 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -11649,8 +12442,82 @@ snapshots: tapable@2.3.0: {} + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-fs@3.1.2: + dependencies: + pump: 3.0.4 + tar-stream: 3.1.8 + optionalDependencies: + bare-fs: 4.5.5 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.1.8: + dependencies: + b4a: 1.8.0 + bare-fs: 4.5.5 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + teex@1.0.1: + dependencies: + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + term-size@2.2.1: {} + testcontainers@11.12.0: + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 4.0.1 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.4.3 + docker-compose: 1.3.2 + dockerode: 4.0.9 + get-port: 7.1.0 + proper-lockfile: 4.1.2 + properties-reader: 3.0.1 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.1.2 + tmp: 0.2.5 + undici: 7.24.4 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.0 + transitivePeerDependencies: + - react-native-b4a + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -11676,6 +12543,8 @@ snapshots: tinyrainbow@3.0.3: {} + tmp@0.2.5: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -11698,7 +12567,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3): + tsup@8.5.1(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) cac: 6.7.14 @@ -11709,7 +12578,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) resolve-from: 5.0.0 rollup: 4.54.0 source-map: 0.7.6 @@ -11762,6 +12631,8 @@ snapshots: tw-animate-css@1.4.0: {} + tweetnacl@0.14.5: {} + twitch-video-element@0.1.6: {} typescript@5.9.3: {} @@ -11779,10 +12650,14 @@ snapshots: jsonc-parser: 3.3.1 nypm: 0.6.5 + undici-types@5.26.5: {} + undici-types@7.18.2: {} undici@6.21.3: {} + undici@7.24.4: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -11905,7 +12780,7 @@ snapshots: dependencies: '@vimeo/player': 2.29.0 - vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): + vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -11919,11 +12794,12 @@ snapshots: jiti: 2.6.1 lightningcss: 1.30.2 tsx: 4.21.0 + yaml: 2.8.2 - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(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.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -11940,7 +12816,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -12015,6 +12891,8 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + ws@7.5.10: {} ws@8.18.3: {} @@ -12029,8 +12907,30 @@ snapshots: xtend@4.0.2: {} + y18n@5.0.8: {} + + yaml@2.8.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + youtube-video-element@1.8.1: {} + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod@3.25.76: {} zod@4.3.3: {} From 78d6c54a4486eff815fa3ee0cfbf4899ecfb3961 Mon Sep 17 00:00:00 2001 From: vincenzodomina Date: Mon, 16 Mar 2026 13:56:49 +0100 Subject: [PATCH 4/7] feat: add testcontainer integration tests, fixes --- packages/state-supabase/sql/chat_state.sql | 10 +++++--- packages/state-supabase/src/index.test.ts | 28 +++++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/state-supabase/sql/chat_state.sql b/packages/state-supabase/sql/chat_state.sql index cfb0da32..521dfd79 100644 --- a/packages/state-supabase/sql/chat_state.sql +++ b/packages/state-supabase/sql/chat_state.sql @@ -1,8 +1,6 @@ -- Copy this file into your declarative schema folder. -- -- By default it creates a `chat_state` schema that exposes RPC functions only. --- If you want a different schema name, replace `chat_state` throughout this file --- and pass the same schema to `createSupabaseState({ schema: "..." })`. -- -- Important: -- 1. Add the schema to your Supabase API exposed schemas. @@ -344,7 +342,7 @@ create or replace function chat_state.chat_state_append_to_list( p_key_prefix text, p_list_key text, p_value jsonb, - p_max_length integer default null, + p_max_length bigint default null, p_ttl_ms bigint default null ) returns boolean @@ -354,7 +352,13 @@ set search_path = pg_catalog, chat_state, pg_temp as $$ declare v_expires_at timestamptz; + v_lock_key bigint; begin + -- Serialize concurrent appends for the same list so trim + TTL stay correct. + /* Separator must not appear in key_prefix/list_key; chr(1) is ASCII SOH, safe for wire protocol. */ + v_lock_key := pg_catalog.hashtext(p_key_prefix || chr(1) || p_list_key); + perform pg_catalog.pg_advisory_xact_lock(v_lock_key); + v_expires_at := case when p_ttl_ms is null then null else now() + (p_ttl_ms * interval '1 millisecond') diff --git a/packages/state-supabase/src/index.test.ts b/packages/state-supabase/src/index.test.ts index fd11f1b1..2786b87c 100644 --- a/packages/state-supabase/src/index.test.ts +++ b/packages/state-supabase/src/index.test.ts @@ -467,8 +467,11 @@ describe("SupabaseStateAdapter", () => { await postgres.copyContentToContainer([ { content: migrationSql, target: "/tmp/chat_state.sql" }, ]); + // Stop on first error so we see which statement failed (e.g. CREATE FUNCTION). const exec = await postgres.exec([ "psql", + "-v", + "ON_ERROR_STOP=1", "-U", "postgres", "-d", @@ -477,7 +480,26 @@ describe("SupabaseStateAdapter", () => { "/tmp/chat_state.sql", ]); if (exec.exitCode !== 0) { - throw new Error(`Migration failed: exit ${exec.exitCode}\n${exec.output}`); + const out = exec.output ?? ""; + const err = (exec as { errorOutput?: string }).errorOutput ?? ""; + throw new Error( + `Migration failed: exit ${exec.exitCode}\nstdout:\n${out}\nstderr:\n${err}` + ); + } + + // Ensure append_to_list was created (catches CREATE failures that would otherwise surface as "function does not exist"). + const { rows: procs } = await pool.query<{ proname: string; args: string }>(` + SELECT p.proname, pg_get_function_identity_arguments(p.oid) AS args + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = 'chat_state' + ORDER BY p.proname + `); + const appendFn = procs.find((r) => r.proname === "chat_state_append_to_list"); + if (!appendFn) { + throw new Error( + `chat_state_append_to_list not found after migration. Functions in chat_state: ${procs.map((p) => `${p.proname}(${p.args})`).join(", ")}` + ); } }, 90_000); @@ -600,11 +622,11 @@ describe("SupabaseStateAdapter", () => { it("chat_state_append_to_list and get_list", async () => { await pool.query( - "SELECT chat_state.chat_state_append_to_list($1, $2, $3, $4, $5)", + "SELECT chat_state.chat_state_append_to_list($1::text, $2::text, $3::jsonb, $4::bigint, $5::bigint)", [KEY_PREFIX, "list-1", JSON.stringify({ id: 1 }), 10, 60_000] ); await pool.query( - "SELECT chat_state.chat_state_append_to_list($1, $2, $3, $4, $5)", + "SELECT chat_state.chat_state_append_to_list($1::text, $2::text, $3::jsonb, $4::bigint, $5::bigint)", [KEY_PREFIX, "list-1", JSON.stringify({ id: 2 }), 10, 60_000] ); const { rows } = await pool.query( From 024cb3ab9cb30a663b0705e22ec6f0e4c974d8c3 Mon Sep 17 00:00:00 2001 From: vincenzodomina Date: Mon, 16 Mar 2026 14:00:04 +0100 Subject: [PATCH 5/7] feat: add testcontainer integration tests, fixes --- packages/state-supabase/src/index.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/state-supabase/src/index.ts b/packages/state-supabase/src/index.ts index 21c69e6d..5cb9635c 100644 --- a/packages/state-supabase/src/index.ts +++ b/packages/state-supabase/src/index.ts @@ -26,6 +26,14 @@ const RPC = { type AnySupabaseClient = SupabaseClient; type RpcArgs = Record; +/** Normalize TTL: treat undefined, null, or <= 0 as "no expiry" (null), matching memory/Redis adapters. */ +function normalizeTtlMs(ttlMs?: number | null): number | null { + if (ttlMs == null || ttlMs <= 0 || !Number.isFinite(ttlMs)) { + return null; + } + return ttlMs; +} + interface StoredLock { expiresAt: number; threadId: string; @@ -177,7 +185,7 @@ export class SupabaseStateAdapter implements StateAdapter { await this.callRpc(RPC.set, { p_cache_key: key, p_key_prefix: this.keyPrefix, - p_ttl_ms: ttlMs ?? null, + p_ttl_ms: normalizeTtlMs(ttlMs), p_value: value, }); } @@ -192,7 +200,7 @@ export class SupabaseStateAdapter implements StateAdapter { const result = await this.callRpc(RPC.setIfNotExists, { p_cache_key: key, p_key_prefix: this.keyPrefix, - p_ttl_ms: ttlMs ?? null, + p_ttl_ms: normalizeTtlMs(ttlMs), p_value: value, }); @@ -224,7 +232,7 @@ export class SupabaseStateAdapter implements StateAdapter { p_key_prefix: this.keyPrefix, p_list_key: key, p_max_length: maxLength, - p_ttl_ms: options?.ttlMs ?? null, + p_ttl_ms: normalizeTtlMs(options?.ttlMs), p_value: value, }); } From 0161b49a02a16e8a29960ebbde06b6d636b6f947 Mon Sep 17 00:00:00 2001 From: vincenzodomina Date: Mon, 16 Mar 2026 14:16:44 +0100 Subject: [PATCH 6/7] feat: add testcontainer integration tests, more tests --- packages/state-supabase/src/index.test.ts | 230 +++++++++++++++++++++- 1 file changed, 229 insertions(+), 1 deletion(-) diff --git a/packages/state-supabase/src/index.test.ts b/packages/state-supabase/src/index.test.ts index 2786b87c..3a4e3ead 100644 --- a/packages/state-supabase/src/index.test.ts +++ b/packages/state-supabase/src/index.test.ts @@ -103,6 +103,12 @@ describe("SupabaseStateAdapter", () => { createSupabaseState({} as Parameters[0]) ).toThrow("Supabase client is required"); }); + + it("should not call Supabase before connect", () => { + const mock = createMockSupabaseClient(); + createSupabaseState({ client: mock.client, logger: mockLogger }); + expect(mock.schema).not.toHaveBeenCalled(); + }); }); describe("ensureConnected", () => { @@ -144,6 +150,63 @@ describe("SupabaseStateAdapter", () => { "not connected" ); }); + + it("should throw when calling unsubscribe before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.unsubscribe("thread1")).rejects.toThrow( + "not connected" + ); + }); + + it("should throw when calling isSubscribed before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.isSubscribed("thread1")).rejects.toThrow( + "not connected" + ); + }); + + it("should throw when calling forceReleaseLock before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.forceReleaseLock("thread1")).rejects.toThrow( + "not connected" + ); + }); + + it("should throw when calling extendLock before connect", async () => { + const adapter = createUnconnectedAdapter(); + const lock: Lock = { + expiresAt: Date.now() + 5000, + threadId: "thread1", + token: "tok", + }; + await expect(adapter.extendLock(lock, 5000)).rejects.toThrow( + "not connected" + ); + }); + + it("should throw when calling set before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.set("key", "value")).rejects.toThrow( + "not connected" + ); + }); + + it("should throw when calling setIfNotExists before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.setIfNotExists("key", "value")).rejects.toThrow( + "not connected" + ); + }); + + it("should throw when calling delete before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.delete("key")).rejects.toThrow("not connected"); + }); + + it("should throw when calling getList before connect", async () => { + const adapter = createUnconnectedAdapter(); + await expect(adapter.getList("key")).rejects.toThrow("not connected"); + }); }); describe("with mock client", () => { @@ -217,6 +280,35 @@ describe("SupabaseStateAdapter", () => { await expect(failingAdapter.connect()).rejects.toThrow("migration missing"); expect(mock.calls).toHaveLength(2); }); + + it("should throw when calling any method after disconnect", async () => { + await adapter.disconnect(); + await expect(adapter.subscribe("thread1")).rejects.toThrow( + "not connected" + ); + await expect(adapter.get("key")).rejects.toThrow("not connected"); + }); + }); + + describe("RPC error propagation", () => { + it("should throw when RPC returns error", async () => { + const rpcError = new Error("rpc failed"); + const mock = createMockSupabaseClient((_schema, fn) => + fn === "chat_state_get" || fn === "chat_state_subscribe" + ? { data: null, error: rpcError } + : { data: true, error: null } + ); + const errorAdapter = new SupabaseStateAdapter({ + client: mock.client, + logger: mockLogger, + }); + await errorAdapter.connect(); + + await expect(errorAdapter.get("key")).rejects.toThrow("rpc failed"); + await expect(errorAdapter.subscribe("thread1")).rejects.toThrow( + "rpc failed" + ); + }); }); describe("subscriptions", () => { @@ -322,6 +414,46 @@ describe("SupabaseStateAdapter", () => { schema: "chat_state", }); }); + + it("should return false when lock extension fails", async () => { + response = { data: false, error: null }; + const lock: Lock = { + expiresAt: Date.now() + 5000, + threadId: "thread1", + token: "sb_test-token", + }; + const result = await adapter.extendLock(lock, 5000); + expect(result).toBe(false); + }); + + it("should normalize lock when RPC returns expiresAt as string", async () => { + const expiresAtMs = Date.now() + 5000; + response = { + data: { + expiresAt: String(expiresAtMs), + threadId: "thread1", + token: "sb_test-token", + }, + error: null, + }; + const lock = await adapter.acquireLock("thread1", 5000); + expect(lock).not.toBeNull(); + expect(lock?.expiresAt).toBe(expiresAtMs); + expect(typeof lock?.expiresAt).toBe("number"); + }); + + it("should return null when RPC returns lock with non-finite expiresAt", async () => { + response = { + data: { + expiresAt: Number.NaN, + threadId: "thread1", + token: "sb_test-token", + }, + error: null, + }; + const lock = await adapter.acquireLock("thread1", 5000); + expect(lock).toBeNull(); + }); }); describe("cache", () => { @@ -357,12 +489,52 @@ describe("SupabaseStateAdapter", () => { }); }); + it("should send p_ttl_ms null when set is called without TTL", async () => { + await adapter.set("key", { a: 1 }); + expect(calls[0].args).toMatchObject({ p_ttl_ms: null }); + }); + + it("should send p_ttl_ms null when set is called with 0 or negative TTL", async () => { + await adapter.set("key", "v", 0); + expect(calls[0].args).toMatchObject({ p_ttl_ms: null }); + await adapter.set("key", "v", -1); + expect(calls[1].args).toMatchObject({ p_ttl_ms: null }); + }); + it("should return true when setIfNotExists stores a value", async () => { response = { data: true, error: null }; const result = await adapter.setIfNotExists("key", "value", 5000); expect(result).toBe(true); }); + it("should return false when setIfNotExists finds existing key", async () => { + response = { data: false, error: null }; + const result = await adapter.setIfNotExists("key", "value"); + expect(result).toBe(false); + }); + + it("should send p_ttl_ms null when setIfNotExists is called without TTL", async () => { + response = { data: true, error: null }; + await adapter.setIfNotExists("key", "value"); + expect(calls[0].args).toMatchObject({ p_ttl_ms: null }); + }); + + it("should send p_ttl_ms null when setIfNotExists is called with 0 TTL", async () => { + response = { data: true, error: null }; + await adapter.setIfNotExists("key", "value", 0); + expect(calls[0].args).toMatchObject({ p_ttl_ms: null }); + }); + + it("should return array and number on cache hit", async () => { + response = { data: [1, 2, 3], error: null }; + const arr = await adapter.get("key"); + expect(arr).toEqual([1, 2, 3]); + + response = { data: 42, error: null }; + const num = await adapter.get("key"); + expect(num).toBe(42); + }); + it("should delete a key with the expected RPC arguments", async () => { await adapter.delete("key"); expect(calls[0]).toEqual({ @@ -392,12 +564,36 @@ describe("SupabaseStateAdapter", () => { }); }); - it("should pass p_max_length null when maxLength is 0 or omitted (no trim)", async () => { + it("should pass p_max_length null when maxLength is 0, negative, or omitted (no trim)", async () => { await adapter.appendToList("mylist", { x: 1 }, { maxLength: 0 }); expect(calls[0].args).toMatchObject({ p_max_length: null }); await adapter.appendToList("mylist", { x: 2 }); expect(calls[1].args).toMatchObject({ p_max_length: null }); + + await adapter.appendToList("mylist", { x: 3 }, { maxLength: -5 }); + expect(calls[2].args).toMatchObject({ p_max_length: null }); + }); + + it("should pass p_ttl_ms null when appendToList is called with only maxLength", async () => { + await adapter.appendToList("mylist", { x: 1 }, { maxLength: 5 }); + expect(calls[0].args).toMatchObject({ + p_max_length: 5, + p_ttl_ms: null, + }); + }); + + it("should pass p_ttl_ms when appendToList is called with only ttlMs", async () => { + await adapter.appendToList("mylist", { x: 1 }, { ttlMs: 30_000 }); + expect(calls[0].args).toMatchObject({ + p_max_length: null, + p_ttl_ms: 30_000, + }); + }); + + it("should pass p_ttl_ms null when appendToList is called with ttlMs 0", async () => { + await adapter.appendToList("mylist", { x: 1 }, { ttlMs: 0 }); + expect(calls[0].args).toMatchObject({ p_ttl_ms: null }); }); it("should return parsed list items from getList", async () => { @@ -419,6 +615,38 @@ describe("SupabaseStateAdapter", () => { expect(client).toBeDefined(); }); }); + + describe("StateAdapter contract", () => { + const stateAdapterMethods = [ + "connect", + "disconnect", + "subscribe", + "unsubscribe", + "isSubscribed", + "acquireLock", + "forceReleaseLock", + "releaseLock", + "extendLock", + "get", + "set", + "setIfNotExists", + "delete", + "appendToList", + "getList", + ] as const; + + it("should implement all StateAdapter methods", () => { + for (const method of stateAdapterMethods) { + expect(adapter).toHaveProperty(method); + expect(typeof adapter[method]).toBe("function"); + } + }); + + it("should expose getClient for advanced usage", () => { + expect(adapter).toHaveProperty("getClient"); + expect(typeof adapter.getClient).toBe("function"); + }); + }); }); // Integration: real Postgres via Testcontainers. Run with pnpm test:integration (RUN_INTEGRATION=1). From cfb103fa428618b22f49ff5b5a43421cdea50ebc Mon Sep 17 00:00:00 2001 From: vincenzodomina Date: Mon, 16 Mar 2026 14:47:12 +0100 Subject: [PATCH 7/7] fix: pnpm check, typecheck, build issues --- packages/state-supabase/src/index.test.ts | 117 ++++++++++++++-------- packages/state-supabase/src/index.ts | 12 ++- 2 files changed, 86 insertions(+), 43 deletions(-) diff --git a/packages/state-supabase/src/index.test.ts b/packages/state-supabase/src/index.test.ts index 3a4e3ead..78cb022f 100644 --- a/packages/state-supabase/src/index.test.ts +++ b/packages/state-supabase/src/index.test.ts @@ -1,18 +1,29 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { SupabaseClient } from "@supabase/supabase-js"; -import { createClient } from "@supabase/supabase-js"; +import type { createClient, SupabaseClient } from "@supabase/supabase-js"; import type { Lock, Logger } from "chat"; import pg from "pg"; import { GenericContainer, Wait } from "testcontainers"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; const { createSupabaseState, SupabaseStateAdapter } = await import("./index"); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const KEY_PREFIX = "chat-sdk"; +/** Postgres log message used by Testcontainers wait strategy. */ +const POSTGRES_READY_REGEX = /database system is ready to accept connections/; + const mockLogger: Logger = { child: () => mockLogger, debug: vi.fn(), @@ -22,8 +33,17 @@ const mockLogger: Logger = { }; type RpcArgs = Record | undefined; -type RpcCall = { args?: RpcArgs; fn: string; schema: string }; -type RpcResponse = { data: unknown; error: Error | null }; + +interface RpcCall { + args?: RpcArgs; + fn: string; + schema: string; +} + +interface RpcResponse { + data: unknown; + error: Error | null; +} function createDeferred() { let resolve!: (value: T | PromiseLike) => void; @@ -38,11 +58,15 @@ function createDeferred() { } function createMockSupabaseClient( - handler?: (schemaName: string, fn: string, args?: RpcArgs) => RpcResponse | Promise + handler?: ( + schemaName: string, + fn: string, + args?: RpcArgs + ) => RpcResponse | Promise ) { const calls: RpcCall[] = []; const resolvedHandler = - handler ?? (() => ({ data: null, error: null } satisfies RpcResponse)); + handler ?? (() => ({ data: null, error: null }) satisfies RpcResponse); const schema = vi.fn().mockImplementation((schemaName: string) => ({ rpc: vi.fn().mockImplementation((fn: string, args?: RpcArgs) => { @@ -53,7 +77,7 @@ function createMockSupabaseClient( return { calls, - client: { schema } as unknown as SupabaseClient, + client: { schema } as unknown as SupabaseClient, schema, }; } @@ -119,7 +143,9 @@ describe("SupabaseStateAdapter", () => { it("should throw when calling subscribe before connect", async () => { const adapter = createUnconnectedAdapter(); - await expect(adapter.subscribe("thread1")).rejects.toThrow("not connected"); + await expect(adapter.subscribe("thread1")).rejects.toThrow( + "not connected" + ); }); it("should throw when calling acquireLock before connect", async () => { @@ -274,10 +300,14 @@ describe("SupabaseStateAdapter", () => { logger: mockLogger, }); - await expect(failingAdapter.connect()).rejects.toThrow("migration missing"); + await expect(failingAdapter.connect()).rejects.toThrow( + "migration missing" + ); expect(mockLogger.error).toHaveBeenCalled(); - await expect(failingAdapter.connect()).rejects.toThrow("migration missing"); + await expect(failingAdapter.connect()).rejects.toThrow( + "migration missing" + ); expect(mock.calls).toHaveLength(2); }); @@ -550,7 +580,11 @@ describe("SupabaseStateAdapter", () => { describe("appendToList / getList", () => { it("should append to a list with the expected RPC arguments", async () => { - await adapter.appendToList("mylist", { foo: "bar" }, { maxLength: 10, ttlMs: 60000 }); + await adapter.appendToList( + "mylist", + { foo: "bar" }, + { maxLength: 10, ttlMs: 60000 } + ); expect(calls[0]).toEqual({ args: { p_key_prefix: "chat-sdk", @@ -662,9 +696,7 @@ describe("SupabaseStateAdapter", () => { const postgres = await new GenericContainer("postgres:16-alpine") .withEnvironment({ POSTGRES_PASSWORD: "postgres" }) .withExposedPorts(5432) - .withWaitStrategy( - Wait.forLogMessage(/database system is ready to accept connections/, 2) - ) + .withWaitStrategy(Wait.forLogMessage(POSTGRES_READY_REGEX, 2)) .withStartupTimeout(60_000) .start(); @@ -716,14 +748,19 @@ describe("SupabaseStateAdapter", () => { } // Ensure append_to_list was created (catches CREATE failures that would otherwise surface as "function does not exist"). - const { rows: procs } = await pool.query<{ proname: string; args: string }>(` + const { rows: procs } = await pool.query<{ + proname: string; + args: string; + }>(` SELECT p.proname, pg_get_function_identity_arguments(p.oid) AS args FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = 'chat_state' ORDER BY p.proname `); - const appendFn = procs.find((r) => r.proname === "chat_state_append_to_list"); + const appendFn = procs.find( + (r) => r.proname === "chat_state_append_to_list" + ); if (!appendFn) { throw new Error( `chat_state_append_to_list not found after migration. Functions in chat_state: ${procs.map((p) => `${p.proname}(${p.args})`).join(", ")}` @@ -732,8 +769,12 @@ describe("SupabaseStateAdapter", () => { }, 90_000); afterAll(async () => { - if (pool) await pool.end(); - if (container) await container.stop(); + if (pool) { + await pool.end(); + } + if (container) { + await container.stop(); + } }); describe("schema and RPC exposure", () => { @@ -776,7 +817,7 @@ describe("SupabaseStateAdapter", () => { }); it("chat_state_acquire_lock returns lock shape and expires when held", async () => { - const token1 = "sb_test_" + Date.now(); + const token1 = `sb_test_${Date.now()}`; const ttlMs = 30_000; const { rows: r1 } = await pool.query( "SELECT chat_state.chat_state_acquire_lock($1, $2, $3, $4) AS result", @@ -789,12 +830,12 @@ describe("SupabaseStateAdapter", () => { expiresAt: number; } | null; expect(lock !== null).toBe(true); - expect(lock!.threadId).toBe("thread-lock"); - expect(lock!.token).toBe(token1); - expect(typeof lock!.expiresAt).toBe("number"); - expect(lock!.expiresAt).toBeGreaterThan(Date.now()); + expect(lock?.threadId).toBe("thread-lock"); + expect(lock?.token).toBe(token1); + expect(typeof lock?.expiresAt).toBe("number"); + expect(lock?.expiresAt).toBeGreaterThan(Date.now()); - const token2 = "sb_other_" + Date.now(); + const token2 = `sb_other_${Date.now()}`; const { rows: r2 } = await pool.query( "SELECT chat_state.chat_state_acquire_lock($1, $2, $3, $4) AS result", [KEY_PREFIX, "thread-lock", token2, ttlMs] @@ -808,15 +849,12 @@ describe("SupabaseStateAdapter", () => { }); it("chat_state_set / get / delete and jsonb roundtrip", async () => { - await pool.query( - "SELECT chat_state.chat_state_set($1, $2, $3, $4)", - [ - KEY_PREFIX, - "cache-key-1", - JSON.stringify({ foo: "bar", n: 42 }), - 60_000, - ] - ); + await pool.query("SELECT chat_state.chat_state_set($1, $2, $3, $4)", [ + KEY_PREFIX, + "cache-key-1", + JSON.stringify({ foo: "bar", n: 42 }), + 60_000, + ]); const { rows: get } = await pool.query( "SELECT chat_state.chat_state_get($1, $2) AS result", [KEY_PREFIX, "cache-key-1"] @@ -892,11 +930,7 @@ describe("SupabaseStateAdapter", () => { "p_ttl_ms", ], chat_state_force_release_lock: ["p_key_prefix", "p_thread_id"], - chat_state_release_lock: [ - "p_key_prefix", - "p_thread_id", - "p_token", - ], + chat_state_release_lock: ["p_key_prefix", "p_thread_id", "p_token"], chat_state_extend_lock: [ "p_key_prefix", "p_thread_id", @@ -957,8 +991,9 @@ describe("SupabaseStateAdapter", () => { expect(subscribed).toBe(true); await adapter.set("integration-cache-key", { x: 1 }, 5000); - const value = - await adapter.get<{ x: number }>("integration-cache-key"); + const value = await adapter.get<{ x: number }>( + "integration-cache-key" + ); expect(value).toEqual({ x: 1 }); await adapter.disconnect(); diff --git a/packages/state-supabase/src/index.ts b/packages/state-supabase/src/index.ts index 5cb9635c..434291a6 100644 --- a/packages/state-supabase/src/index.ts +++ b/packages/state-supabase/src/index.ts @@ -23,7 +23,13 @@ const RPC = { unsubscribe: "chat_state_unsubscribe", } as const; -type AnySupabaseClient = SupabaseClient; +/** Minimal Database shape required by SupabaseClient; index signature allows any schema name. */ +interface AnyDatabase { + PostgrestVersion: string; + [schema: string]: unknown; +} + +type AnySupabaseClient = SupabaseClient; type RpcArgs = Record; /** Normalize TTL: treat undefined, null, or <= 0 as "no expiry" (null), matching memory/Redis adapters. */ @@ -283,7 +289,9 @@ function normalizeLock(lock: StoredLock | null): Lock | null { } const expiresAt = - typeof lock.expiresAt === "number" ? lock.expiresAt : Number(lock.expiresAt); + typeof lock.expiresAt === "number" + ? lock.expiresAt + : Number(lock.expiresAt); if (!Number.isFinite(expiresAt)) { return null;