From 4ac750e65b0ef429fcfae45c83d4a9e5508ce143 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 25 Sep 2026 09:28:40 -0700 Subject: [PATCH] feat(pwa): move app.logicsrc.com from Turso/libSQL to Postgres via @profullstack/libsql-pg (#218) Production reads DATABASE_URL (postgres://) through @profullstack/libsql-pg, which keeps the @libsql/client surface; no query changed. A missing, non-Postgres or leftover libsql:// URL fails at boot in production. Dev and tests keep libSQL (file:/:memory:) as a devDependency. Postgres migrations in src/migrations-pg/ (converted with libsql-pg convert-schema, same file names so the copied _migrations ledger matches); migrate.mjs picks the dialect. Tests run against Postgres with PWA_TEST_DATABASE_URL. Co-authored-by: Claude Fable 5.1 --- apps/pwa/README.md | 28 ++- apps/pwa/package.json | 7 +- apps/pwa/src/config.mjs | 9 +- apps/pwa/src/db.mjs | 57 ++++- apps/pwa/src/migrate.mjs | 14 +- apps/pwa/src/migrations-pg/001_auth.sql | 53 ++++ apps/pwa/src/migrations-pg/002_credshare.sql | 89 +++++++ .../migrations-pg/003_cli_device_codes.sql | 16 ++ apps/pwa/test/credshare-rekey.test.mjs | 12 +- apps/pwa/test/credshare-vaults.test.mjs | 12 +- apps/pwa/test/team-members.test.mjs | 12 +- package-lock.json | 227 +++++++++++++++++- 12 files changed, 507 insertions(+), 29 deletions(-) create mode 100644 apps/pwa/src/migrations-pg/001_auth.sql create mode 100644 apps/pwa/src/migrations-pg/002_credshare.sql create mode 100644 apps/pwa/src/migrations-pg/003_cli_device_codes.sql diff --git a/apps/pwa/README.md b/apps/pwa/README.md index ef5b164..ce941cf 100644 --- a/apps/pwa/README.md +++ b/apps/pwa/README.md @@ -1,16 +1,40 @@ # @logicsrc/pwa — LogicSRC credentials -Express + libSQL/Turso app for **team credential sharing**: auth (email/password, +Express app on Postgres for **team credential sharing**: auth (email/password, passkeys, CoinPay OAuth, sessions, `lsk_` CLI API keys) + end-to-end-encrypted team vaults. Zero-knowledge — the server only stores ciphertext, per-member sealed vault keys, and identity public keys. Decryption happens in the `logicsrc` CLI. ```bash -cp .env.example .env # set SESSION_SECRET; TURSO_* for prod (else local file db) +cp .env.example .env # set SESSION_SECRET; DATABASE_URL=postgres://… for prod (else a local libSQL file db) npm install npm start # migrates on boot, serves on :8080 ``` +## Database + +Production runs on Postgres: `DATABASE_URL` must be a `postgres://` URL and the +app refuses to start on anything else there (a leftover `libsql://` value is +called out by name). The client is +[`@profullstack/libsql-pg`](https://github.com/profullstack/libsql-pg), which +keeps the `@libsql/client` surface the code was written against and rewrites +the remaining SQLite idioms per statement. Development and the tests use libSQL +itself (`file:./data/local.db` by default, `:memory:` in tests). + +Migrations run at boot and live in two dialect copies with the same file names: +`src/migrations/` (SQLite) and `src/migrations-pg/` (Postgres, generated with +`npx libsql-pg convert-schema` and reviewed). `npm run migrate` applies the copy +matching `DATABASE_URL`. To move an existing Turso database: + +```bash +DATABASE_URL=postgres://… npm run migrate # schema +npx libsql-pg copy --from "$TURSO_DATABASE_URL" --token "$TURSO_AUTH_TOKEN" \ + --to "$DATABASE_URL" --verify # rows +``` + +Set `PWA_TEST_DATABASE_URL=postgres://…` to run the test suite against a real +Postgres (it drops and recreates the `public` schema of that database). + The CLI connects with `LOGICSRC_API= logicsrc login` (browser OAuth-PKCE loopback → an `lsk_` key). See `docs/credential-sharing.md` in the repo root. diff --git a/apps/pwa/package.json b/apps/pwa/package.json index b21b888..d975708 100644 --- a/apps/pwa/package.json +++ b/apps/pwa/package.json @@ -3,7 +3,7 @@ "version": "0.1.1", "private": true, "type": "module", - "description": "LogicSRC credentials — Express + libSQL/Turso app: auth + end-to-end-encrypted team credential sharing.", + "description": "LogicSRC credentials — Express app on Postgres (@profullstack/libsql-pg): auth + end-to-end-encrypted team credential sharing.", "engines": { "node": ">=20" }, @@ -14,10 +14,13 @@ "test": "node --test" }, "dependencies": { - "@libsql/client": "^0.14.0", + "@profullstack/libsql-pg": "^0.1.2", "@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/server": "^13.1.0", "cookie-parser": "^1.4.7", "express": "^4.21.2" + }, + "devDependencies": { + "@libsql/client": "^0.14.0" } } diff --git a/apps/pwa/src/config.mjs b/apps/pwa/src/config.mjs index 882708a..74072c6 100644 --- a/apps/pwa/src/config.mjs +++ b/apps/pwa/src/config.mjs @@ -36,9 +36,12 @@ export const config = { rpName: "LogicSRC", sessionSecret: process.env.SESSION_SECRET || "dev-insecure-secret-change-me", db: { - // Turso (libSQL) in prod; a local file for dev. TURSO_* takes precedence. - url: process.env.TURSO_DATABASE_URL || process.env.DATABASE_URL || "file:./data/local.db", - authToken: process.env.TURSO_AUTH_TOKEN || process.env.DATABASE_AUTH_TOKEN || undefined, + // Postgres (postgres://) in production, through @profullstack/libsql-pg; a + // local libSQL file or `:memory:` for dev and tests. DATABASE_URL wins. A + // leftover TURSO_DATABASE_URL is still read so that db.mjs can refuse it + // with a message that names it, rather than silently opening a file db. + url: process.env.DATABASE_URL || process.env.TURSO_DATABASE_URL || "file:./data/local.db", + authToken: process.env.DATABASE_AUTH_TOKEN || process.env.TURSO_AUTH_TOKEN || undefined, }, resend: { apiKey: process.env.RESEND_API_KEY || "", diff --git a/apps/pwa/src/db.mjs b/apps/pwa/src/db.mjs index 037b35e..92641e1 100644 --- a/apps/pwa/src/db.mjs +++ b/apps/pwa/src/db.mjs @@ -1,17 +1,58 @@ -// libSQL (SQLite / Turso) client + a tiny query helper. -import { createClient } from "@libsql/client"; +// Database client + a tiny query helper. +// +// Production runs on Postgres through @profullstack/libsql-pg, which keeps the +// @libsql/client surface (execute / batch / rows / rowsAffected) over a `pg` +// pool and rewrites the SQLite idioms in our statements per query, so no +// caller changed when the app left Turso. Development and the test suite keep +// using libSQL itself (a local file or `:memory:`), which is a devDependency. import fs from "node:fs"; import path from "node:path"; +import { createClient as createPgClient } from "@profullstack/libsql-pg"; import { config } from "./config.mjs"; -// For a local file: url, make sure the directory exists. -if (config.db.url.startsWith("file:")) { - const p = config.db.url.slice("file:".length); - const dir = path.dirname(path.resolve(config.root, p)); - fs.mkdirSync(dir, { recursive: true }); +const POSTGRES = /^postgres(ql)?:\/\//i; + +/** True when `url` is a Postgres DSN (what production must use). */ +export const isPostgresUrl = (url) => POSTGRES.test(url); + +/** + * Fail fast on a database URL the app cannot run on. A missing or non-Postgres + * URL in production is a deploy error, not a condition to limp along under: + * there is no fallback to a file database there. + */ +export function assertDatabaseUrl(url = config.db.url, env = config.env) { + if (isPostgresUrl(url)) return url; + if (/^libsql:/i.test(url) || /\.turso\.io/i.test(url)) { + throw new Error( + "DATABASE_URL must be a postgres:// URL. A libsql:// (Turso) URL was found" + + (process.env.TURSO_DATABASE_URL ? " in TURSO_DATABASE_URL" : "") + + "; the app moved to Postgres. Copy the data with `npx libsql-pg copy` and unset TURSO_*.", + ); + } + if (env === "production") { + throw new Error(`DATABASE_URL must be a postgres:// URL in production, got "${url.split(":")[0]}:"`); + } + if (url === ":memory:" || url.startsWith("file:")) return url; + throw new Error(`Unsupported DATABASE_URL "${url.split(":")[0]}:": use postgres://, file: or :memory:`); } -export const db = createClient({ url: config.db.url, authToken: config.db.authToken }); +async function open() { + const url = assertDatabaseUrl(); + if (isPostgresUrl(url)) return createPgClient({ url, dialect: "sqlite" }); + + // Local libSQL for dev and tests only (devDependency, never loaded in production). + if (url.startsWith("file:")) { + const dir = path.dirname(path.resolve(config.root, url.slice("file:".length))); + fs.mkdirSync(dir, { recursive: true }); + } + const { createClient } = await import("@libsql/client"); + return createClient({ url, authToken: config.db.authToken }); +} + +export const db = await open(); + +/** True when the live client talks to Postgres. */ +export const isPostgres = isPostgresUrl(config.db.url); /** Run a statement; returns the raw result. */ export const run = (sql, args = []) => db.execute({ sql, args }); diff --git a/apps/pwa/src/migrate.mjs b/apps/pwa/src/migrate.mjs index 906fa94..244501d 100644 --- a/apps/pwa/src/migrate.mjs +++ b/apps/pwa/src/migrate.mjs @@ -1,11 +1,19 @@ // Apply SQL migrations in order. Idempotent — tracks applied files in _migrations. +// +// Two copies of every migration exist, one per dialect, with the SAME file +// names: `migrations/` (SQLite, for the local dev/test database) and +// `migrations-pg/` (Postgres, generated with `npx libsql-pg convert-schema` +// and reviewed). The ledger is keyed by file name, so a database copied from +// Turso with `libsql-pg copy` carries its `_migrations` rows across and the +// Postgres side recognises them as applied. import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { db, run, all } from "./db.mjs"; +import { db, run, all, isPostgres } from "./db.mjs"; const HERE = path.dirname(fileURLToPath(import.meta.url)); -const DIR = path.join(HERE, "migrations"); +export const MIGRATIONS_DIR = path.join(HERE, isPostgres ? "migrations-pg" : "migrations"); +const DIR = MIGRATIONS_DIR; export async function migrate() { // Bootstrap the tracking table (the first migration also declares it IF NOT EXISTS). @@ -16,7 +24,7 @@ export async function migrate() { for (const file of files) { if (done.has(file)) { console.log(`· ${file} (already applied)`); continue; } const sql = fs.readFileSync(path.join(DIR, file), "utf8"); - // libSQL executes one statement per call — split on semicolons at line ends. + // One statement per call (libSQL requires it; the Postgres shim binds per statement) — split on semicolons at line ends. const statements = sql.split(/;\s*(?:\n|$)/).map((s) => s.trim()).filter(Boolean); for (const stmt of statements) await run(stmt); await run(`INSERT INTO _migrations (name, applied_at) VALUES (?, ?)`, [file, Date.now()]); diff --git a/apps/pwa/src/migrations-pg/001_auth.sql b/apps/pwa/src/migrations-pg/001_auth.sql new file mode 100644 index 0000000..0a95615 --- /dev/null +++ b/apps/pwa/src/migrations-pg/001_auth.sql @@ -0,0 +1,53 @@ +-- Converted from SQLite by @profullstack/libsql-pg. Review every TODO before applying. +-- Types: INTEGER -> bigint, REAL -> double precision, BLOB -> bytea, BOOLEAN -> boolean, +-- DATETIME/TIMESTAMP -> timestamptz, TEXT -> text; INTEGER PRIMARY KEY -> identity. + +create table if not exists users ( + id text PRIMARY KEY, + email text UNIQUE, + password_hash text, + coinpay_sub text UNIQUE, + display_name text, + created_at bigint NOT NULL +); + +create table if not exists webauthn_credentials ( + id text PRIMARY KEY, + user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE, + public_key text NOT NULL, + counter bigint NOT NULL DEFAULT 0, + transports text, + created_at bigint NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_webauthn_user ON webauthn_credentials(user_id); + +create table if not exists sessions ( + token text PRIMARY KEY, + user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at bigint NOT NULL, + expires_at bigint NOT NULL +); + +create table if not exists api_keys ( + id text PRIMARY KEY, + user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name text, + token_hash text NOT NULL, + prefix text NOT NULL, + created_at bigint NOT NULL, + last_used_at bigint +); + +CREATE INDEX IF NOT EXISTS idx_apikeys_user ON api_keys(user_id); + +create table if not exists cli_auth_codes ( + code text PRIMARY KEY, + user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE, + code_challenge text NOT NULL, + redirect_uri text NOT NULL, + name text, + used bigint NOT NULL DEFAULT 0, + created_at bigint NOT NULL, + expires_at bigint NOT NULL +); diff --git a/apps/pwa/src/migrations-pg/002_credshare.sql b/apps/pwa/src/migrations-pg/002_credshare.sql new file mode 100644 index 0000000..110fd90 --- /dev/null +++ b/apps/pwa/src/migrations-pg/002_credshare.sql @@ -0,0 +1,89 @@ +-- Converted from SQLite by @profullstack/libsql-pg. Review every TODO before applying. +-- Types: INTEGER -> bigint, REAL -> double precision, BLOB -> bytea, BOOLEAN -> boolean, +-- DATETIME/TIMESTAMP -> timestamptz, TEXT -> text; INTEGER PRIMARY KEY -> identity. + +create table if not exists credshare_keys ( + user_id text PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + public_key text NOT NULL, + updated_at bigint NOT NULL +); + +create table if not exists credshare_teams ( + id text PRIMARY KEY, + slug text NOT NULL UNIQUE, + name text NOT NULL, + created_by text NOT NULL REFERENCES users(id), + created_at bigint NOT NULL +); + +create table if not exists credshare_members ( + id text PRIMARY KEY, + team_id text NOT NULL REFERENCES credshare_teams(id) ON DELETE CASCADE, + user_id text REFERENCES users(id) ON DELETE SET NULL, + email text NOT NULL, + role text NOT NULL DEFAULT 'member', + status text NOT NULL DEFAULT 'invited', + invited_by text REFERENCES users(id), + joined_at bigint, + created_at bigint NOT NULL, + UNIQUE(team_id, email) +); + +CREATE INDEX IF NOT EXISTS idx_credshare_members_team ON credshare_members(team_id); + +CREATE INDEX IF NOT EXISTS idx_credshare_members_user ON credshare_members(user_id); + +create table if not exists credshare_invites ( + id text PRIMARY KEY, + team_id text NOT NULL REFERENCES credshare_teams(id) ON DELETE CASCADE, + email text NOT NULL, + role text NOT NULL DEFAULT 'member', + token_hash text NOT NULL UNIQUE, + created_by text NOT NULL REFERENCES users(id), + expires_at bigint NOT NULL, + accepted_at bigint, + created_at bigint NOT NULL +); + +create table if not exists credshare_vaults ( + id text PRIMARY KEY, + team_id text NOT NULL REFERENCES credshare_teams(id) ON DELETE CASCADE, + name text NOT NULL, + created_by text NOT NULL REFERENCES users(id), + created_at bigint NOT NULL, + UNIQUE(team_id, name) +); + +create table if not exists credshare_vault_grants ( + vault_id text NOT NULL REFERENCES credshare_vaults(id) ON DELETE CASCADE, + user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE, + wrapped_dek text NOT NULL, + granted_by text NOT NULL REFERENCES users(id), + created_at bigint NOT NULL, + PRIMARY KEY (vault_id, user_id) +); + +create table if not exists credshare_secrets ( + vault_id text NOT NULL REFERENCES credshare_vaults(id) ON DELETE CASCADE, + name text NOT NULL, + nonce text NOT NULL, + ciphertext text NOT NULL, + fingerprint text NOT NULL, + version bigint NOT NULL, + updated_by text NOT NULL REFERENCES users(id), + updated_at bigint NOT NULL, + PRIMARY KEY (vault_id, name) +); + +create table if not exists credshare_audit ( + id text PRIMARY KEY, + team_id text, + vault_id text, + actor_user_id text NOT NULL REFERENCES users(id), + action text NOT NULL, + key_name text, + fingerprint text, + created_at bigint NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_credshare_audit_vault ON credshare_audit(vault_id, created_at DESC); diff --git a/apps/pwa/src/migrations-pg/003_cli_device_codes.sql b/apps/pwa/src/migrations-pg/003_cli_device_codes.sql new file mode 100644 index 0000000..7708e86 --- /dev/null +++ b/apps/pwa/src/migrations-pg/003_cli_device_codes.sql @@ -0,0 +1,16 @@ +-- Converted from SQLite by @profullstack/libsql-pg. Review every TODO before applying. +-- Types: INTEGER -> bigint, REAL -> double precision, BLOB -> bytea, BOOLEAN -> boolean, +-- DATETIME/TIMESTAMP -> timestamptz, TEXT -> text; INTEGER PRIMARY KEY -> identity. + +create table if not exists cli_device_codes ( + device_code_hash text PRIMARY KEY, + user_code text NOT NULL UNIQUE, + user_id text REFERENCES users(id) ON DELETE CASCADE, + name text, + status text NOT NULL DEFAULT 'pending', + created_at bigint NOT NULL, + expires_at bigint NOT NULL, + last_polled_at bigint +); + +CREATE INDEX IF NOT EXISTS idx_cli_device_user_code ON cli_device_codes(user_code); diff --git a/apps/pwa/test/credshare-rekey.test.mjs b/apps/pwa/test/credshare-rekey.test.mjs index 911c121..5f9cefa 100644 --- a/apps/pwa/test/credshare-rekey.test.mjs +++ b/apps/pwa/test/credshare-rekey.test.mjs @@ -7,7 +7,7 @@ // // Runs against an in-memory libSQL database, so DATABASE_URL must be set before // anything imports db.mjs. -process.env.DATABASE_URL = ":memory:"; +process.env.DATABASE_URL = process.env.PWA_TEST_DATABASE_URL || ":memory:"; import test from "node:test"; import assert from "node:assert/strict"; @@ -17,13 +17,19 @@ import { dirname, join } from "node:path"; import express from "express"; const here = dirname(fileURLToPath(import.meta.url)); -const { db, run, get, all } = await import("../src/db.mjs"); +const { db, run, get, all, isPostgres } = await import("../src/db.mjs"); const { credshareRouter } = await import("../src/routes/credshare.mjs"); /** Apply the schema this router depends on. */ async function migrate() { + // Against a real Postgres (PWA_TEST_DATABASE_URL) start from an empty schema; + // run with --test-concurrency=1 then, the files share one database. + if (isPostgres) { + await db.execute("DROP SCHEMA public CASCADE"); + await db.execute("CREATE SCHEMA public"); + } for (const file of ["001_auth.sql", "002_credshare.sql"]) { - const sql = readFileSync(join(here, "..", "src", "migrations", file), "utf8"); + const sql = readFileSync(join(here, "..", "src", isPostgres ? "migrations-pg" : "migrations", file), "utf8"); for (const statement of sql.split(/;\s*$/m).map((s) => s.trim()).filter(Boolean)) { await db.execute(statement); } diff --git a/apps/pwa/test/credshare-vaults.test.mjs b/apps/pwa/test/credshare-vaults.test.mjs index 1d10f04..acc69fc 100644 --- a/apps/pwa/test/credshare-vaults.test.mjs +++ b/apps/pwa/test/credshare-vaults.test.mjs @@ -13,7 +13,7 @@ // // Runs against an in-memory libSQL database, so DATABASE_URL must be set before // anything imports db.mjs. -process.env.DATABASE_URL = ":memory:"; +process.env.DATABASE_URL = process.env.PWA_TEST_DATABASE_URL || ":memory:"; import test from "node:test"; import assert from "node:assert/strict"; @@ -23,12 +23,18 @@ import { dirname, join } from "node:path"; import express from "express"; const here = dirname(fileURLToPath(import.meta.url)); -const { db, run } = await import("../src/db.mjs"); +const { db, run, isPostgres } = await import("../src/db.mjs"); const { credshareRouter } = await import("../src/routes/credshare.mjs"); async function migrate() { + // Against a real Postgres (PWA_TEST_DATABASE_URL) start from an empty schema; + // run with --test-concurrency=1 then, the files share one database. + if (isPostgres) { + await db.execute("DROP SCHEMA public CASCADE"); + await db.execute("CREATE SCHEMA public"); + } for (const file of ["001_auth.sql", "002_credshare.sql"]) { - const sql = readFileSync(join(here, "..", "src", "migrations", file), "utf8"); + const sql = readFileSync(join(here, "..", "src", isPostgres ? "migrations-pg" : "migrations", file), "utf8"); for (const statement of sql.split(/;\s*$/m).map((s) => s.trim()).filter(Boolean)) { await db.execute(statement); } diff --git a/apps/pwa/test/team-members.test.mjs b/apps/pwa/test/team-members.test.mjs index 6d5fe73..60905f2 100644 --- a/apps/pwa/test/team-members.test.mjs +++ b/apps/pwa/test/team-members.test.mjs @@ -1,6 +1,6 @@ // Integration coverage for team-member CRUD, invite-key rotation, and the // dashboard controls that expose those operations. -process.env.DATABASE_URL = ":memory:"; +process.env.DATABASE_URL = process.env.PWA_TEST_DATABASE_URL || ":memory:"; import test from "node:test"; import assert from "node:assert/strict"; @@ -10,13 +10,19 @@ import { dirname, join } from "node:path"; import express from "express"; const here = dirname(fileURLToPath(import.meta.url)); -const { db, run, get } = await import("../src/db.mjs"); +const { db, run, get, isPostgres } = await import("../src/db.mjs"); const { sha256 } = await import("../src/lib/crypto.mjs"); const { credshareRouter } = await import("../src/routes/credshare.mjs"); const { pagesRouter } = await import("../src/routes/pages.mjs"); +// Against a real Postgres (PWA_TEST_DATABASE_URL) start from an empty schema; +// run with --test-concurrency=1 then, the files share one database. +if (isPostgres) { + await db.execute("DROP SCHEMA public CASCADE"); + await db.execute("CREATE SCHEMA public"); +} for (const file of ["001_auth.sql", "002_credshare.sql"]) { - const sql = readFileSync(join(here, "..", "src", "migrations", file), "utf8"); + const sql = readFileSync(join(here, "..", "src", isPostgres ? "migrations-pg" : "migrations", file), "utf8"); for (const statement of sql.split(/;\s*$/m).map((s) => s.trim()).filter(Boolean)) await db.execute(statement); } diff --git a/package-lock.json b/package-lock.json index cf45519..f225118 100644 --- a/package-lock.json +++ b/package-lock.json @@ -164,12 +164,15 @@ "name": "@logicsrc/pwa", "version": "0.1.1", "dependencies": { - "@libsql/client": "^0.14.0", + "@profullstack/libsql-pg": "^0.1.2", "@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/server": "^13.1.0", "cookie-parser": "^1.4.7", "express": "^4.21.2" }, + "devDependencies": { + "@libsql/client": "^0.14.0" + }, "engines": { "node": ">=20" } @@ -178,6 +181,7 @@ "version": "0.14.0", "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.14.0.tgz", "integrity": "sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==", + "dev": true, "license": "MIT", "dependencies": { "@libsql/core": "^0.14.0", @@ -191,6 +195,7 @@ "version": "0.14.0", "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.14.0.tgz", "integrity": "sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==", + "dev": true, "license": "MIT", "dependencies": { "js-base64": "^3.7.5" @@ -203,6 +208,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -216,6 +222,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -226,6 +233,7 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.7.0.tgz", "integrity": "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==", + "dev": true, "license": "MIT", "dependencies": { "@libsql/isomorphic-fetch": "^0.3.1", @@ -241,6 +249,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -254,6 +263,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -267,6 +277,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -280,6 +291,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -293,6 +305,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -373,6 +386,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -472,6 +486,7 @@ "arm64", "wasm32" ], + "dev": true, "license": "MIT", "os": [ "darwin", @@ -2532,6 +2547,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.3.1.tgz", "integrity": "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -3270,6 +3286,70 @@ "node": ">=20.11" } }, + "node_modules/@profullstack/libsql-pg": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@profullstack/libsql-pg/-/libsql-pg-0.1.2.tgz", + "integrity": "sha512-6YrQujssWhkyWInB0G4CWOhH6Pj6kydeK4RKrXOosxQ12flpO4VjUEGTabtSh9zZ7FzCRch0hwFNKApJl/7b4w==", + "license": "MIT", + "dependencies": { + "@profullstack/libsql-pg": "^0.1.1", + "pg": "^8.13.0" + }, + "bin": { + "libsql-pg": "bin/libsql-pg.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@libsql/client": "^0.15.0" + }, + "peerDependencies": { + "@libsql/client": ">=0.5.0" + }, + "peerDependenciesMeta": { + "@libsql/client": { + "optional": true + } + } + }, + "node_modules/@profullstack/libsql-pg/node_modules/@libsql/client": { + "version": "0.15.15", + "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.15.15.tgz", + "integrity": "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@libsql/core": "^0.15.14", + "@libsql/hrana-client": "^0.7.0", + "js-base64": "^3.7.5", + "libsql": "^0.5.22", + "promise-limit": "^2.7.0" + } + }, + "node_modules/@profullstack/libsql-pg/node_modules/@libsql/core": { + "version": "0.15.15", + "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.15.15.tgz", + "integrity": "sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA==", + "license": "MIT", + "optional": true, + "dependencies": { + "js-base64": "^3.7.5" + } + }, + "node_modules/@profullstack/libsql-pg/node_modules/@libsql/hrana-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.7.0.tgz", + "integrity": "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@libsql/isomorphic-fetch": "^0.3.1", + "@libsql/isomorphic-ws": "^0.1.5", + "js-base64": "^3.7.5", + "node-fetch": "^3.3.2" + } + }, "node_modules/@profullstack/logicsrc-mcp": { "resolved": "packages/logicsrc-mcp", "link": true @@ -4859,6 +4939,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 12" @@ -5424,6 +5505,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "devOptional": true, "funding": [ { "type": "github", @@ -5480,6 +5562,7 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "devOptional": true, "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -6602,6 +6685,7 @@ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "deprecated": "Use your platform's native DOMException instead", + "devOptional": true, "funding": [ { "type": "github", @@ -6621,6 +6705,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "devOptional": true, "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -6894,6 +6979,95 @@ "url": "https://github.com/sponsors/KillyMXI" } }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7032,6 +7206,45 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -8736,6 +8949,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 8" @@ -8855,6 +9069,15 @@ "node": ">=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -8963,7 +9186,7 @@ }, "packages/cli": { "name": "@logicsrc/cli", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@fission-ai/openspec": "^1.13.0", "@logicsrc/account-core": "file:../account-core",