mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-14 01:39:56 -05:00
Chore: Remove Old Directory (Prepare for Patches)
This commit is contained in:
@@ -1,88 +1,98 @@
|
||||
const VAULT_KEY = "moku-credential-vault";
|
||||
const SALT_ITERATIONS = 200_000;
|
||||
const KEY_USAGE: KeyUsage[] = ["encrypt", "decrypt"];
|
||||
import { platformService } from '$lib/platform-service'
|
||||
|
||||
const VAULT_STORE_KEY = 'moku-vault'
|
||||
const SALT_ITERATIONS = 200_000
|
||||
const KEY_USAGE: KeyUsage[] = ['encrypt', 'decrypt']
|
||||
|
||||
export interface VaultPayload {
|
||||
refreshToken?: string;
|
||||
basicUser?: string;
|
||||
basicPass?: string;
|
||||
authMode: "UI_LOGIN" | "BASIC_AUTH" | "NONE";
|
||||
refreshToken?: string
|
||||
basicUser?: string
|
||||
basicPass?: string
|
||||
authMode: 'UI_LOGIN' | 'BASIC_AUTH' | 'NONE'
|
||||
}
|
||||
|
||||
interface StoredVault {
|
||||
salt: string;
|
||||
iv: string;
|
||||
data: string;
|
||||
salt: string
|
||||
iv: string
|
||||
data: string
|
||||
}
|
||||
|
||||
function toB64(buf: ArrayBuffer): string {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buf)));
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buf)))
|
||||
}
|
||||
|
||||
function fromB64(s: string): Uint8Array {
|
||||
return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
||||
return Uint8Array.from(atob(s), c => c.charCodeAt(0))
|
||||
}
|
||||
|
||||
async function deriveKey(pin: string, salt: Uint8Array): Promise<CryptoKey> {
|
||||
const enc = new TextEncoder();
|
||||
const keyMat = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]);
|
||||
const enc = new TextEncoder()
|
||||
const keyMat = await crypto.subtle.importKey('raw', enc.encode(pin), 'PBKDF2', false, ['deriveKey'])
|
||||
return crypto.subtle.deriveKey(
|
||||
{ name: "PBKDF2", salt, iterations: SALT_ITERATIONS, hash: "SHA-256" },
|
||||
{ name: 'PBKDF2', salt, iterations: SALT_ITERATIONS, hash: 'SHA-256' },
|
||||
keyMat,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
KEY_USAGE,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function vaultExists(): boolean {
|
||||
return !!localStorage.getItem(VAULT_KEY);
|
||||
}
|
||||
|
||||
export async function lockVault(pin: string, payload: VaultPayload): Promise<void> {
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const key = await deriveKey(pin, salt);
|
||||
|
||||
const enc = new TextEncoder();
|
||||
const cipher = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
key,
|
||||
enc.encode(JSON.stringify(payload)),
|
||||
);
|
||||
|
||||
localStorage.setItem(VAULT_KEY, JSON.stringify({
|
||||
salt: toB64(salt),
|
||||
iv: toB64(iv),
|
||||
data: toB64(cipher),
|
||||
} satisfies StoredVault));
|
||||
}
|
||||
|
||||
export async function unlockVault(pin: string): Promise<VaultPayload | null> {
|
||||
const raw = localStorage.getItem(VAULT_KEY);
|
||||
if (!raw) return null;
|
||||
|
||||
async function readRaw(): Promise<StoredVault | null> {
|
||||
try {
|
||||
const stored = JSON.parse(raw) as StoredVault;
|
||||
const key = await deriveKey(pin, fromB64(stored.salt));
|
||||
const plain = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: fromB64(stored.iv) },
|
||||
key,
|
||||
fromB64(stored.data),
|
||||
);
|
||||
return JSON.parse(new TextDecoder().decode(plain)) as VaultPayload;
|
||||
const raw = await platformService.getCredential(VAULT_STORE_KEY)
|
||||
return raw ? JSON.parse(raw) as StoredVault : null
|
||||
} catch {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function clearVault(): void {
|
||||
localStorage.removeItem(VAULT_KEY);
|
||||
async function writeRaw(vault: StoredVault): Promise<void> {
|
||||
await platformService.storeCredential(VAULT_STORE_KEY, JSON.stringify(vault))
|
||||
}
|
||||
|
||||
export async function vaultExists(): Promise<boolean> {
|
||||
return (await readRaw()) !== null
|
||||
}
|
||||
|
||||
export async function lockVault(pin: string, payload: VaultPayload): Promise<void> {
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16))
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12))
|
||||
const key = await deriveKey(pin, salt)
|
||||
const enc = new TextEncoder()
|
||||
|
||||
const cipher = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
enc.encode(JSON.stringify(payload)),
|
||||
)
|
||||
|
||||
await writeRaw({ salt: toB64(salt), iv: toB64(iv), data: toB64(cipher) })
|
||||
}
|
||||
|
||||
export async function unlockVault(pin: string): Promise<VaultPayload | null> {
|
||||
const stored = await readRaw()
|
||||
if (!stored) return null
|
||||
|
||||
try {
|
||||
const key = await deriveKey(pin, fromB64(stored.salt))
|
||||
const plain = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: fromB64(stored.iv) },
|
||||
key,
|
||||
fromB64(stored.data),
|
||||
)
|
||||
return JSON.parse(new TextDecoder().decode(plain)) as VaultPayload
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearVault(): Promise<void> {
|
||||
await platformService.storeCredential(VAULT_STORE_KEY, '')
|
||||
}
|
||||
|
||||
export async function rekeyVault(oldPin: string, newPin: string): Promise<boolean> {
|
||||
const payload = await unlockVault(oldPin);
|
||||
if (!payload) return false;
|
||||
await lockVault(newPin, payload);
|
||||
return true;
|
||||
const payload = await unlockVault(oldPin)
|
||||
if (!payload) return false
|
||||
await lockVault(newPin, payload)
|
||||
return true
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
export { loadAllStores, persistSettings, persistLibrary, persistUpdates } from "./persist";
|
||||
export type { PersistedData } from "./persist";
|
||||
export {
|
||||
loadSettings, saveSettings,
|
||||
loadLibrary, saveLibrary,
|
||||
loadUpdates, saveUpdates,
|
||||
loadBackups, saveBackups,
|
||||
} from './persist'
|
||||
|
||||
export { vaultExists, lockVault, unlockVault, clearVault, rekeyVault } from "./credentialVault";
|
||||
export type { VaultPayload } from "./credentialVault";
|
||||
export type {
|
||||
PersistedSettings,
|
||||
PersistedLibrary,
|
||||
PersistedUpdates,
|
||||
} from './persist'
|
||||
|
||||
export {
|
||||
vaultExists,
|
||||
lockVault,
|
||||
unlockVault,
|
||||
clearVault,
|
||||
rekeyVault,
|
||||
} from './credentialVault'
|
||||
|
||||
export type { VaultPayload } from './credentialVault'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { platformService } from '$lib/platform-service'
|
||||
import type { ReadSession } from '$lib/types/history'
|
||||
import type { BookmarkEntry, MarkerEntry } from '$lib/types/history'
|
||||
import type { ReadSession } from '$lib/types/history'
|
||||
import type { BookmarkEntry, MarkerEntry } from '$lib/types/history'
|
||||
|
||||
const STORE_VERSION = 2
|
||||
|
||||
@@ -22,10 +22,6 @@ export interface PersistedUpdates {
|
||||
acknowledgedUpdateIds: number[]
|
||||
}
|
||||
|
||||
export interface PersistedBackups {
|
||||
backupList: { url: string; name: string }[]
|
||||
}
|
||||
|
||||
function migrateLibrary(raw: unknown, fromVersion: number): PersistedLibrary {
|
||||
const data = (raw ?? {}) as Record<string, unknown>
|
||||
|
||||
@@ -36,27 +32,25 @@ function migrateLibrary(raw: unknown, fromVersion: number): PersistedLibrary {
|
||||
pageNumber?: number; readAt: number
|
||||
}>
|
||||
|
||||
const sessions: ReadSession[] = oldHistory.map(e => ({
|
||||
id: crypto.randomUUID(),
|
||||
mangaId: e.mangaId,
|
||||
mangaTitle: e.mangaTitle,
|
||||
thumbnailUrl: e.thumbnailUrl,
|
||||
startChapterId: e.chapterId,
|
||||
startChapterName: e.chapterName,
|
||||
endChapterId: e.chapterId,
|
||||
endChapterName: e.chapterName,
|
||||
startPage: 1,
|
||||
endPage: e.pageNumber ?? 1,
|
||||
startedAt: e.readAt,
|
||||
endedAt: e.readAt,
|
||||
durationMs: 0,
|
||||
chaptersSpanned: 1,
|
||||
}))
|
||||
|
||||
return {
|
||||
sessions,
|
||||
bookmarks: (data.bookmarks ?? []) as BookmarkEntry[],
|
||||
markers: (data.markers ?? []) as MarkerEntry[],
|
||||
sessions: oldHistory.map(e => ({
|
||||
id: crypto.randomUUID(),
|
||||
mangaId: e.mangaId,
|
||||
mangaTitle: e.mangaTitle,
|
||||
thumbnailUrl: e.thumbnailUrl,
|
||||
startChapterId: e.chapterId,
|
||||
startChapterName: e.chapterName,
|
||||
endChapterId: e.chapterId,
|
||||
endChapterName: e.chapterName,
|
||||
startPage: 1,
|
||||
endPage: e.pageNumber ?? 1,
|
||||
startedAt: e.readAt,
|
||||
endedAt: e.readAt,
|
||||
durationMs: 0,
|
||||
chaptersSpanned: 1,
|
||||
})),
|
||||
bookmarks: (data.bookmarks ?? []) as BookmarkEntry[],
|
||||
markers: (data.markers ?? []) as MarkerEntry[],
|
||||
dailyReadCounts: (data.dailyReadCounts ?? {}) as Record<string, number>,
|
||||
}
|
||||
}
|
||||
@@ -69,19 +63,30 @@ function migrateLibrary(raw: unknown, fromVersion: number): PersistedLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
function evacuateLocalStorage(key: string): unknown | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
try {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw)
|
||||
localStorage.removeItem(key)
|
||||
return parsed
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadSettings(): Promise<PersistedSettings> {
|
||||
const raw = await platformService.loadStore('settings')
|
||||
const raw = await platformService.loadStore('settings')
|
||||
const data = (raw ?? {}) as Record<string, unknown>
|
||||
|
||||
const legacyRaw = typeof window !== 'undefined' ? localStorage.getItem('moku_settings') : null
|
||||
if (legacyRaw && !data.settings) {
|
||||
try {
|
||||
const legacySettings = JSON.parse(legacyRaw)
|
||||
localStorage.removeItem('moku_settings')
|
||||
const result: PersistedSettings = { storeVersion: STORE_VERSION, settings: legacySettings }
|
||||
if (!data.settings) {
|
||||
const legacy = evacuateLocalStorage('moku_settings')
|
||||
if (legacy) {
|
||||
const result: PersistedSettings = { storeVersion: STORE_VERSION, settings: legacy }
|
||||
await saveSettings(result)
|
||||
return result
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -99,15 +104,13 @@ export async function loadLibrary(): Promise<PersistedLibrary> {
|
||||
const data = (raw ?? {}) as Record<string, unknown>
|
||||
const version = (data.storeVersion as number) ?? 1
|
||||
|
||||
const legacyRaw = typeof window !== 'undefined' ? localStorage.getItem('moku-store') : null
|
||||
if (legacyRaw && !(data.sessions || data.history)) {
|
||||
try {
|
||||
const legacy = JSON.parse(legacyRaw)
|
||||
if (!data.sessions && !data.history) {
|
||||
const legacy = evacuateLocalStorage('moku-store')
|
||||
if (legacy) {
|
||||
const migrated = migrateLibrary(legacy, 1)
|
||||
localStorage.removeItem('moku-store')
|
||||
await saveLibrary(migrated)
|
||||
return migrated
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return migrateLibrary(raw, version)
|
||||
@@ -136,15 +139,12 @@ export async function loadBackups(): Promise<{ url: string; name: string }[]> {
|
||||
const data = (raw ?? {}) as Record<string, unknown>
|
||||
|
||||
if (!data.backupList) {
|
||||
try {
|
||||
const legacyRaw = typeof window !== 'undefined' ? localStorage.getItem('moku_backups') : null
|
||||
if (legacyRaw) {
|
||||
const list = JSON.parse(legacyRaw) as { url: string; name: string }[]
|
||||
localStorage.removeItem('moku_backups')
|
||||
await saveBackups(list)
|
||||
return list
|
||||
}
|
||||
} catch {}
|
||||
const legacy = evacuateLocalStorage('moku_backups')
|
||||
if (legacy) {
|
||||
const list = legacy as { url: string; name: string }[]
|
||||
await saveBackups(list)
|
||||
return list
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user