mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-01 12:23:50 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
c535aaa32c
commit
4ac750e65b
12 changed files with 507 additions and 29 deletions
|
|
@ -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=<origin> logicsrc login` (browser OAuth-PKCE
|
||||
loopback → an `lsk_` key). See `docs/credential-sharing.md` in the repo root.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 || "",
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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()]);
|
||||
|
|
|
|||
53
apps/pwa/src/migrations-pg/001_auth.sql
Normal file
53
apps/pwa/src/migrations-pg/001_auth.sql
Normal file
|
|
@ -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
|
||||
);
|
||||
89
apps/pwa/src/migrations-pg/002_credshare.sql
Normal file
89
apps/pwa/src/migrations-pg/002_credshare.sql
Normal file
|
|
@ -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);
|
||||
16
apps/pwa/src/migrations-pg/003_cli_device_codes.sql
Normal file
16
apps/pwa/src/migrations-pg/003_cli_device_codes.sql
Normal file
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
227
package-lock.json
generated
227
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue