Add vault-sql shared project scaffolding

This commit is contained in:
Leendert de Borst
2025-06-22 16:23:01 +02:00
committed by Leendert de Borst
parent 8b2702cbe3
commit d149e5aeec
13 changed files with 5683 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ set -u # Treat unset variables as errors
chmod +x ./identity-generator/build.sh
chmod +x ./password-generator/build.sh
chmod +x ./models/build.sh
chmod +x ./vault-sql/build.sh
# Run all build scripts
echo "🚀 Starting build process for all modules..."
@@ -19,4 +20,7 @@ cd ../password-generator
cd ../models
./build.sh
cd ../vault-sql
./build.sh
echo "✅ All builds completed successfully."

46
shared/vault-sql/build.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
set -e # Stop on error
set -u # Treat unset variables as errors
# Define output targets for vault-sql
TARGETS=(
"../../apps/browser-extension/src/utils/dist/shared/vault-sql"
"../../apps/mobile-app/utils/dist/shared/vault-sql"
"../../apps/server/AliasVault.Client/wwwroot/js/dist/shared/vault-sql"
)
# Build and distribute vault-sql
package_name="vault-sql"
package_path="."
echo "📦 Building $package_name..."
npm install && npm run lint && npm run build
dist_path="dist"
for target in "${TARGETS[@]}"; do
echo "📂 Copying $package_name$target"
mkdir -p "$target"
# Remove any existing files in the target directory
rm -rf "$target/*"
# Copy all build outputs
cp -R "$dist_path"/* "$target/"
# Write README
cat > "$target/README.md" <<EOF
# ⚠️ Auto-Generated Files
This folder contains the output of the shared \`$package_name\` module from the \`/shared\` directory in the AliasVault project.
**Do not edit any of these files manually.**
To make changes:
1. Update the source files in the \`/shared/vault-sql/src\` directory
2. Run the \`build.sh\` script in the module directory to regenerate the outputs and copy them here.
EOF
done
echo "✅ Vault-SQL build and copy completed."

View File

@@ -0,0 +1,56 @@
import js from "@eslint/js";
import tsParser from "@typescript-eslint/parser";
import tsPlugin from "@typescript-eslint/eslint-plugin";
export default [
{
ignores: [
"dist/**",
"node_modules/**",
]
},
js.configs.recommended,
{
files: ["src/**/*.ts"],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./tsconfig.json",
tsconfigRootDir: ".",
},
},
plugins: {
"@typescript-eslint": tsPlugin,
},
rules: {
...tsPlugin.configs.recommended.rules,
"curly": ["error", "all"],
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
"@typescript-eslint/no-unused-vars": ["error", {
"vars": "all",
"args": "after-used",
"ignoreRestSiblings": true,
"varsIgnorePattern": "^_",
"argsIgnorePattern": "^_"
}],
"indent": ["error", 2],
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1, "maxBOF": 0 }],
"spaced-comment": ["error", "always"],
"multiline-comment-style": ["error", "starred-block"],
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": ["PascalCase"],
"prefix": ["I"]
},
{
"selector": "typeAlias",
"format": ["PascalCase"]
}
],
}
}
];

4603
shared/vault-sql/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
{
"name": "@aliasvault/vault-sql",
"version": "1.0.0",
"type": "module",
"main": "dist/index.mjs",
"types": "dist/index.d.mts",
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"test": "vitest",
"test:coverage": "vitest --coverage"
},
"files": [
"dist"
],
"license": "MIT",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0",
"eslint": "^9.19.0",
"tsup": "^8.4.0",
"typescript": "^5.3.3",
"vitest": "^2.0.0",
"@vitest/coverage-v8": "^2.0.0"
}
}

View File

