story-research-zapwall/lib/articleStorage.ts

150 lines
4.2 KiB
TypeScript

import type { AlbyInvoice } from '@/types/alby'
import { storageService } from './storage/indexedDB'
interface StoredArticleData {
content: string
authorPubkey: string
articleId: string
decryptionKey?: string
decryptionIV?: string
invoice: {
invoice: string
paymentHash: string
amount: number
expiresAt: number
} | null
createdAt: number
}
// Default expiration: 30 days in milliseconds
const DEFAULT_EXPIRATION = 30 * 24 * 60 * 60 * 1000
const MASTER_KEY_STORAGE_KEY = 'article_storage_master_key'
async function getOrCreateMasterKey(): Promise<string> {
if (typeof window === 'undefined') {
throw new Error('Storage encryption requires browser environment')
}
const { storageService } = await import('./storage/indexedDB')
const existing = await storageService.get<string>(MASTER_KEY_STORAGE_KEY, 'article_storage')
if (existing) {
return existing
}
const keyBytes = crypto.getRandomValues(new Uint8Array(32))
let binary = ''
keyBytes.forEach((b) => {
binary += String.fromCharCode(b)
})
const key = btoa(binary)
await storageService.set(MASTER_KEY_STORAGE_KEY, key, 'article_storage')
return key
}
async function deriveSecret(articleId: string): Promise<string> {
const masterKey = await getOrCreateMasterKey()
return `${masterKey}:${articleId}`
}
/**
* Store private content temporarily until payment is confirmed
* Also stores the invoice if provided
* Uses IndexedDB exclusively
* Content expires after 30 days by default
* If decryptionKey and decryptionIV are provided, they will be stored for sending after payment
*/
export async function storePrivateContent(
articleId: string,
content: string,
authorPubkey: string,
invoice?: AlbyInvoice,
decryptionKey?: string,
decryptionIV?: string
): Promise<void> {
try {
const key = `article_private_content_${articleId}`
const secret = await deriveSecret(articleId)
const data: StoredArticleData = {
content,
authorPubkey,
articleId,
...(decryptionKey ? { decryptionKey } : {}),
...(decryptionIV ? { decryptionIV } : {}),
invoice: invoice
? {
invoice: invoice.invoice,
paymentHash: invoice.paymentHash,
amount: invoice.amount,
expiresAt: invoice.expiresAt,
}
: null,
createdAt: Date.now(),
}
// Store with expiration (30 days)
await storageService.set(key, data, secret, DEFAULT_EXPIRATION)
} catch (error) {
console.error('Error storing private content:', error)
}
}
/**
* Get stored private content for an article
* Returns null if not found or expired
*/
export async function getStoredPrivateContent(articleId: string): Promise<{
content: string
authorPubkey: string
decryptionKey?: string
decryptionIV?: string
invoice?: AlbyInvoice
} | null> {
try {
const key = `article_private_content_${articleId}`
const secret = await deriveSecret(articleId)
const data = await storageService.get<StoredArticleData>(key, secret)
if (!data) {
return null
}
return {
content: data.content,
authorPubkey: data.authorPubkey,
...(data.decryptionKey ? { decryptionKey: data.decryptionKey } : {}),
...(data.decryptionIV ? { decryptionIV: data.decryptionIV } : {}),
...(data.invoice
? {
invoice: {
invoice: data.invoice.invoice,
paymentHash: data.invoice.paymentHash,
amount: data.invoice.amount,
expiresAt: data.invoice.expiresAt,
} as AlbyInvoice,
}
: {}),
}
} catch (error) {
console.error('Error retrieving private content:', error)
return null
}
}
/**
* Get stored invoice for an article
*/
export async function getStoredInvoice(articleId: string): Promise<AlbyInvoice | null> {
const stored = await getStoredPrivateContent(articleId)
return stored?.invoice ?? null
}
/**
* Remove stored private content (after successful send or expiry)
*/
export async function removeStoredPrivateContent(articleId: string): Promise<void> {
try {
const key = `article_private_content_${articleId}`
await storageService.delete(key)
} catch (error) {
console.error('Error removing private content:', error)
}
}