feat(vault): server-side kryptert fillager (V4.13)
Shade kunne flytte filer mellom peers (@shade/files) og lagre én liten profil-blob per konto, men hadde ingen alltid-på lagring av krypterte filer. Uten den kan ingen Shade-app tilby backup, og ingen klient lese data mens peeren som eier dem er avslått. Objekter er innholdsadresserte på hashen av CHIFFERTEKSTEN, så relayen kan lagre, deduplisere og verifisere uten nøkkel — den regner om hashen ved opplasting og avviser feilnavngitte objekter. Stier bor inne i det krypterte manifestet, aldri i objektnavn: relayen skal ikke lære hva filene heter. Loggen er append-only, så historikk og rollback følger av modellen. SqliteVaultStore har med vilje INGEN minne-fallback, i motsetning til blob-storen. Den fallbacken slettet Prisms profil ved en rutine-redeploy 2026-08-12 fordi den fungerte helt til containeren ble recreated, uten en eneste feilmelding. En backup som glemmer er verre enn ingen backup, så uten SHADE_VAULT_DB_PATH mountes rutene ikke — med en logglinje som sier hvorfor. Én feil fanget av testene: pubkeyen ble først lagt på UTENFOR signaturen, som både brøt verifyPayload og ville latt hvem som helst bytte identitet i transit på den TOFU-pinnende førsteskrivingen. 16 vault- + 7 store-tester, alle mot de ekte rutehåndtererne gjennom Honos fetch. Kjeden er dessuten kjørt mot en ekte HTTP-server med et ekte workspace: 491 filer / 7,7 MB, alle bit-identiske etter gjenoppretting, og andre push etter én endring sendte 0 KB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
"@shade/inbox-server": "workspace:*",
|
||||
"@shade/key-transparency": "workspace:*",
|
||||
"@shade/observability": "workspace:*",
|
||||
"@shade/storage-sqlite": "workspace:*",
|
||||
"@shade/vault": "workspace:*",
|
||||
"hono": "^4.12.12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -83,6 +83,30 @@ async function createInboxStore(): Promise<InboxStore & { close?: () => void | P
|
||||
* also opt the blob store *off* entirely via `SHADE_DISABLE_BLOB=1` —
|
||||
* useful for relays that only want the inbox surface.
|
||||
*/
|
||||
/**
|
||||
* Build the vault store, or `null` when none is configured.
|
||||
*
|
||||
* Deliberately NOT falling back to an in-memory store the way the blob
|
||||
* primitive does. A vault holds the user's actual files: an in-memory one
|
||||
* accepts every write, serves every read, and loses the lot on the next
|
||||
* container recreate — a backup that reports success and is not there. The
|
||||
* blob store's silent fallback destroyed Prism's profile on 2026-08-12
|
||||
* precisely because nothing failed loudly. Here, no path means no vault.
|
||||
*/
|
||||
async function createVaultStore(): Promise<import('@shade/vault').VaultStore | null> {
|
||||
const sqlitePath = process.env.SHADE_VAULT_DB_PATH;
|
||||
if (!sqlitePath) {
|
||||
logger.warn(
|
||||
'Vault not enabled — set SHADE_VAULT_DB_PATH to a path on a persistent volume. ' +
|
||||
'There is no in-memory fallback on purpose: a vault that forgets is not a backup.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const { SqliteVaultStore } = await import('@shade/storage-sqlite');
|
||||
logger.info('Using SQLite vault store', { path: sqlitePath });
|
||||
return new SqliteVaultStore(sqlitePath);
|
||||
}
|
||||
|
||||
async function createBlobStore(): Promise<BlobStore & { close?: () => void | Promise<void> }> {
|
||||
const sqlitePath = process.env.SHADE_BLOB_DB_PATH;
|
||||
const pgUrl = process.env.SHADE_BLOB_PG_URL ?? process.env.SHADE_PREKEY_PG_URL;
|
||||
@@ -255,6 +279,21 @@ if (blobDisabled) {
|
||||
logger.info('Blob primitive enabled', { route: '/v1/blob/:slotId' });
|
||||
}
|
||||
|
||||
// V4.13 — vault: server-side encrypted file store. Where the blob primitive
|
||||
// holds one small blob per account, a vault holds a whole collection with an
|
||||
// append-only version log. Off unless a store is configured, because a vault
|
||||
// that silently lives in RAM is worse than no vault at all — see the warning
|
||||
// on SHADE_VAULT_DB_PATH below.
|
||||
const vaultDisabled = process.env.SHADE_DISABLE_VAULT === '1';
|
||||
const vaultStore = vaultDisabled ? null : await createVaultStore();
|
||||
if (vaultDisabled) {
|
||||
logger.info('Vault disabled (SHADE_DISABLE_VAULT=1)');
|
||||
} else if (vaultStore) {
|
||||
const { createVaultRoutes } = await import('@shade/vault/server');
|
||||
app.route('/', createVaultRoutes(vaultStore, crypto));
|
||||
logger.info('Vault enabled', { route: '/v1/vault/:vaultId' });
|
||||
}
|
||||
|
||||
// ─── Optional: Observer + Dashboard ──────────────────────────
|
||||
|
||||
const observerToken = process.env.SHADE_OBSERVER_TOKEN;
|
||||
|
||||
Reference in New Issue
Block a user