feat(storage): add resilient SQLite foundation

This commit is contained in:
chick
2026-07-16 13:20:59 +08:00
parent 26c1bcc6da
commit ef9a634a02
9 changed files with 1282 additions and 1 deletions
+4 -1
View File
@@ -5,13 +5,16 @@
"type": "module", "type": "module",
"exports": "./src/index.ts", "exports": "./src/index.ts",
"scripts": { "scripts": {
"test": "vitest run --root ../.. apps/api/src/app.test.ts apps/api/src/start.test.ts", "test": "vitest run --root ../.. apps/api/src/app.test.ts apps/api/src/start.test.ts apps/api/src/infrastructure/database/database.test.ts",
"typecheck": "tsc -p tsconfig.json" "typecheck": "tsc -p tsconfig.json"
}, },
"dependencies": { "dependencies": {
"better-sqlite3": "12.11.1",
"drizzle-orm": "0.45.2",
"fastify": "5.10.0" "fastify": "5.10.0"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "7.6.13",
"@types/node": "24.13.3" "@types/node": "24.13.3"
} }
} }
+4
View File
@@ -8,3 +8,7 @@ export {
export type { BuildAppOptions, ListenEnvironment, ListenOptions, ReadinessResult } from './app.js'; export type { BuildAppOptions, ListenEnvironment, ListenOptions, ReadinessResult } from './app.js';
export { startApi } from './start.js'; export { startApi } from './start.js';
export type { StartApiOptions } from './start.js'; export type { StartApiOptions } from './start.js';
export { backupDatabase, restoreDatabase } from './infrastructure/database/backup.js';
export { openDatabase } from './infrastructure/database/database.js';
export { MIGRATIONS, migrateDatabase } from './infrastructure/database/migrations.js';
export * as databaseSchema from './infrastructure/database/schema.js';
@@ -0,0 +1,116 @@
import Database from 'better-sqlite3';
import { chmod, copyFile, lstat, mkdir, open, rename, rm, stat } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import { dirname, resolve } from 'node:path';
import { beginRestore, getManagedDatabaseIdentity, type SqliteDatabase } from './database.js';
async function assertRegularFile(path: string, label: string): Promise<void> {
const details = await lstat(path);
if (details.isSymbolicLink()) throw new Error(`${label} must not be a symbolic link`);
if (!details.isFile()) throw new Error(`${label} must be a regular file`);
}
async function assertDistinctFiles(databasePath: string, backupPath: string): Promise<void> {
if (resolve(databasePath) === resolve(backupPath))
throw new Error('Live and backup paths are the same');
await assertRegularFile(backupPath, 'Backup');
try {
await assertRegularFile(databasePath, 'Live database');
const [live, backup] = await Promise.all([stat(databasePath), stat(backupPath)]);
if (live.dev === backup.dev && live.ino === backup.ino)
throw new Error('Live and backup paths are aliases of the same file');
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
}
async function reservePrivateFile(path: string): Promise<void> {
const handle = await open(path, 'wx', 0o600);
await handle.close();
}
async function assertValidDatabase(path: string): Promise<void> {
let database: Database.Database | undefined;
try {
database = new Database(path, { fileMustExist: true, readonly: true });
const integrity = database.pragma('integrity_check', { simple: true });
if (integrity !== 'ok') throw new Error(`Backup integrity check failed: ${String(integrity)}`);
} catch (error) {
throw new Error('Backup is not a valid SQLite database', { cause: error });
} finally {
database?.close();
}
}
async function syncFile(path: string): Promise<void> {
const handle = await open(path, 'r');
try {
await handle.sync();
} finally {
await handle.close();
}
}
async function syncDirectory(path: string): Promise<void> {
const handle = await open(path, 'r');
try {
await handle.sync();
} finally {
await handle.close();
}
}
export async function backupDatabase(database: SqliteDatabase, backupPath: string): Promise<void> {
const source = getManagedDatabaseIdentity(database);
const directory = dirname(backupPath);
await mkdir(directory, { recursive: true });
try {
const destination = await stat(backupPath);
if (`${destination.dev}:${destination.ino}` === source.deviceInode) {
throw new Error('Backup destination aliases the live database identity');
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
const temporaryPath = `${backupPath}.${randomUUID()}.tmp`;
try {
await reservePrivateFile(temporaryPath);
await database.backup(temporaryPath);
await chmod(temporaryPath, 0o600);
await assertValidDatabase(temporaryPath);
await syncFile(temporaryPath);
await rename(temporaryPath, backupPath);
await chmod(backupPath, 0o600);
await syncDirectory(directory);
} finally {
await rm(temporaryPath, { force: true });
}
}
export async function restoreDatabase(databasePath: string, backupPath: string): Promise<void> {
const endRestore = beginRestore(databasePath);
const directory = dirname(databasePath);
const temporaryPath = `${databasePath}.${randomUUID()}.restore`;
try {
await assertDistinctFiles(databasePath, backupPath);
await assertValidDatabase(backupPath);
await mkdir(directory, { recursive: true });
await reservePrivateFile(temporaryPath);
await copyFile(backupPath, temporaryPath);
await chmod(temporaryPath, 0o600);
await assertValidDatabase(temporaryPath);
await syncFile(temporaryPath);
// Atomic replacement comes first: any failure before this point leaves live and sidecars intact.
await rename(temporaryPath, databasePath);
await chmod(databasePath, 0o600);
await syncDirectory(directory);
// No managed connection can exist under the exclusive gate; stale sidecars are now safe to remove.
await rm(`${databasePath}-wal`, { force: true });
await rm(`${databasePath}-shm`, { force: true });
await syncDirectory(directory);
} finally {
await rm(temporaryPath, { force: true });
endRestore();
}
}
@@ -0,0 +1,291 @@
import { link, lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { backupDatabase, restoreDatabase } from './backup.js';
import { openDatabase } from './database.js';
import { MIGRATIONS, migrateDatabase } from './migrations.js';
const directories: string[] = [];
async function temporaryDirectory(): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), 'multi-simadmin-database-'));
directories.push(directory);
return directory;
}
afterEach(async () => {
await Promise.all(
directories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })),
);
});
describe('database migrations', () => {
it('rejects duplicate, unordered, backfilled, and drifted migration catalogs', async () => {
const directory = await temporaryDirectory();
const database = openDatabase(join(directory, 'app.sqlite'));
const one = { id: 1, name: 'one', statements: ['CREATE TABLE one (id TEXT)'] };
migrateDatabase(database, [one]);
expect(() =>
migrateDatabase(database, [{ ...one, statements: ['CREATE TABLE changed (id TEXT)'] }]),
).toThrow(/checksum|drift/i);
expect(() => migrateDatabase(database, [one, { ...one, name: 'two' }])).toThrow(
/duplicate.*id/i,
);
expect(() => migrateDatabase(database, [one, { id: 2, name: 'one', statements: [] }])).toThrow(
/duplicate.*name/i,
);
expect(() => migrateDatabase(database, [{ id: 2, name: 'two', statements: [] }, one])).toThrow(
/increasing/i,
);
migrateDatabase(database, [{ id: 2, name: 'two', statements: [] }]);
expect(() => migrateDatabase(database, [{ id: 0, name: 'backfill', statements: [] }])).toThrow(
/backfill/i,
);
database.close();
});
it('creates all business tables, migration metadata, indexes, foreign keys, and UTC timestamp fields', async () => {
const directory = await temporaryDirectory();
const database = openDatabase(join(directory, 'app.sqlite'));
migrateDatabase(database);
const tables = database
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
)
.all()
.map((row) => (row as { name: string }).name);
expect(tables).toEqual([
'app_settings',
'audit_events',
'capabilities',
'instance_tags',
'instances',
'job_items',
'jobs',
'schema_migrations',
'secret_references',
'status_snapshots',
]);
expect(database.pragma('foreign_keys', { simple: true })).toBe(1);
const instanceColumns = database.pragma('table_info(instances)') as Array<{
name: string;
notnull: number;
}>;
expect(instanceColumns).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'config_revision', notnull: 1 }),
expect.objectContaining({ name: 'created_at', notnull: 1 }),
expect.objectContaining({ name: 'updated_at', notnull: 1 }),
]),
);
const foreignKeys = database.pragma('foreign_key_list(job_items)') as Array<{
from: string;
on_delete: string;
table: string;
}>;
expect(foreignKeys).toEqual(
expect.arrayContaining([
expect.objectContaining({ from: 'job_id', on_delete: 'CASCADE', table: 'jobs' }),
expect.objectContaining({ from: 'instance_id', on_delete: 'RESTRICT', table: 'instances' }),
]),
);
const indexes = database
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE 'idx_%' ORDER BY name",
)
.all()
.map((row) => (row as { name: string }).name);
expect(indexes).toEqual(
expect.arrayContaining([
'idx_audit_events_created_at',
'idx_jobs_status_created_at',
'idx_status_snapshots_instance_observed_at',
]),
);
database.close();
});
it('replays without resetting persisted data', async () => {
const directory = await temporaryDirectory();
const database = openDatabase(join(directory, 'app.sqlite'));
migrateDatabase(database);
database
.prepare(
'INSERT INTO instances (id, name, base_url, config_revision, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
)
.run(
'one',
'One',
'http://example.invalid',
7,
'2026-01-01T00:00:00.000Z',
'2026-01-01T00:00:00.000Z',
);
migrateDatabase(database);
expect(
database.prepare('SELECT name, config_revision FROM instances WHERE id = ?').get('one'),
).toEqual({
config_revision: 7,
name: 'One',
});
expect(database.prepare('SELECT COUNT(*) AS count FROM schema_migrations').get()).toEqual({
count: MIGRATIONS.length,
});
database.close();
});
it('rolls back the entire migration when a statement fails', async () => {
const directory = await temporaryDirectory();
const database = openDatabase(join(directory, 'app.sqlite'));
expect(() =>
migrateDatabase(database, [
{
id: 999,
name: 'deliberately-broken',
statements: ['CREATE TABLE should_rollback (id TEXT PRIMARY KEY)', 'THIS IS NOT SQL'],
},
]),
).toThrow();
expect(
database.prepare("SELECT name FROM sqlite_master WHERE name = 'should_rollback'").get(),
).toBeUndefined();
expect(
database.prepare("SELECT name FROM sqlite_master WHERE name = 'schema_migrations'").get(),
).toBeUndefined();
database.close();
});
});
describe('database backup and restore', () => {
it('refuses restore while a managed live connection is open', async () => {
const directory = await temporaryDirectory();
const databasePath = join(directory, 'app.sqlite');
const backupPath = join(directory, 'backup.sqlite');
const database = openDatabase(databasePath);
migrateDatabase(database);
await backupDatabase(database, backupPath);
await expect(restoreDatabase(databasePath, backupPath)).rejects.toThrow(/open|connection/i);
database.close();
});
it('refuses restore through hardlink and symlinked-parent aliases while a managed connection is open', async () => {
const directory = await temporaryDirectory();
const realDirectory = join(directory, 'real');
const aliasDirectory = join(directory, 'alias');
await mkdir(realDirectory);
await symlink(realDirectory, aliasDirectory);
const databasePath = join(realDirectory, 'app.sqlite');
const hardlinkPath = join(directory, 'hardlink.sqlite');
const backupPath = join(directory, 'backup.sqlite');
const database = openDatabase(databasePath);
migrateDatabase(database);
await backupDatabase(database, backupPath);
await link(databasePath, hardlinkPath);
await expect(restoreDatabase(hardlinkPath, backupPath)).rejects.toThrow(
/open|connection|identity/i,
);
await expect(restoreDatabase(join(aliasDirectory, 'app.sqlite'), backupPath)).rejects.toThrow(
/open|connection|identity/i,
);
database.close();
});
it('rejects backup destinations that alias the managed live database', async () => {
const directory = await temporaryDirectory();
const databasePath = join(directory, 'app.sqlite');
const hardlinkPath = join(directory, 'hardlink.sqlite');
const database = openDatabase(databasePath);
migrateDatabase(database);
await link(databasePath, hardlinkPath);
await expect(backupDatabase(database, hardlinkPath)).rejects.toThrow(/same|alias|identity/i);
database.close();
});
it('creates backups with owner-only permissions', async () => {
const directory = await temporaryDirectory();
const database = openDatabase(join(directory, 'app.sqlite'));
migrateDatabase(database);
const backupPath = join(directory, 'backup.sqlite');
await backupDatabase(database, backupPath);
expect((await lstat(backupPath)).mode & 0o777).toBe(0o600);
database.close();
});
it('rejects same-path, symlink, and hardlink restore aliases', async () => {
const directory = await temporaryDirectory();
const databasePath = join(directory, 'app.sqlite');
const backupPath = join(directory, 'backup.sqlite');
const database = openDatabase(databasePath);
migrateDatabase(database);
await backupDatabase(database, backupPath);
database.close();
await expect(restoreDatabase(databasePath, databasePath)).rejects.toThrow(/same|alias/i);
const symlinkPath = join(directory, 'backup-link.sqlite');
await symlink(backupPath, symlinkPath);
await expect(restoreDatabase(databasePath, symlinkPath)).rejects.toThrow(/symbolic|symlink/i);
const hardlinkPath = join(directory, 'live-hardlink.sqlite');
await link(databasePath, hardlinkPath);
await expect(restoreDatabase(databasePath, hardlinkPath)).rejects.toThrow(/same|alias/i);
});
it('backs up an active WAL database, then atomically restores its earlier state', async () => {
const directory = await temporaryDirectory();
const databasePath = join(directory, 'app.sqlite');
const backupPath = join(directory, 'backup.sqlite');
const database = openDatabase(databasePath);
migrateDatabase(database);
database
.prepare(
'INSERT INTO app_settings (key, value_json, created_at, updated_at) VALUES (?, ?, ?, ?)',
)
.run('colour', '"blue"', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z');
await backupDatabase(database, backupPath);
database.prepare('UPDATE app_settings SET value_json = ? WHERE key = ?').run('"red"', 'colour');
database.close();
await restoreDatabase(databasePath, backupPath);
const restored = openDatabase(databasePath);
expect(
restored.prepare('SELECT value_json FROM app_settings WHERE key = ?').get('colour'),
).toEqual({ value_json: '"blue"' });
expect(restored.pragma('integrity_check', { simple: true })).toBe('ok');
restored.close();
});
it('rejects a corrupt backup without changing the live database', async () => {
const directory = await temporaryDirectory();
const databasePath = join(directory, 'app.sqlite');
const corruptPath = join(directory, 'corrupt.sqlite');
const database = openDatabase(databasePath);
migrateDatabase(database);
database
.prepare(
'INSERT INTO app_settings (key, value_json, created_at, updated_at) VALUES (?, ?, ?, ?)',
)
.run('safe', 'true', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z');
database.close();
const before = await readFile(databasePath);
await writeFile(corruptPath, 'not a sqlite database');
await expect(restoreDatabase(databasePath, corruptPath)).rejects.toThrow(
/backup|integrity|database/i,
);
expect(await readFile(databasePath)).toEqual(before);
const unchanged = openDatabase(databasePath);
expect(
unchanged.prepare('SELECT value_json FROM app_settings WHERE key = ?').get('safe'),
).toEqual({ value_json: 'true' });
unchanged.close();
});
});
@@ -0,0 +1,115 @@
import Database from 'better-sqlite3';
import { mkdirSync, realpathSync, statSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
export type SqliteDatabase = Database.Database;
export interface DatabaseIdentity {
readonly canonicalPath: string;
readonly deviceInode: string;
}
const openPaths = new Map<string, number>();
const openIdentities = new Map<string, number>();
const restorePaths = new Set<string>();
const restoreIdentities = new Set<string>();
const managedIdentities = new WeakMap<SqliteDatabase, DatabaseIdentity>();
function increment(map: Map<string, number>, key: string): void {
map.set(key, (map.get(key) ?? 0) + 1);
}
function decrement(map: Map<string, number>, key: string): void {
const remaining = (map.get(key) ?? 1) - 1;
if (remaining === 0) map.delete(key);
else map.set(key, remaining);
}
function canonicalPath(path: string): string {
const absolute = resolve(path);
return join(realpathSync(dirname(absolute)), basename(absolute));
}
function identityForExistingPath(path: string): DatabaseIdentity {
const canonical = realpathSync(path);
const details = statSync(canonical);
return {
canonicalPath: canonical,
deviceInode: `${details.dev}:${details.ino}`,
};
}
export function getManagedDatabaseIdentity(database: SqliteDatabase): DatabaseIdentity {
const identity = managedIdentities.get(database);
if (!identity) throw new Error('Database connection is not managed by openDatabase');
return identity;
}
export function hasOpenDatabase(path: string): boolean {
const canonical = canonicalPath(path);
if ((openPaths.get(canonical) ?? 0) > 0) return true;
try {
return (openIdentities.get(identityForExistingPath(path).deviceInode) ?? 0) > 0;
} catch {
return false;
}
}
export function beginRestore(path: string): () => void {
const identity = identityForExistingPath(path);
if (restorePaths.has(identity.canonicalPath) || restoreIdentities.has(identity.deviceInode)) {
throw new Error('A restore is already in progress for this database identity');
}
if (
(openPaths.get(identity.canonicalPath) ?? 0) > 0 ||
(openIdentities.get(identity.deviceInode) ?? 0) > 0
) {
throw new Error('Cannot restore database while a managed connection is open for this identity');
}
restorePaths.add(identity.canonicalPath);
restoreIdentities.add(identity.deviceInode);
return () => {
restorePaths.delete(identity.canonicalPath);
restoreIdentities.delete(identity.deviceInode);
};
}
export function openDatabase(path: string): SqliteDatabase {
const absolute = resolve(path);
mkdirSync(dirname(absolute), { recursive: true });
const preOpenPath = canonicalPath(absolute);
if (restorePaths.has(preOpenPath))
throw new Error('Cannot open database while restore is in progress');
const database = new Database(absolute);
const identity = identityForExistingPath(absolute);
if (restorePaths.has(identity.canonicalPath) || restoreIdentities.has(identity.deviceInode)) {
database.close();
throw new Error('Cannot open database while restore is in progress for this identity');
}
increment(openPaths, identity.canonicalPath);
increment(openIdentities, identity.deviceInode);
managedIdentities.set(database, identity);
const close = database.close.bind(database);
let closed = false;
database.close = () => {
if (!closed) {
closed = true;
decrement(openPaths, identity.canonicalPath);
decrement(openIdentities, identity.deviceInode);
managedIdentities.delete(database);
}
close();
return database;
};
try {
database.pragma('journal_mode = WAL');
database.pragma('foreign_keys = ON');
database.pragma('busy_timeout = 5000');
return database;
} catch (error) {
database.close();
throw error;
}
}
@@ -0,0 +1,188 @@
import type { SqliteDatabase } from './database.js';
import { createHash } from 'node:crypto';
export interface Migration {
readonly id: number;
readonly name: string;
readonly statements: readonly string[];
}
export const MIGRATIONS: readonly Migration[] = [
{
id: 1,
name: 'initial-control-plane-schema',
statements: [
`CREATE TABLE instances (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
base_url TEXT NOT NULL UNIQUE,
auth_mode TEXT NOT NULL DEFAULT 'password',
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
config_revision INTEGER NOT NULL DEFAULT 1 CHECK (config_revision > 0),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`,
`CREATE TABLE instance_tags (
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (instance_id, tag)
)`,
`CREATE TABLE capabilities (
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
operation_id TEXT NOT NULL,
state TEXT NOT NULL CHECK (state IN ('supported', 'unsupported', 'auth-required', 'degraded', 'unknown')),
upstream_version TEXT,
detail_code TEXT,
observed_at TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (instance_id, operation_id)
)`,
`CREATE TABLE status_snapshots (
id TEXT PRIMARY KEY,
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
category TEXT NOT NULL,
state TEXT NOT NULL CHECK (state IN ('fresh', 'stale', 'expired', 'unknown')),
payload_json TEXT NOT NULL,
observed_at TEXT NOT NULL,
expires_at TEXT,
created_at TEXT NOT NULL
)`,
`CREATE TABLE jobs (
id TEXT PRIMARY KEY,
parent_job_id TEXT REFERENCES jobs(id) ON DELETE RESTRICT,
operation_id TEXT NOT NULL,
risk_level TEXT NOT NULL CHECK (risk_level IN ('R0', 'R1', 'R2', 'R3')),
status TEXT NOT NULL,
requested_by TEXT NOT NULL,
request_id TEXT NOT NULL,
parameters_digest TEXT NOT NULL,
created_at TEXT NOT NULL,
started_at TEXT,
finished_at TEXT,
updated_at TEXT NOT NULL
)`,
`CREATE TABLE job_items (
id TEXT PRIMARY KEY,
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE RESTRICT,
attempt_number INTEGER NOT NULL DEFAULT 1 CHECK (attempt_number > 0),
status TEXT NOT NULL,
result_code TEXT,
created_at TEXT NOT NULL,
started_at TEXT,
finished_at TEXT,
updated_at TEXT NOT NULL,
UNIQUE (job_id, instance_id, attempt_number)
)`,
`CREATE TABLE audit_events (
id TEXT PRIMARY KEY,
instance_id TEXT REFERENCES instances(id) ON DELETE SET NULL,
job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
actor TEXT NOT NULL,
operation_id TEXT NOT NULL,
risk_level TEXT NOT NULL CHECK (risk_level IN ('R0', 'R1', 'R2', 'R3')),
request_id TEXT NOT NULL,
parameters_summary_json TEXT NOT NULL,
body_digest TEXT,
result_code TEXT NOT NULL,
duration_ms INTEGER NOT NULL CHECK (duration_ms >= 0),
created_at TEXT NOT NULL
)`,
`CREATE TABLE app_settings (
key TEXT PRIMARY KEY,
value_json TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`,
`CREATE TABLE secret_references (
id TEXT PRIMARY KEY,
instance_id TEXT REFERENCES instances(id) ON DELETE CASCADE,
purpose TEXT NOT NULL,
provider TEXT NOT NULL,
external_reference TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE (provider, external_reference)
)`,
'CREATE INDEX idx_status_snapshots_instance_observed_at ON status_snapshots(instance_id, observed_at DESC)',
'CREATE INDEX idx_jobs_status_created_at ON jobs(status, created_at DESC)',
'CREATE INDEX idx_audit_events_created_at ON audit_events(created_at DESC)',
'CREATE INDEX idx_capabilities_state ON capabilities(state)',
'CREATE INDEX idx_job_items_job_status ON job_items(job_id, status)',
],
},
];
const createMigrationsTable = `CREATE TABLE schema_migrations (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
checksum TEXT NOT NULL,
applied_at TEXT NOT NULL
)`;
function checksum(migration: Migration): string {
return createHash('sha256').update(JSON.stringify(migration.statements)).digest('hex');
}
function validateCatalog(migrations: readonly Migration[]): void {
const ids = new Set<number>();
const names = new Set<string>();
let previous = -Infinity;
for (const migration of migrations) {
if (!Number.isSafeInteger(migration.id) || migration.id < 0)
throw new Error(`Migration id ${migration.id} is invalid`);
if (ids.has(migration.id)) throw new Error(`Duplicate migration id ${migration.id}`);
if (names.has(migration.name)) throw new Error(`Duplicate migration name ${migration.name}`);
if (migration.id <= previous) throw new Error('Migration ids must be strictly increasing');
ids.add(migration.id);
names.add(migration.name);
previous = migration.id;
}
}
export function migrateDatabase(
database: SqliteDatabase,
migrations: readonly Migration[] = MIGRATIONS,
): void {
validateCatalog(migrations);
database.transaction(() => {
database.exec(
'CREATE TABLE IF NOT EXISTS schema_migrations (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, checksum TEXT NOT NULL, applied_at TEXT NOT NULL)',
);
const maximum = database.prepare('SELECT MAX(id) AS id FROM schema_migrations').get() as {
id: number | null;
};
const applied = database.prepare('SELECT name, checksum FROM schema_migrations WHERE id = ?');
const appliedByName = database.prepare('SELECT id FROM schema_migrations WHERE name = ?');
const record = database.prepare(
'INSERT INTO schema_migrations (id, name, checksum, applied_at) VALUES (?, ?, ?, ?)',
);
for (const migration of migrations) {
const digest = checksum(migration);
const existing = applied.get(migration.id) as { name: string; checksum: string } | undefined;
if (existing) {
if (existing.name !== migration.name)
throw new Error(`Migration ${migration.id} name mismatch`);
if (existing.checksum !== digest)
throw new Error(`Migration ${migration.id} checksum drift detected`);
continue;
}
const existingName = appliedByName.get(migration.name) as { id: number } | undefined;
if (existingName)
throw new Error(
`Migration ${migration.name} drift detected: applied as id ${existingName.id}`,
);
if (maximum.id !== null && migration.id < maximum.id)
throw new Error(
`Migration ${migration.id} is an unapplied backfill below applied id ${maximum.id}`,
);
for (const statement of migration.statements) database.exec(statement);
record.run(migration.id, migration.name, digest, new Date().toISOString());
}
})();
}
// Kept separate so a failed first migration transaction removes metadata too.
export const migrationMetadataSql = createMigrationsTable;
@@ -0,0 +1,192 @@
import { desc, sql } from 'drizzle-orm';
import {
type AnySQLiteColumn,
check,
index,
integer,
primaryKey,
sqliteTable,
text,
unique,
} from 'drizzle-orm/sqlite-core';
const timestamps = {
createdAt: text('created_at').notNull(),
updatedAt: text('updated_at').notNull(),
};
export const instances = sqliteTable(
'instances',
{
id: text('id').primaryKey(),
name: text('name').notNull(),
baseUrl: text('base_url').notNull(),
authMode: text('auth_mode').notNull().default('password'),
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
configRevision: integer('config_revision').notNull().default(1),
...timestamps,
},
(table) => [
unique('instances_base_url_unique').on(table.baseUrl),
check('instances_enabled_check', sql`${table.enabled} in (0, 1)`),
check('instances_config_revision_check', sql`${table.configRevision} > 0`),
],
);
export const instanceTags = sqliteTable(
'instance_tags',
{
instanceId: text('instance_id')
.notNull()
.references(() => instances.id, { onDelete: 'cascade' }),
tag: text('tag').notNull(),
createdAt: text('created_at').notNull(),
},
(table) => [primaryKey({ columns: [table.instanceId, table.tag] })],
);
export const capabilities = sqliteTable(
'capabilities',
{
instanceId: text('instance_id')
.notNull()
.references(() => instances.id, { onDelete: 'cascade' }),
operationId: text('operation_id').notNull(),
state: text('state').notNull(),
upstreamVersion: text('upstream_version'),
detailCode: text('detail_code'),
observedAt: text('observed_at').notNull(),
...timestamps,
},
(table) => [
primaryKey({ columns: [table.instanceId, table.operationId] }),
check(
'capabilities_state_check',
sql`${table.state} in ('supported', 'unsupported', 'auth-required', 'degraded', 'unknown')`,
),
index('idx_capabilities_state').on(table.state),
],
);
export const statusSnapshots = sqliteTable(
'status_snapshots',
{
id: text('id').primaryKey(),
instanceId: text('instance_id')
.notNull()
.references(() => instances.id, { onDelete: 'cascade' }),
category: text('category').notNull(),
state: text('state').notNull(),
payloadJson: text('payload_json').notNull(),
observedAt: text('observed_at').notNull(),
expiresAt: text('expires_at'),
createdAt: text('created_at').notNull(),
},
(table) => [
check(
'status_snapshots_state_check',
sql`${table.state} in ('fresh', 'stale', 'expired', 'unknown')`,
),
index('idx_status_snapshots_instance_observed_at').on(table.instanceId, desc(table.observedAt)),
],
);
export const jobs = sqliteTable(
'jobs',
{
id: text('id').primaryKey(),
parentJobId: text('parent_job_id').references((): AnySQLiteColumn => jobs.id, {
onDelete: 'restrict',
}),
operationId: text('operation_id').notNull(),
riskLevel: text('risk_level').notNull(),
status: text('status').notNull(),
requestedBy: text('requested_by').notNull(),
requestId: text('request_id').notNull(),
parametersDigest: text('parameters_digest').notNull(),
createdAt: text('created_at').notNull(),
startedAt: text('started_at'),
finishedAt: text('finished_at'),
updatedAt: text('updated_at').notNull(),
},
(table) => [
check('jobs_risk_level_check', sql`${table.riskLevel} in ('R0', 'R1', 'R2', 'R3')`),
index('idx_jobs_status_created_at').on(table.status, desc(table.createdAt)),
],
);
export const jobItems = sqliteTable(
'job_items',
{
id: text('id').primaryKey(),
jobId: text('job_id')
.notNull()
.references(() => jobs.id, { onDelete: 'cascade' }),
instanceId: text('instance_id')
.notNull()
.references(() => instances.id, { onDelete: 'restrict' }),
attemptNumber: integer('attempt_number').notNull().default(1),
status: text('status').notNull(),
resultCode: text('result_code'),
createdAt: text('created_at').notNull(),
startedAt: text('started_at'),
finishedAt: text('finished_at'),
updatedAt: text('updated_at').notNull(),
},
(table) => [
unique('job_items_job_instance_attempt_unique').on(
table.jobId,
table.instanceId,
table.attemptNumber,
),
check('job_items_attempt_number_check', sql`${table.attemptNumber} > 0`),
index('idx_job_items_job_status').on(table.jobId, table.status),
],
);
export const auditEvents = sqliteTable(
'audit_events',
{
id: text('id').primaryKey(),
instanceId: text('instance_id').references(() => instances.id, { onDelete: 'set null' }),
jobId: text('job_id').references(() => jobs.id, { onDelete: 'set null' }),
actor: text('actor').notNull(),
operationId: text('operation_id').notNull(),
riskLevel: text('risk_level').notNull(),
requestId: text('request_id').notNull(),
parametersSummaryJson: text('parameters_summary_json').notNull(),
bodyDigest: text('body_digest'),
resultCode: text('result_code').notNull(),
durationMs: integer('duration_ms').notNull(),
createdAt: text('created_at').notNull(),
},
(table) => [
check('audit_events_risk_level_check', sql`${table.riskLevel} in ('R0', 'R1', 'R2', 'R3')`),
check('audit_events_duration_ms_check', sql`${table.durationMs} >= 0`),
index('idx_audit_events_created_at').on(desc(table.createdAt)),
],
);
export const appSettings = sqliteTable('app_settings', {
key: text('key').primaryKey(),
valueJson: text('value_json').notNull(),
...timestamps,
});
export const secretReferences = sqliteTable(
'secret_references',
{
id: text('id').primaryKey(),
instanceId: text('instance_id').references(() => instances.id, { onDelete: 'cascade' }),
purpose: text('purpose').notNull(),
provider: text('provider').notNull(),
externalReference: text('external_reference').notNull(),
...timestamps,
},
(table) => [
unique('secret_references_provider_external_unique').on(
table.provider,
table.externalReference,
),
],
);
+371
View File
@@ -36,10 +36,19 @@ importers:
apps/api: apps/api:
dependencies: dependencies:
better-sqlite3:
specifier: 12.11.1
version: 12.11.1
drizzle-orm:
specifier: 0.45.2
version: 0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)
fastify: fastify:
specifier: 5.10.0 specifier: 5.10.0
version: 5.10.0 version: 5.10.0
devDependencies: devDependencies:
'@types/better-sqlite3':
specifier: 7.6.13
version: 7.6.13
'@types/node': '@types/node':
specifier: 24.13.3 specifier: 24.13.3
version: 24.13.3 version: 24.13.3
@@ -446,6 +455,9 @@ packages:
'@standard-schema/spec@1.1.0': '@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@types/better-sqlite3@7.6.13':
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
'@types/chai@5.2.3': '@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -601,6 +613,19 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
better-sqlite3@12.11.1:
resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
brace-expansion@1.1.16: brace-expansion@1.1.16:
resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==}
@@ -611,6 +636,9 @@ packages:
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
callsites@3.1.0: callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -623,6 +651,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@@ -654,6 +685,14 @@ packages:
supports-color: supports-color:
optional: true optional: true
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
deep-is@0.1.4: deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -665,6 +704,105 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
drizzle-orm@0.45.2:
resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==}
peerDependencies:
'@aws-sdk/client-rds-data': '>=3'
'@cloudflare/workers-types': '>=4'
'@electric-sql/pglite': '>=0.2.0'
'@libsql/client': '>=0.10.0'
'@libsql/client-wasm': '>=0.10.0'
'@neondatabase/serverless': '>=0.10.0'
'@op-engineering/op-sqlite': '>=2'
'@opentelemetry/api': ^1.4.1
'@planetscale/database': '>=1.13'
'@prisma/client': '*'
'@tidbcloud/serverless': '*'
'@types/better-sqlite3': '*'
'@types/pg': '*'
'@types/sql.js': '*'
'@upstash/redis': '>=1.34.7'
'@vercel/postgres': '>=0.8.0'
'@xata.io/client': '*'
better-sqlite3: '>=7'
bun-types: '*'
expo-sqlite: '>=14.0.0'
gel: '>=2'
knex: '*'
kysely: '*'
mysql2: '>=2'
pg: '>=8'
postgres: '>=3'
prisma: '*'
sql.js: '>=1'
sqlite3: '>=5'
peerDependenciesMeta:
'@aws-sdk/client-rds-data':
optional: true
'@cloudflare/workers-types':
optional: true
'@electric-sql/pglite':
optional: true
'@libsql/client':
optional: true
'@libsql/client-wasm':
optional: true
'@neondatabase/serverless':
optional: true
'@op-engineering/op-sqlite':
optional: true
'@opentelemetry/api':
optional: true
'@planetscale/database':
optional: true
'@prisma/client':
optional: true
'@tidbcloud/serverless':
optional: true
'@types/better-sqlite3':
optional: true
'@types/pg':
optional: true
'@types/sql.js':
optional: true
'@upstash/redis':
optional: true
'@vercel/postgres':
optional: true
'@xata.io/client':
optional: true
better-sqlite3:
optional: true
bun-types:
optional: true
expo-sqlite:
optional: true
gel:
optional: true
knex:
optional: true
kysely:
optional: true
mysql2:
optional: true
pg:
optional: true
postgres:
optional: true
prisma:
optional: true
sql.js:
optional: true
sqlite3:
optional: true
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
es-module-lexer@1.7.0: es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
@@ -725,6 +863,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
expect-type@1.4.0: expect-type@1.4.0:
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -775,6 +917,9 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
find-my-way@9.6.0: find-my-way@9.6.0:
resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==} resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -790,11 +935,17 @@ packages:
flatted@3.4.2: flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
glob-parent@6.0.2: glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -815,6 +966,9 @@ packages:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2: ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@@ -834,6 +988,9 @@ packages:
inherits@2.0.4: inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
ipaddr.js@2.4.0: ipaddr.js@2.4.0:
resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@@ -897,6 +1054,10 @@ packages:
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
hasBin: true hasBin: true
mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
minimatch@10.2.5: minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
@@ -908,10 +1069,16 @@ packages:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
minipass@7.1.3: minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -920,9 +1087,16 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-abi@3.94.0:
resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==}
engines: {node: '>=10'}
obug@2.1.3: obug@2.1.3:
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
@@ -931,6 +1105,9 @@ packages:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -983,6 +1160,12 @@ packages:
resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
hasBin: true
prelude-ls@1.2.1: prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -998,6 +1181,9 @@ packages:
process-warning@5.0.0: process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1005,6 +1191,14 @@ packages:
quick-format-unescaped@4.0.4: quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
real-require@0.2.0: real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
@@ -1036,6 +1230,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-regex2@5.1.1: safe-regex2@5.1.1:
resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==}
hasBin: true hasBin: true
@@ -1069,6 +1266,12 @@ packages:
siginfo@2.0.0: siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
sonic-boom@4.2.1: sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
@@ -1090,6 +1293,13 @@ packages:
std-env@3.10.0: std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
strip-json-comments@3.1.1: strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1098,6 +1308,13 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
tar-fs@2.1.5:
resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
thread-stream@4.2.0: thread-stream@4.2.0:
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -1131,6 +1348,9 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4' typescript: '>=4.8.4'
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
type-check@0.4.0: type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1153,6 +1373,9 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite@7.3.6: vite@7.3.6:
resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -1241,6 +1464,9 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
yocto-queue@0.1.0: yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1512,6 +1738,10 @@ snapshots:
'@standard-schema/spec@1.1.0': {} '@standard-schema/spec@1.1.0': {}
'@types/better-sqlite3@7.6.13':
dependencies:
'@types/node': 24.13.3
'@types/chai@5.2.3': '@types/chai@5.2.3':
dependencies: dependencies:
'@types/deep-eql': 4.0.2 '@types/deep-eql': 4.0.2
@@ -1702,6 +1932,23 @@ snapshots:
balanced-match@4.0.4: {} balanced-match@4.0.4: {}
base64-js@1.5.1: {}
better-sqlite3@12.11.1:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
bl@4.1.0:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
brace-expansion@1.1.16: brace-expansion@1.1.16:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -1715,6 +1962,11 @@ snapshots:
dependencies: dependencies:
balanced-match: 4.0.4 balanced-match: 4.0.4
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
callsites@3.1.0: {} callsites@3.1.0: {}
chai@6.2.2: {} chai@6.2.2: {}
@@ -1724,6 +1976,8 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
chownr@1.1.4: {}
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@@ -1746,12 +2000,29 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
deep-extend@0.6.0: {}
deep-is@0.1.4: {} deep-is@0.1.4: {}
depd@2.0.0: {} depd@2.0.0: {}
dequal@2.0.3: {} dequal@2.0.3: {}
detect-libc@2.1.2: {}
drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1):
optionalDependencies:
'@types/better-sqlite3': 7.6.13
better-sqlite3: 12.11.1
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
es-module-lexer@1.7.0: {} es-module-lexer@1.7.0: {}
esbuild@0.28.1: esbuild@0.28.1:
@@ -1857,6 +2128,8 @@ snapshots:
esutils@2.0.3: {} esutils@2.0.3: {}
expand-template@2.0.3: {}
expect-type@1.4.0: {} expect-type@1.4.0: {}
fast-decode-uri-component@1.0.1: {} fast-decode-uri-component@1.0.1: {}
@@ -1916,6 +2189,8 @@ snapshots:
dependencies: dependencies:
flat-cache: 4.0.1 flat-cache: 4.0.1
file-uri-to-path@1.0.0: {}
find-my-way@9.6.0: find-my-way@9.6.0:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -1934,9 +2209,13 @@ snapshots:
flatted@3.4.2: {} flatted@3.4.2: {}
fs-constants@1.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
github-from-package@0.0.0: {}
glob-parent@6.0.2: glob-parent@6.0.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -1959,6 +2238,8 @@ snapshots:
statuses: 2.0.2 statuses: 2.0.2
toidentifier: 1.0.1 toidentifier: 1.0.1
ieee754@1.2.1: {}
ignore@5.3.2: {} ignore@5.3.2: {}
ignore@7.0.6: {} ignore@7.0.6: {}
@@ -1972,6 +2253,8 @@ snapshots:
inherits@2.0.4: {} inherits@2.0.4: {}
ini@1.3.8: {}
ipaddr.js@2.4.0: {} ipaddr.js@2.4.0: {}
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
@@ -2027,6 +2310,8 @@ snapshots:
mime@3.0.0: {} mime@3.0.0: {}
mimic-response@3.1.0: {}
minimatch@10.2.5: minimatch@10.2.5:
dependencies: dependencies:
brace-expansion: 5.0.7 brace-expansion: 5.0.7
@@ -2039,18 +2324,32 @@ snapshots:
dependencies: dependencies:
brace-expansion: 2.1.2 brace-expansion: 2.1.2
minimist@1.2.8: {}
minipass@7.1.3: {} minipass@7.1.3: {}
mkdirp-classic@0.5.3: {}
ms@2.1.3: {} ms@2.1.3: {}
nanoid@3.3.16: {} nanoid@3.3.16: {}
napi-build-utils@2.0.0: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
node-abi@3.94.0:
dependencies:
semver: 7.8.5
obug@2.1.3: {} obug@2.1.3: {}
on-exit-leak-free@2.1.2: {} on-exit-leak-free@2.1.2: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@@ -2113,6 +2412,21 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.1.2
expand-template: 2.0.3
github-from-package: 0.0.0
minimist: 1.2.8
mkdirp-classic: 0.5.3
napi-build-utils: 2.0.0
node-abi: 3.94.0
pump: 3.0.4
rc: 1.2.8
simple-get: 4.0.1
tar-fs: 2.1.5
tunnel-agent: 0.6.0
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier@3.8.1: {} prettier@3.8.1: {}
@@ -2121,10 +2435,28 @@ snapshots:
process-warning@5.0.0: {} process-warning@5.0.0: {}
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
once: 1.4.0
punycode@2.3.1: {} punycode@2.3.1: {}
quick-format-unescaped@4.0.4: {} quick-format-unescaped@4.0.4: {}
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
minimist: 1.2.8
strip-json-comments: 2.0.1
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
real-require@0.2.0: {} real-require@0.2.0: {}
real-require@1.0.0: {} real-require@1.0.0: {}
@@ -2170,6 +2502,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.62.2 '@rollup/rollup-win32-x64-msvc': 4.62.2
fsevents: 2.3.3 fsevents: 2.3.3
safe-buffer@5.2.1: {}
safe-regex2@5.1.1: safe-regex2@5.1.1:
dependencies: dependencies:
ret: 0.5.0 ret: 0.5.0
@@ -2192,6 +2526,14 @@ snapshots:
siginfo@2.0.0: {} siginfo@2.0.0: {}
simple-concat@1.0.1: {}
simple-get@4.0.1:
dependencies:
decompress-response: 6.0.0
once: 1.4.0
simple-concat: 1.0.1
sonic-boom@4.2.1: sonic-boom@4.2.1:
dependencies: dependencies:
atomic-sleep: 1.0.0 atomic-sleep: 1.0.0
@@ -2206,12 +2548,33 @@ snapshots:
std-env@3.10.0: {} std-env@3.10.0: {}
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-json-comments@2.0.1: {}
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
supports-color@7.2.0: supports-color@7.2.0:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
tar-fs@2.1.5:
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
pump: 3.0.4
tar-stream: 2.2.0
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
thread-stream@4.2.0: thread-stream@4.2.0:
dependencies: dependencies:
real-require: 1.0.0 real-require: 1.0.0
@@ -2235,6 +2598,10 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
type-check@0.4.0: type-check@0.4.0:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
@@ -2258,6 +2625,8 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
util-deprecate@1.0.2: {}
vite@7.3.6(@types/node@24.13.3): vite@7.3.6(@types/node@24.13.3):
dependencies: dependencies:
esbuild: 0.28.1 esbuild: 0.28.1
@@ -2318,4 +2687,6 @@ snapshots:
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
wrappy@1.0.2: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
+1
View File
@@ -5,4 +5,5 @@ packages:
# Vitest uses esbuild for TypeScript transformation. pnpm 11 blocks every # Vitest uses esbuild for TypeScript transformation. pnpm 11 blocks every
# dependency build by default, so keep the allow-list explicit and minimal. # dependency build by default, so keep the allow-list explicit and minimal.
allowBuilds: allowBuilds:
better-sqlite3: true
esbuild: true esbuild: true