Migrate from per-note vaults
trustset seals each note in its own Mera vault, so reading six notes is six passkey prompts. Moving that one file to a satchel namespace makes it one prompt per session, and the contract, the ABI and the on-chain bytes layout around it stay.
What changes for the user
| Action | Per-note Mera vault | One satchel session |
|---|---|---|
| Open six notes | six prompts | one prompt |
| Seal a note | one prompt each | no prompt after the session opens |
| Error text | three hand-written strings | SatchelError carries message and fix |
The diff
One file, web/src/lib/sealed.ts in Silk-Nodes/trustset. The Notes contract, NOTES_ABI and the Kind type are untouched.
--- a/web/src/lib/sealed.ts
+++ b/web/src/lib/sealed.ts
@@
-import {
- createSecretVaultWithExistingPasskey, decryptSecretVaultWithPasskey, isMeraError, parseSecretVault,
- type PasskeySecretVault,
-} from "@category-labs/mera";
+import { open, ns, parseSealed, SatchelError, type Sealed, type Satchel } from "satchel-sdk";
@@
-export type Sealed = { kind: Kind; by: string; at: number; vault: PasskeySecretVault | null; index: number };
+export type Note = { kind: Kind; by: string; at: number; vault: Sealed | null; index: number };
+const NOTES = ns("trustset/notes");
+let session: Promise<Satchel> | undefined;
+const satchel = () => (session ??= open({ rpId: location.hostname })); // one prompt per session
@@ readNotes
- try { vault = parseSecretVault(JSON.parse(dec.decode(ethers.getBytes(r.vault)))); } catch { /* unreadable, shown as such */ }
+ try { vault = parseSealed(ethers.getBytes(r.vault)); } catch { /* unreadable, shown as such */ }
@@ sealNote
- const secret = enc.encode(text);
- const vault = await createSecretVaultWithExistingPasskey({ rpId: location.hostname, secret });
- const bytes = enc.encode(JSON.stringify(vault));
+ const bytes = (await (await satchel()).seal(NOTES, text)).toBytes();
@@ openNote
-export async function openNote(vault: PasskeySecretVault): Promise<string> {
- const out = await decryptSecretVaultWithPasskey({ rpId: location.hostname, vault });
- const text = dec.decode(out as Uint8Array);
- (out as Uint8Array).fill(0);
- return text;
-}
+export async function openNote(vault: Sealed): Promise<string> {
+ return dec.decode(await (await satchel()).unseal(NOTES, vault));
+}
@@ explainSeal
- if (isMeraError(e)) {
- if (e.code === "PRF_UNAVAILABLE") return "This passkey cannot derive keys. iCloud Keychain and Google Password Manager passkeys can; some security keys cannot.";
- if (e.code === "DECRYPT_FAILED") return "That passkey is not the one this note was sealed with.";
- if (e.code === "PASSKEY_OPERATION_FAILED") return "The passkey prompt was closed, or there is no trustset passkey on this device yet.";
- return e.message;
- }
+ if (e instanceof SatchelError) return `${e.message} ${e.fix}`;What stays and what goes
- Stays: the contract, the ABI, the
Kindtype, the bytes stored on chain per note. - Goes: the JSON round trip of the vault and the three hand-written error strings. SatchelError carries the message and the fix.
- Namespace: one string,
trustset/notes. A second feature that needs state is anotherns()and no second prompt.
Notes already sealed as Mera vaults
Notes sealed before the change stay readable through the old call path until they are re-sealed. resealLegacy does it in one pass: each legacy vault is opened once, then sealed under the namespace.
import { decryptSecretVaultWithPasskey, parseSecretVault } from "@category-labs/mera";
import { ns, type Satchel } from "satchel-sdk";
const NOTES = ns("trustset/notes");
/** Reads every legacy Mera vault once, then seals the plaintext under the namespace. */
export async function resealLegacy(s: Satchel, legacy: Uint8Array[]): Promise<Uint8Array[]> {
const out: Uint8Array[] = [];
for (const raw of legacy) {
const vault = parseSecretVault(JSON.parse(new TextDecoder().decode(raw)));
const plain = await decryptSecretVaultWithPasskey({ rpId: location.hostname, vault });
out.push((await s.seal(NOTES, plain)).toBytes());
plain.fill(0);
}
return out;
}Each legacy vault still costs one prompt to open, once. After that the note is a satchel blob and never costs another.
The applied change
The patch applied to a trustset checkout, with its build log, is in integrations/trustset.