import { db } from 'officerdb/db'; import { encryptSecret, decryptSecret } from 'officerdb/crypto'; import { eq } from 'drizzle-orm'; import { proxyManagerConnection, type ProxyManagerConnection } from '../db/schema'; import { normaliseUrl, type NpmCredentials } from './npm-client'; /** * Reading and writing the one connection row. * * The secret is encrypted with this plugin's own purpose key — `encryptSecret('proxy-manager', …)` — so * a leak of that key opens this column and nothing else. That is the per-purpose split the platform's * secret store already decided on; this only follows it. * * **The secret never leaves this module in plaintext except into `npmRequest`.** There is no route that * returns it, at any permission level, to anybody, including the owner. A settings form that wants to * show "a secret is set" gets a boolean. */ const PURPOSE = 'proxy-manager'; export type ConnectionView = { configured: boolean; url: string | null; identity: string | null; /** Never the secret itself. Only whether one is stored. */ hasSecret: boolean; version: string | null; lastProbeAt: string | null; lastProbeOk: boolean | null; }; const row = async (): Promise => { const rows = await db.select().from(proxyManagerConnection).limit(1); return rows[0] ?? null; }; export async function readConnection(): Promise { const found = await row(); if (!found) { return { configured: false, url: null, identity: null, hasSecret: false, version: null, lastProbeAt: null, lastProbeOk: null }; } return { configured: true, url: found.url, identity: found.identity, hasSecret: found.secret.length > 0, version: found.version, lastProbeAt: found.lastProbeAt ? found.lastProbeAt.toISOString() : null, lastProbeOk: found.lastProbeOk === null ? null : found.lastProbeOk === 1, }; } /** Decrypted, for the client only. Never serialise the result of this. */ export async function readCredentials(): Promise { const found = await row(); if (!found) return null; return { url: found.url, identity: found.identity, secret: decryptSecret(PURPOSE, found.secret) }; } export type SaveParams = { url: string; identity: string; secret?: string }; /** * `secret` omitted means "keep the stored one" — so the owner can correct a typo in the URL without * retyping a password, and so a settings form never has to round-trip the secret to send it back. */ export async function saveConnection(params: SaveParams): Promise { const url = normaliseUrl(params.url); const identity = params.identity.trim(); const existing = await row(); if (!existing && !params.secret) throw new Error('A secret is required the first time the connection is set.'); const secret = params.secret ? encryptSecret(PURPOSE, params.secret) : (existing?.secret as string); if (existing) { await db .update(proxyManagerConnection) .set({ url, identity, secret, updatedAt: new Date() }) .where(eq(proxyManagerConnection.id, existing.id)); } else { await db.insert(proxyManagerConnection).values({ url, identity, secret }); } return readConnection(); } export async function deleteConnection(): Promise { await db.delete(proxyManagerConnection); } export async function recordProbe(version: string | null, ok: boolean): Promise { const existing = await row(); if (!existing) return; await db .update(proxyManagerConnection) .set({ version, lastProbeAt: new Date(), lastProbeOk: ok ? 1 : 0 }) .where(eq(proxyManagerConnection.id, existing.id)); }