@@ -0,0 +1,279 @@
import { COMPLETE_SCHEMA_SQL, MIGRATION_SCRIPTS } from './sql/SqlConstants.js';
import { VAULT_VERSIONS, CURRENT_VAULT_VERSION, type IVaultVersion } from './types/VaultVersion.js';
/**
* Database execution interface for different platforms
*/
export interface IDbExecutor {
/**
* Execute SQL command
*/
executeSql(sql: string): Promise<void>;
/**
* Execute SQL command and return results
*/
executeSqlWithResults<T = unknown>(sql: string): Promise<T[]>;
/**
* Execute multiple SQL commands in a transaction
*/
executeTransaction(sqlCommands: string[]): Promise<void>;
}
/**
* Vault creation and migration result
*/
export interface IVaultOperationResult {
success: boolean;
version: string;
migrationNumber: number;
error?: string;
}
/**
* Vault version info from database
*/
export interface ICurrentVaultInfo {
version: string;
migrationNumber: number;
needsUpgrade: boolean;
availableUpgrades: IVaultVersion[];
}
/**
* Vault SQL manager for creating and migrating vaults across platforms
*/
export class VaultManager {
constructor(private dbExecutor: IDbExecutor) {}
/**
* Create a new vault with the latest schema
*/
async createNewVault(): Promise<IVaultOperationResult> {
try {
// Enable foreign key constraints and create complete schema
const sqlCommands = [
'PRAGMA foreign_keys = ON;',
COMPLETE_SCHEMA_SQL,
// Insert version tracking
`INSERT INTO "Settings" ("Key", "Value", "CreatedAt", "UpdatedAt", "IsDeleted")
VALUES ('vault_version', '${CURRENT_VAULT_VERSION.version}', datetime('now'), datetime('now'), 0);`,
`INSERT INTO "Settings" ("Key", "Value", "CreatedAt", "UpdatedAt", "IsDeleted")
VALUES ('vault_migration_number', '${CURRENT_VAULT_VERSION.migrationNumber}', datetime('now'), datetime('now'), 0);`
];
await this.dbExecutor.executeTransaction(sqlCommands);
return {
success: true,
version: CURRENT_VAULT_VERSION.version,
migrationNumber: CURRENT_VAULT_VERSION.migrationNumber
};
} catch (error) {
return {
success: false,
version: '0.0.0',
migrationNumber: 0,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Get current vault version and upgrade information
*/
async getCurrentVaultInfo(): Promise<ICurrentVaultInfo> {
try {
// Check if Settings table exists
const tables = await this.dbExecutor.executeSqlWithResults<{name: string}>(
"SELECT name FROM sqlite_master WHERE type='table' AND name='Settings';"
);
if (tables.length === 0) {
// Very old vault or no vault - needs full migration
return {
version: '0.0.0',
migrationNumber: 0,
needsUpgrade: true,
availableUpgrades: VAULT_VERSIONS
};
}
// Try to get version from Settings table
const versionResult = await this.dbExecutor.executeSqlWithResults<{Value: string}>(
"SELECT Value FROM Settings WHERE Key = 'vault_version' AND IsDeleted = 0 LIMIT 1;"
);
const migrationResult = await this.dbExecutor.executeSqlWithResults<{Value: string}>(
"SELECT Value FROM Settings WHERE Key = 'vault_migration_number' AND IsDeleted = 0 LIMIT 1;"
);
let currentVersion = '1.0.0';
let currentMigrationNumber = 1;
if (versionResult.length > 0) {
currentVersion = versionResult[0].Value;
}
if (migrationResult.length > 0) {
currentMigrationNumber = parseInt(migrationResult[0].Value, 10);
}
const needsUpgrade = currentMigrationNumber < CURRENT_VAULT_VERSION.migrationNumber;
const availableUpgrades = VAULT_VERSIONS.filter(v => v.migrationNumber > currentMigrationNumber);
return {
version: currentVersion,
migrationNumber: currentMigrationNumber,
needsUpgrade,
availableUpgrades
};
} catch {
// If we can't determine version, assume it needs upgrade
return {
version: '0.0.0',
migrationNumber: 0,
needsUpgrade: true,
availableUpgrades: VAULT_VERSIONS
};
}
}
/**
* Upgrade vault to latest version
*/
async upgradeVault(): Promise<IVaultOperationResult> {
try {
const currentInfo = await this.getCurrentVaultInfo();
if (!currentInfo.needsUpgrade) {
return {
success: true,
version: currentInfo.version,
migrationNumber: currentInfo.migrationNumber
};
}
// Apply migrations in order
const sqlCommands: string[] = ['PRAGMA foreign_keys = ON;'];
for (const upgrade of currentInfo.availableUpgrades) {
const migrationSql = MIGRATION_SCRIPTS[upgrade.migrationNumber];
if (migrationSql) {
sqlCommands.push(migrationSql);
}
}
// Update version tracking
sqlCommands.push(
`INSERT OR REPLACE INTO "Settings" ("Key", "Value", "CreatedAt", "UpdatedAt", "IsDeleted")
VALUES ('vault_version', '${CURRENT_VAULT_VERSION.version}', datetime('now'), datetime('now'), 0);`
);
sqlCommands.push(
`INSERT OR REPLACE INTO "Settings" ("Key", "Value", "CreatedAt", "UpdatedAt", "IsDeleted")
VALUES ('vault_migration_number', '${CURRENT_VAULT_VERSION.migrationNumber}', datetime('now'), datetime('now'), 0);`
);
await this.dbExecutor.executeTransaction(sqlCommands);
return {
success: true,
version: CURRENT_VAULT_VERSION.version,
migrationNumber: CURRENT_VAULT_VERSION.migrationNumber
};
} catch (error) {
return {
success: false,
version: '0.0.0',
migrationNumber: 0,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Upgrade vault to a specific version
*/
async upgradeVaultToVersion(targetVersion: string): Promise<IVaultOperationResult> {
try {
const currentInfo = await this.getCurrentVaultInfo();
const targetVersionInfo = VAULT_VERSIONS.find(v => v.version === targetVersion);
if (!targetVersionInfo) {
return {
success: false,
version: currentInfo.version,
migrationNumber: currentInfo.migrationNumber,
error: `Target version ${targetVersion} not found`
};
}
if (currentInfo.migrationNumber >= targetVersionInfo.migrationNumber) {
return {
success: true,
version: currentInfo.version,
migrationNumber: currentInfo.migrationNumber
};
}
// Apply migrations up to target version
const sqlCommands: string[] = ['PRAGMA foreign_keys = ON;'];
const migrationsToApply = VAULT_VERSIONS.filter(
v => v.migrationNumber > currentInfo.migrationNumber &&
v.migrationNumber <= targetVersionInfo.migrationNumber
);
for (const migration of migrationsToApply) {
const migrationSql = MIGRATION_SCRIPTS[migration.migrationNumber];
if (migrationSql) {
sqlCommands.push(migrationSql);
}
}
// Update version tracking
sqlCommands.push(
`INSERT OR REPLACE INTO "Settings" ("Key", "Value", "CreatedAt", "UpdatedAt", "IsDeleted")
VALUES ('vault_version', '${targetVersionInfo.version}', datetime('now'), datetime('now'), 0);`
);
sqlCommands.push(
`INSERT OR REPLACE INTO "Settings" ("Key", "Value", "CreatedAt", "UpdatedAt", "IsDeleted")
VALUES ('vault_migration_number', '${targetVersionInfo.migrationNumber}', datetime('now'), datetime('now'), 0);`
);
await this.dbExecutor.executeTransaction(sqlCommands);
return {
success: true,
version: targetVersionInfo.version,
migrationNumber: targetVersionInfo.migrationNumber
};
} catch (error) {
return {
success: false,
version: '0.0.0',
migrationNumber: 0,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Check if vault database exists and is valid
*/
async isValidVault(): Promise<boolean> {
try {
// Check if core tables exist
const tables = await this.dbExecutor.executeSqlWithResults<{name: string}>(
`SELECT name FROM sqlite_master WHERE type='table' AND name IN
('Aliases', 'Services', 'Credentials', 'Passwords', 'Attachments', 'EncryptionKeys', 'Settings', 'TotpCodes');`
);
// Should have all 8 core tables
return tables.length >= 5; // At minimum, need core tables (some might not exist in older versions)
} catch {
return false;
}
}
}

View File

@@ -0,0 +1,198 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { VaultManager, type IDbExecutor } from '../VaultManager.js';
import { CURRENT_VAULT_VERSION } from '../types/VaultVersion.js';
// Mock database executor for testing
class MockDbExecutor implements IDbExecutor {
private queries: string[] = [];
private mockResults: Record<string, unknown[]> = {};
async executeSql(sql: string): Promise<void> {
this.queries.push(sql);
}
async executeSqlWithResults<T = unknown>(sql: string): Promise<T[]> {
this.queries.push(sql);
// Normalize SQL for matching - remove extra whitespace and newlines
const key = sql.toLowerCase().replace(/\s+/g, ' ').trim();
return (this.mockResults[key] as T[]) || [];
}
async executeTransaction(sqlCommands: string[]): Promise<void> {
this.queries.push(...sqlCommands);
}
// Test helpers
getExecutedQueries(): string[] {
return [...this.queries];
}
clearQueries(): void {
this.queries = [];
}
setMockResult(sql: string, result: unknown[]): void {
// Normalize SQL for matching - remove extra whitespace and newlines
const key = sql.toLowerCase().replace(/\s+/g, ' ').trim();
this.mockResults[key] = result;
}
}
describe('VaultManager', () => {
let vaultManager: VaultManager;
let mockDb: MockDbExecutor;
beforeEach(() => {
mockDb = new MockDbExecutor();
vaultManager = new VaultManager(mockDb);
});
describe('createNewVault', () => {
it('should create a new vault with latest schema', async () => {
const result = await vaultManager.createNewVault();
expect(result.success).toBe(true);
expect(result.version).toBe(CURRENT_VAULT_VERSION.version);
expect(result.migrationNumber).toBe(CURRENT_VAULT_VERSION.migrationNumber);
const queries = mockDb.getExecutedQueries();
expect(queries).toContain('PRAGMA foreign_keys = ON;');
expect(queries.some(q => q.includes('CREATE TABLE "Aliases"'))).toBe(true);
expect(queries.some(q => q.includes('vault_version'))).toBe(true);
});
it('should handle errors during vault creation', async () => {
const failingDb = {
executeSql: vi.fn().mockRejectedValue(new Error('Database error')),
executeSqlWithResults: vi.fn(),
executeTransaction: vi.fn().mockRejectedValue(new Error('Database error'))
};
const failingVaultManager = new VaultManager(failingDb);
const result = await failingVaultManager.createNewVault();
expect(result.success).toBe(false);
expect(result.error).toBe('Database error');
});
});
describe('getCurrentVaultInfo', () => {
it('should return correct info for new vault without Settings table', async () => {
// Mock no Settings table exists
mockDb.setMockResult("select name from sqlite_master where type='table' and name='settings';", []);
const info = await vaultManager.getCurrentVaultInfo();
expect(info.version).toBe('0.0.0');
expect(info.migrationNumber).toBe(0);
expect(info.needsUpgrade).toBe(true);
expect(info.availableUpgrades.length).toBeGreaterThan(0);
});
it('should return correct info for existing vault with version info', async () => {
// Mock Settings table exists
mockDb.setMockResult("select name from sqlite_master where type='table' and name='settings';", [{ name: 'Settings' }]);
// Mock version queries
mockDb.setMockResult("select value from settings where key = 'vault_version' and isdeleted = 0 limit 1;", [{ Value: '1.2.0' }]);
mockDb.setMockResult("select value from settings where key = 'vault_migration_number' and isdeleted = 0 limit 1;", [{ Value: '4' }]);
const info = await vaultManager.getCurrentVaultInfo();
expect(info.version).toBe('1.2.0');
expect(info.migrationNumber).toBe(4);
expect(info.needsUpgrade).toBe(info.migrationNumber < CURRENT_VAULT_VERSION.migrationNumber);
});
it('should handle errors and default to needs upgrade', async () => {
const failingDb = {
executeSql: vi.fn(),
executeSqlWithResults: vi.fn().mockRejectedValue(new Error('Database error')),
executeTransaction: vi.fn()
};
const failingVaultManager = new VaultManager(failingDb);
const info = await failingVaultManager.getCurrentVaultInfo();
expect(info.version).toBe('0.0.0');
expect(info.migrationNumber).toBe(0);
expect(info.needsUpgrade).toBe(true);
});
});
describe('upgradeVault', () => {
it('should not upgrade if vault is already current', async () => {
// Mock Settings table exists
mockDb.setMockResult("select name from sqlite_master where type='table' and name='settings';", [{ name: 'Settings' }]);
// Mock current version
mockDb.setMockResult("select value from settings where key = 'vault_version' and isdeleted = 0 limit 1;", [{ Value: CURRENT_VAULT_VERSION.version }]);
mockDb.setMockResult("select value from settings where key = 'vault_migration_number' and isdeleted = 0 limit 1;", [{ Value: CURRENT_VAULT_VERSION.migrationNumber.toString() }]);
const result = await vaultManager.upgradeVault();
expect(result.success).toBe(true);
expect(result.version).toBe(CURRENT_VAULT_VERSION.version);
expect(result.migrationNumber).toBe(CURRENT_VAULT_VERSION.migrationNumber);
});
it('should upgrade from older version', async () => {
// Mock Settings table exists with older version
mockDb.setMockResult("select name from sqlite_master where type='table' and name='settings';", [{ name: 'Settings' }]);
mockDb.setMockResult("select value from settings where key = 'vault_version' and isdeleted = 0 limit 1;", [{ Value: '1.0.0' }]);
mockDb.setMockResult("select value from settings where key = 'vault_migration_number' and isdeleted = 0 limit 1;", [{ Value: '1' }]);
const result = await vaultManager.upgradeVault();
expect(result.success).toBe(true);
expect(result.version).toBe(CURRENT_VAULT_VERSION.version);
expect(result.migrationNumber).toBe(CURRENT_VAULT_VERSION.migrationNumber);
const queries = mockDb.getExecutedQueries();
expect(queries).toContain('PRAGMA foreign_keys = ON;');
expect(queries.some(q => q.includes('vault_version'))).toBe(true);
});
});
describe('isValidVault', () => {
it('should return true for valid vault with core tables', async () => {
mockDb.setMockResult(
`select name from sqlite_master where type='table' and name in
('aliases', 'services', 'credentials', 'passwords', 'attachments', 'encryptionkeys', 'settings', 'totpcodes');`,
[
{ name: 'Aliases' },
{ name: 'Services' },
{ name: 'Credentials' },
{ name: 'Passwords' },
{ name: 'Attachments' }
]
);
const isValid = await vaultManager.isValidVault();
expect(isValid).toBe(true);
});
it('should return false for vault with insufficient tables', async () => {
mockDb.setMockResult(
`select name from sqlite_master where type='table' and name in
('aliases', 'services', 'credentials', 'passwords', 'attachments', 'encryptionkeys', 'settings', 'totpcodes');`,
[{ name: 'Aliases' }]
);
const isValid = await vaultManager.isValidVault();
expect(isValid).toBe(false);
});
it('should return false on database error', async () => {
const failingDb = {
executeSql: vi.fn(),
executeSqlWithResults: vi.fn().mockRejectedValue(new Error('Database error')),
executeTransaction: vi.fn()
};
const failingVaultManager = new VaultManager(failingDb);
const isValid = await failingVaultManager.isValidVault();
expect(isValid).toBe(false);
});
});
});

View File

@@ -0,0 +1,27 @@
/**
* @aliasvault/vault-sql
*
* Shared SQL scripts and utilities for AliasVault database operations.
* Provides cross-platform vault creation and migration functionality.
*/
// Export VaultManager and interfaces
export {
VaultManager,
type IDbExecutor,
type IVaultOperationResult,
type ICurrentVaultInfo
} from './VaultManager.js';
// Export version types and constants
export {
type IVaultVersion,
VAULT_VERSIONS,
CURRENT_VAULT_VERSION
} from './types/VaultVersion.js';
// Export SQL constants
export {
COMPLETE_SCHEMA_SQL,
MIGRATION_SCRIPTS
} from './sql/SqlConstants.js';

View File

@@ -0,0 +1,300 @@
/**
* Complete database schema SQL (latest version)
*/
export const COMPLETE_SCHEMA_SQL = `-- AliasVault Client Database Complete Schema
-- Final schema after all migrations (up to version 1.5.0)
-- This script creates the complete database structure
-- Enable foreign key constraints
PRAGMA foreign_keys = ON;
-- Create Aliases table
CREATE TABLE "Aliases" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Gender" VARCHAR(255),
"FirstName" VARCHAR(255),
"LastName" VARCHAR(255),
"NickName" VARCHAR(255),
"BirthDate" TEXT NOT NULL,
"Email" TEXT, -- Renamed from EmailPrefix in v1.0.2
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"IsDeleted" INTEGER NOT NULL DEFAULT 0 -- Added in v1.4.0
-- Note: Address, phone, hobbies, and bank account columns removed in v1.3.0
);
-- Create Services table
CREATE TABLE "Services" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Name" TEXT,
"Url" TEXT,
"Logo" BLOB,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"IsDeleted" INTEGER NOT NULL DEFAULT 0 -- Added in v1.4.0
);
-- Create Credentials table
CREATE TABLE "Credentials" (
"Id" TEXT NOT NULL PRIMARY KEY,
"AliasId" TEXT NOT NULL,
"Notes" TEXT,
"Username" TEXT, -- Made optional in v1.3.1
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ServiceId" TEXT NOT NULL,
"IsDeleted" INTEGER NOT NULL DEFAULT 0, -- Added in v1.4.0
FOREIGN KEY ("AliasId") REFERENCES "Aliases" ("Id") ON DELETE CASCADE,
FOREIGN KEY ("ServiceId") REFERENCES "Services" ("Id") ON DELETE CASCADE
);
-- Create Attachments table (renamed from Attachment in v1.4.1)
CREATE TABLE "Attachments" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Filename" TEXT NOT NULL,
"Blob" BLOB NOT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"CredentialId" TEXT NOT NULL,
"IsDeleted" INTEGER NOT NULL DEFAULT 0, -- Added in v1.4.0
FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
);
-- Create Passwords table
CREATE TABLE "Passwords" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Value" TEXT,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"CredentialId" TEXT NOT NULL,
"IsDeleted" INTEGER NOT NULL DEFAULT 0, -- Added in v1.4.0
FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
);
-- Create EncryptionKeys table (added in v1.1.0)
CREATE TABLE "EncryptionKeys" (
"Id" TEXT NOT NULL PRIMARY KEY,
"PublicKey" TEXT NOT NULL,
"PrivateKey" TEXT NOT NULL,
"IsPrimary" INTEGER NOT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"IsDeleted" INTEGER NOT NULL DEFAULT 0 -- Added in v1.4.0
);
-- Create Settings table (added in v1.2.0)
CREATE TABLE "Settings" (
"Key" TEXT NOT NULL PRIMARY KEY,
"Value" TEXT,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"IsDeleted" INTEGER NOT NULL DEFAULT 0 -- Added in v1.4.0
);
-- Create TotpCodes table (added in v1.5.0)
CREATE TABLE "TotpCodes" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Name" TEXT NOT NULL,
"SecretKey" TEXT NOT NULL,
"CredentialId" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"IsDeleted" INTEGER NOT NULL,
FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
);
-- Create indexes
CREATE INDEX "IX_Attachments_CredentialId" ON "Attachments" ("CredentialId");
CREATE INDEX "IX_Credentials_AliasId" ON "Credentials" ("AliasId");
CREATE INDEX "IX_Credentials_ServiceId" ON "Credentials" ("ServiceId");
CREATE INDEX "IX_Passwords_CredentialId" ON "Passwords" ("CredentialId");
CREATE INDEX "IX_TotpCodes_CredentialId" ON "TotpCodes" ("CredentialId");`;
/**
* Individual migration SQL scripts
*/
export const MIGRATION_SCRIPTS: Record<number, string> = {
1: `-- Migration 1.0.0: Initial Migration
-- Create the initial database schema for AliasVault Client
-- Create Aliases table
CREATE TABLE "Aliases" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Gender" VARCHAR(255),
"FirstName" VARCHAR(255),
"LastName" VARCHAR(255),
"NickName" VARCHAR(255),
"BirthDate" TEXT NOT NULL,
"AddressStreet" VARCHAR(255),
"AddressCity" VARCHAR(255),
"AddressState" VARCHAR(255),
"AddressZipCode" VARCHAR(255),
"AddressCountry" VARCHAR(255),
"Hobbies" TEXT,
"EmailPrefix" TEXT,
"PhoneMobile" TEXT,
"BankAccountIBAN" TEXT,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL
);
-- Create Services table
CREATE TABLE "Services" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Name" TEXT,
"Url" TEXT,
"Logo" BLOB,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL
);
-- Create Credentials table
CREATE TABLE "Credentials" (
"Id" TEXT NOT NULL PRIMARY KEY,
"AliasId" TEXT NOT NULL,
"Notes" TEXT,
"Username" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ServiceId" TEXT NOT NULL,
FOREIGN KEY ("AliasId") REFERENCES "Aliases" ("Id") ON DELETE CASCADE,
FOREIGN KEY ("ServiceId") REFERENCES "Services" ("Id") ON DELETE CASCADE
);
-- Create Attachment table
CREATE TABLE "Attachment" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Filename" TEXT NOT NULL,
"Blob" BLOB NOT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"CredentialId" TEXT NOT NULL,
FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
);
-- Create Passwords table
CREATE TABLE "Passwords" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Value" TEXT,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"CredentialId" TEXT NOT NULL,
FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
);
-- Create indexes
CREATE INDEX "IX_Attachment_CredentialId" ON "Attachment" ("CredentialId");
CREATE INDEX "IX_Credentials_AliasId" ON "Credentials" ("AliasId");
CREATE INDEX "IX_Credentials_ServiceId" ON "Credentials" ("ServiceId");
CREATE INDEX "IX_Passwords_CredentialId" ON "Passwords" ("CredentialId");`,
2: `-- Migration 1.0.2: Change Email Column
-- Rename EmailPrefix to Email in Aliases table
ALTER TABLE "Aliases" RENAME COLUMN "EmailPrefix" TO "Email";`,
3: `-- Migration 1.1.0: Add PKI Tables
-- Add EncryptionKeys table for PKI support
CREATE TABLE "EncryptionKeys" (
"Id" TEXT NOT NULL PRIMARY KEY,
"PublicKey" TEXT NOT NULL,
"PrivateKey" TEXT NOT NULL,
"IsPrimary" INTEGER NOT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL
);`,
4: `-- Migration 1.2.0: Add Settings Table
-- Add Settings table for user preferences
CREATE TABLE "Settings" (
"Key" TEXT NOT NULL PRIMARY KEY,
"Value" TEXT,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL
);`,
5: `-- Migration 1.3.0: Update Identity Structure
-- Remove address, phone, hobbies, and bank account columns from Aliases table
ALTER TABLE "Aliases" DROP COLUMN "AddressStreet";
ALTER TABLE "Aliases" DROP COLUMN "AddressCity";
ALTER TABLE "Aliases" DROP COLUMN "AddressState";
ALTER TABLE "Aliases" DROP COLUMN "AddressZipCode";
ALTER TABLE "Aliases" DROP COLUMN "AddressCountry";
ALTER TABLE "Aliases" DROP COLUMN "Hobbies";
ALTER TABLE "Aliases" DROP COLUMN "PhoneMobile";
ALTER TABLE "Aliases" DROP COLUMN "BankAccountIBAN";`,
6: `-- Migration 1.3.1: Make Username Optional
-- Make Username column nullable in Credentials table
-- SQLite doesn't support ALTER COLUMN directly, so we need to recreate the table
-- Create temporary table with new structure
CREATE TABLE "Credentials_temp" (
"Id" TEXT NOT NULL PRIMARY KEY,
"AliasId" TEXT NOT NULL,
"Notes" TEXT,
"Username" TEXT, -- Made optional (nullable)
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ServiceId" TEXT NOT NULL,
FOREIGN KEY ("AliasId") REFERENCES "Aliases" ("Id") ON DELETE CASCADE,
FOREIGN KEY ("ServiceId") REFERENCES "Services" ("Id") ON DELETE CASCADE
);
-- Copy data from old table to new table
INSERT INTO "Credentials_temp"
SELECT "Id", "AliasId", "Notes", "Username", "CreatedAt", "UpdatedAt", "ServiceId"
FROM "Credentials";
-- Drop old table
DROP TABLE "Credentials";
-- Rename temp table to original name
ALTER TABLE "Credentials_temp" RENAME TO "Credentials";
-- Recreate indexes
CREATE INDEX "IX_Credentials_AliasId" ON "Credentials" ("AliasId");
CREATE INDEX "IX_Credentials_ServiceId" ON "Credentials" ("ServiceId");`,
7: `-- Migration 1.4.0: Add Sync Support
-- Add IsDeleted column to all tables for soft delete support
ALTER TABLE "Aliases" ADD COLUMN "IsDeleted" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "Services" ADD COLUMN "IsDeleted" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "Credentials" ADD COLUMN "IsDeleted" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "Attachment" ADD COLUMN "IsDeleted" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "Passwords" ADD COLUMN "IsDeleted" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "EncryptionKeys" ADD COLUMN "IsDeleted" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "Settings" ADD COLUMN "IsDeleted" INTEGER NOT NULL DEFAULT 0;`,
8: `-- Migration 1.4.1: Rename Attachments Plural
-- Rename Attachment table to Attachments for consistency
ALTER TABLE "Attachment" RENAME TO "Attachments";
-- Drop old index
DROP INDEX "IX_Attachment_CredentialId";
-- Create new index with correct name
CREATE INDEX "IX_Attachments_CredentialId" ON "Attachments" ("CredentialId");`,
9: `-- Migration 1.5.0: Add TOTP Codes
-- Add TotpCodes table for 2FA support
CREATE TABLE "TotpCodes" (
"Id" TEXT NOT NULL PRIMARY KEY,
"Name" TEXT NOT NULL,
"SecretKey" TEXT NOT NULL,
"CredentialId" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"IsDeleted" INTEGER NOT NULL,
FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
);
-- Create index
CREATE INDEX "IX_TotpCodes_CredentialId" ON "TotpCodes" ("CredentialId");`
};

View File

@@ -0,0 +1,89 @@
/**
* Vault database version information
*/
export interface IVaultVersion {
/**
* The version number (e.g., "1.5.0")
*/
version: string;
/**
* The migration number
*/
migrationNumber: number;
/**
* Description of changes in this version
*/
description: string;
/**
* Release date
*/
releaseDate: string;
}
/**
* Available vault versions in chronological order
*/
export const VAULT_VERSIONS: IVaultVersion[] = [
{
version: '1.0.0',
migrationNumber: 1,
description: 'Initial database schema with core vault functionality',
releaseDate: '2024-07-08'
},
{
version: '1.0.2',
migrationNumber: 2,
description: 'Email column rename for improved clarity',
releaseDate: '2024-07-11'
},
{
version: '1.1.0',
migrationNumber: 3,
description: 'PKI support with encryption keys table',
releaseDate: '2024-07-29'
},
{
version: '1.2.0',
migrationNumber: 4,
description: 'Settings table for user preferences',
releaseDate: '2024-08-05'
},
{
version: '1.3.0',
migrationNumber: 5,
description: 'Identity structure simplification',
releaseDate: '2024-08-05'
},
{
version: '1.3.1',
migrationNumber: 6,
description: 'Optional username support',
releaseDate: '2024-08-12'
},
{
version: '1.4.0',
migrationNumber: 7,
description: 'Soft delete support for synchronization',
releaseDate: '2024-09-16'
},
{
version: '1.4.1',
migrationNumber: 8,
description: 'Attachment table rename for consistency',
releaseDate: '2024-09-17'
},
{
version: '1.5.0',
migrationNumber: 9,
description: 'TOTP (2FA) support',
releaseDate: '2025-03-10'
}
];
/**
* Current/latest vault version
*/
export const CURRENT_VAULT_VERSION = VAULT_VERSIONS[VAULT_VERSIONS.length - 1];

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"module": "ESNext",
"target": "ES2020",
"lib": ["ES2020"],
"strict": true,
"moduleResolution": "bundler",
"esModuleInterop": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"skipLibCheck": true,
},
"include": ["src/**/*"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/**/index.ts'],
format: ['esm'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
treeshake: true,
target: 'es2020',
minify: false,
banner: {
js: '// <auto-generated>\n// This file was automatically generated. Do not edit manually.\n',
}
});

View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.test.ts',
'**/*.config.ts'
]
}
}
});