mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 08:41:08 +00:00
feat: [wip] accountStore
This commit is contained in:
parent
71a7a3c76e
commit
333b5a4ed4
4 changed files with 184 additions and 64 deletions
|
@ -11,13 +11,13 @@ type SavedObject = {
|
||||||
actKeyPrivate: CryptoKey;
|
actKeyPrivate: CryptoKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AccountCredentials = {
|
export type AccountCredentials = {
|
||||||
idKeyId: string;
|
idKeyId: string;
|
||||||
encodedIdKeyPair?: EncodedBlahKeyPair;
|
encodedIdKeyPair?: EncodedBlahKeyPair;
|
||||||
actKeyPair: BlahKeyPair;
|
actKeyPair: BlahKeyPair;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AccountKeyStoreDB extends DBSchema {
|
interface AccountKeyDBSchema extends DBSchema {
|
||||||
[IDB_OBJECT_STORE_NAME]: {
|
[IDB_OBJECT_STORE_NAME]: {
|
||||||
key: string;
|
key: string;
|
||||||
value: SavedObject;
|
value: SavedObject;
|
||||||
|
@ -36,15 +36,15 @@ async function savedObjectToAccountCredentials(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AccountKeyStore {
|
class AccountKeyDB {
|
||||||
private db: IDBPDatabase<AccountKeyStoreDB>;
|
private db: IDBPDatabase<AccountKeyDBSchema>;
|
||||||
|
|
||||||
private constructor(db: IDBPDatabase<AccountKeyStoreDB>) {
|
private constructor(db: IDBPDatabase<AccountKeyDBSchema>) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async open(): Promise<AccountKeyStore> {
|
static async open(): Promise<AccountKeyDB> {
|
||||||
const db = await openDB<AccountKeyStoreDB>(IDB_NAME, 1, {
|
const db = await openDB<AccountKeyDBSchema>(IDB_NAME, 1, {
|
||||||
upgrade(db) {
|
upgrade(db) {
|
||||||
if (!db.objectStoreNames.contains(IDB_OBJECT_STORE_NAME)) {
|
if (!db.objectStoreNames.contains(IDB_OBJECT_STORE_NAME)) {
|
||||||
const objStore = db.createObjectStore(IDB_OBJECT_STORE_NAME, { keyPath: 'idKeyId' });
|
const objStore = db.createObjectStore(IDB_OBJECT_STORE_NAME, { keyPath: 'idKeyId' });
|
||||||
|
@ -53,7 +53,7 @@ export class AccountKeyStore {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return new AccountKeyStore(db);
|
return new AccountKeyDB(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAccount(
|
async addAccount(
|
||||||
|
@ -115,3 +115,13 @@ export class AccountKeyStore {
|
||||||
this.db.close();
|
this.db.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let accountKeyDB: AccountKeyDB | null = null;
|
||||||
|
export async function openAccountKeyDB(): Promise<AccountKeyDB> {
|
||||||
|
if (!accountKeyDB) {
|
||||||
|
accountKeyDB = await AccountKeyDB.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountKeyDB;
|
||||||
|
}
|
||||||
|
export type { AccountKeyDB };
|
85
src/lib/accounts/accountStore.ts
Normal file
85
src/lib/accounts/accountStore.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { writable, type Readable } from 'svelte/store';
|
||||||
|
import { type AccountKeyDB, openAccountKeyDB } from './accountKeyDB';
|
||||||
|
import { BlahIdentity, type BlahIdentityFile, type BlahProfile } from '@blah-im/core/identity';
|
||||||
|
import { type IdentityFileDB, openIdentityFileDB } from '$lib/identityFiles/identityFileDB';
|
||||||
|
import { BlahKeyPair } from '@blah-im/core/crypto';
|
||||||
|
|
||||||
|
export type Account = BlahIdentityFile & {
|
||||||
|
holdingKeyPrivate: boolean;
|
||||||
|
holdingPrivateOfActKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AccountStore implements Readable<Account[]> {
|
||||||
|
private keyDB: AccountKeyDB;
|
||||||
|
private identityFileDB: IdentityFileDB;
|
||||||
|
private internalStore = writable<Account[]>([]);
|
||||||
|
subscribe = this.internalStore.subscribe;
|
||||||
|
|
||||||
|
private constructor(keyDB: AccountKeyDB, identityFileDB: IdentityFileDB) {
|
||||||
|
this.keyDB = keyDB;
|
||||||
|
this.identityFileDB = identityFileDB;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async open(): Promise<AccountStore> {
|
||||||
|
const keyDB = await openAccountKeyDB();
|
||||||
|
const identityFileDB = await openIdentityFileDB();
|
||||||
|
const store = new AccountStore(keyDB, identityFileDB);
|
||||||
|
await store.loadAccounts();
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAccounts() {
|
||||||
|
const accountCreds = await this.keyDB.fetchAllAccounts();
|
||||||
|
const identityFileMap = await this.identityFileDB.fetchIdentityFiles(
|
||||||
|
accountCreds.map((x) => x.idKeyId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const accounts = accountCreds.flatMap((creds) => {
|
||||||
|
const identityFile = identityFileMap.get(creds.idKeyId);
|
||||||
|
if (!identityFile) return [];
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...identityFile,
|
||||||
|
holdingKeyPrivate: !!creds.encodedIdKeyPair,
|
||||||
|
holdingPrivateOfActKey: creds.actKeyPair.id
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.internalStore.set(accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async identityForAccount(
|
||||||
|
accountOrIdKeyId: Account | string,
|
||||||
|
password?: string
|
||||||
|
): Promise<BlahIdentity> {
|
||||||
|
const idKeyId =
|
||||||
|
typeof accountOrIdKeyId === 'string' ? accountOrIdKeyId : accountOrIdKeyId.id_key;
|
||||||
|
|
||||||
|
const identityFile = await this.identityFileDB.fetchIdentityFile(idKeyId);
|
||||||
|
if (!identityFile) throw new Error('Identity file not found');
|
||||||
|
|
||||||
|
const accountCreds = await this.keyDB.fetchAccount(idKeyId);
|
||||||
|
const encodedIdKeyPair = accountCreds?.encodedIdKeyPair;
|
||||||
|
const idKeyPair = encodedIdKeyPair
|
||||||
|
? await BlahKeyPair.fromEncoded(encodedIdKeyPair, password)
|
||||||
|
: undefined;
|
||||||
|
const actKeyPair = accountCreds?.actKeyPair;
|
||||||
|
|
||||||
|
return await BlahIdentity.fromIdentityFile(identityFile, idKeyPair, actKeyPair);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveIdentityFile(identity: BlahIdentity) {
|
||||||
|
const identityFile = identity.generateIdentityFile();
|
||||||
|
await this.identityFileDB.updateIdentityFile(identityFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAccount(profile: BlahProfile, password: string) {
|
||||||
|
const idKeyPair = await BlahKeyPair.generate(true);
|
||||||
|
const actKeyPair = await BlahKeyPair.generate(false);
|
||||||
|
const identity = await BlahIdentity.create(idKeyPair, actKeyPair, profile);
|
||||||
|
const encodedIdKeyPair = await idKeyPair.encode(password);
|
||||||
|
await this.keyDB.addAccount(idKeyPair.id, actKeyPair, encodedIdKeyPair);
|
||||||
|
await this.saveIdentityFile(identity);
|
||||||
|
}
|
||||||
|
}
|
81
src/lib/identityFiles/identityFileDB.ts
Normal file
81
src/lib/identityFiles/identityFileDB.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import type { BlahIdentityFile } from '@blah-im/core/identity';
|
||||||
|
import { openDB, type DBSchema, type IDBPDatabase } from 'idb';
|
||||||
|
|
||||||
|
const IDB_NAME = 'weblah-identities';
|
||||||
|
const IDB_OBJECT_STORE_NAME = 'identities';
|
||||||
|
|
||||||
|
const IDENTITY_FILE_MAX_AGE = 1000 * 60 * 60 * 24 * 30; // 30 days
|
||||||
|
|
||||||
|
interface IdentityFileDBSchema extends DBSchema {
|
||||||
|
[IDB_OBJECT_STORE_NAME]: {
|
||||||
|
key: string;
|
||||||
|
value: BlahIdentityFile & { lastUpdatedAt: Date };
|
||||||
|
indexes: { id_urls: string };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class IdentityFileDB {
|
||||||
|
private db: IDBPDatabase<IdentityFileDBSchema>;
|
||||||
|
|
||||||
|
private constructor(db: IDBPDatabase<IdentityFileDBSchema>) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async open(): Promise<IdentityFileDB> {
|
||||||
|
const db = await openDB<IdentityFileDBSchema>(IDB_NAME, 1, {
|
||||||
|
upgrade(db) {
|
||||||
|
if (!db.objectStoreNames.contains(IDB_OBJECT_STORE_NAME)) {
|
||||||
|
const store = db.createObjectStore(IDB_OBJECT_STORE_NAME, { keyPath: 'idKeyId' });
|
||||||
|
store.createIndex('id_urls', 'profile.signee.payload.id_urls', {
|
||||||
|
multiEntry: true,
|
||||||
|
unique: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = new IdentityFileDB(db);
|
||||||
|
await store.removeExpiredIdentityFiles();
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIdentityFile(identityFile: BlahIdentityFile): Promise<void> {
|
||||||
|
await this.db.put(IDB_OBJECT_STORE_NAME, { ...identityFile, lastUpdatedAt: new Date() });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchIdentityFile(idKeyId: string): Promise<BlahIdentityFile | undefined> {
|
||||||
|
return await this.db.get(IDB_OBJECT_STORE_NAME, idKeyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchIdentityFiles(idKeyIds: string[]): Promise<Map<string, BlahIdentityFile>> {
|
||||||
|
return new Map(
|
||||||
|
(
|
||||||
|
await Promise.all(
|
||||||
|
idKeyIds.map(async (idKeyId): Promise<[string, BlahIdentityFile] | null> => {
|
||||||
|
const profile = await this.fetchIdentityFile(idKeyId);
|
||||||
|
return profile ? [idKeyId, profile] : null;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).filter((x): x is [string, BlahIdentityFile] => !!x)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIdentityFileByIdUrl(idUrl: string): Promise<BlahIdentityFile | undefined> {
|
||||||
|
return await this.db.getFromIndex(IDB_OBJECT_STORE_NAME, 'id_urls', idUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeExpiredIdentityFiles(): Promise<void> {
|
||||||
|
const now = new Date();
|
||||||
|
const cutoff = new Date(now.getTime() - IDENTITY_FILE_MAX_AGE);
|
||||||
|
await this.db.delete(IDB_OBJECT_STORE_NAME, IDBKeyRange.upperBound(cutoff));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let identityFileDB: IdentityFileDB | null = null;
|
||||||
|
export async function openIdentityFileDB(): Promise<IdentityFileDB> {
|
||||||
|
if (!identityFileDB) {
|
||||||
|
identityFileDB = await IdentityFileDB.open();
|
||||||
|
}
|
||||||
|
return identityFileDB;
|
||||||
|
}
|
||||||
|
export type { IdentityFileDB };
|
|
@ -1,56 +0,0 @@
|
||||||
import type { BlahProfile } from '@blah-im/core/identity';
|
|
||||||
import { openDB, type DBSchema, type IDBPDatabase } from 'idb';
|
|
||||||
|
|
||||||
const IDB_NAME = 'weblah-profiles';
|
|
||||||
const IDB_OBJECT_STORE_NAME = 'profiles';
|
|
||||||
|
|
||||||
const PROFILE_MAX_AGE = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
||||||
|
|
||||||
interface ProfileStoreDB extends DBSchema {
|
|
||||||
[IDB_OBJECT_STORE_NAME]: {
|
|
||||||
key: string;
|
|
||||||
value: BlahProfile & { idKeyId: string; lastUpdatedAt: Date };
|
|
||||||
indexes: { id_urls: string };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ProfileStore {
|
|
||||||
private db: IDBPDatabase<ProfileStoreDB>;
|
|
||||||
|
|
||||||
private constructor(db: IDBPDatabase<ProfileStoreDB>) {
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async open(): Promise<ProfileStore> {
|
|
||||||
const db = await openDB<ProfileStoreDB>(IDB_NAME, 1, {
|
|
||||||
upgrade(db) {
|
|
||||||
if (!db.objectStoreNames.contains(IDB_OBJECT_STORE_NAME)) {
|
|
||||||
const store = db.createObjectStore(IDB_OBJECT_STORE_NAME, { keyPath: 'idKeyId' });
|
|
||||||
store.createIndex('id_urls', 'id_urls', { multiEntry: true, unique: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = new ProfileStore(db);
|
|
||||||
await store.removeOldProfiles();
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateProfile(idKeyId: string, profile: BlahProfile): Promise<void> {
|
|
||||||
await this.db.put(IDB_OBJECT_STORE_NAME, { idKeyId, ...profile, lastUpdatedAt: new Date() });
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProfile(idKeyId: string): Promise<BlahProfile | undefined> {
|
|
||||||
return await this.db.get(IDB_OBJECT_STORE_NAME, idKeyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProfileByIdUrl(idUrl: string): Promise<BlahProfile | undefined> {
|
|
||||||
return await this.db.getFromIndex(IDB_OBJECT_STORE_NAME, 'id_urls', idUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeOldProfiles(): Promise<void> {
|
|
||||||
const now = new Date();
|
|
||||||
const cutoff = new Date(now.getTime() - PROFILE_MAX_AGE);
|
|
||||||
await this.db.delete(IDB_OBJECT_STORE_NAME, IDBKeyRange.upperBound(cutoff));
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue