mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
156 lines
4.4 KiB
TypeScript
156 lines
4.4 KiB
TypeScript
// Backup restore waits for owner-bound trusted-mint convergence before it
|
|
// reports success, while retaining the hostile-file merge policy.
|
|
|
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
|
|
import {linkingPubKeyHex, saveLinkingKey} from './keys'
|
|
import {applyBackup, buildBackup} from './storage'
|
|
import {addTrustedMint, readTrustedMints} from './trustedMints'
|
|
import {stubLocalStorage} from './test-utils'
|
|
|
|
const LINKING_KEY = new Uint8Array(32).fill(7)
|
|
const OWNER_ID = linkingPubKeyHex(LINKING_KEY)
|
|
const KEY_A = '02' + 'aa'.repeat(32)
|
|
const KEY_B = '03' + 'bb'.repeat(32)
|
|
|
|
type LockRequest = {
|
|
readonly callback: () => unknown
|
|
readonly resolve: (value: unknown) => void
|
|
readonly reject: (reason: unknown) => void
|
|
}
|
|
|
|
class DeferredLocks {
|
|
readonly requests: LockRequest[] = []
|
|
|
|
readonly request = (_name: string, callback: () => unknown): Promise<unknown> =>
|
|
new Promise((resolve, reject) => {
|
|
this.requests.push({callback, resolve, reject})
|
|
})
|
|
|
|
async releaseNext(): Promise<void> {
|
|
const request = this.requests.shift()
|
|
if (!request) throw new Error('Expected a queued lock request.')
|
|
try {
|
|
request.resolve(await request.callback())
|
|
} catch (error) {
|
|
request.reject(error instanceof Error ? error : new Error(String(error)))
|
|
}
|
|
}
|
|
}
|
|
|
|
const backup = (server: string, mintPubkey: string) => ({
|
|
type: 'sattle-backup' as const,
|
|
version: 1 as const,
|
|
createdAt: 1,
|
|
bearers: [],
|
|
trustedMints: [
|
|
{
|
|
server,
|
|
mintPubkey,
|
|
addedAt: 1,
|
|
locked: true,
|
|
pendingMintPubkey: KEY_B,
|
|
},
|
|
],
|
|
})
|
|
|
|
const installProvenOwner = (): Promise<void> => saveLinkingKey(LINKING_KEY)
|
|
|
|
beforeEach(() => {
|
|
vi.unstubAllGlobals()
|
|
stubLocalStorage()
|
|
})
|
|
|
|
describe('owner-bound backup restore', () => {
|
|
it('exports only the active owner trusted-mint registry', async () => {
|
|
await installProvenOwner()
|
|
await addTrustedMint('backup-mint.example', KEY_A, {ownerId: OWNER_ID})
|
|
|
|
expect(buildBackup(OWNER_ID).trustedMints).toEqual([
|
|
expect.objectContaining({server: 'backup-mint.example', mintPubkey: KEY_A}),
|
|
])
|
|
expect(buildBackup(OWNER_ID).ownerId).toBe(OWNER_ID)
|
|
expect(buildBackup().trustedMints).toEqual([])
|
|
})
|
|
|
|
it('drops fresh-device mint trust instead of using a valid file owner marker', async () => {
|
|
const result = await applyBackup({
|
|
...backup('file-mint.example', KEY_A),
|
|
ownerId: OWNER_ID,
|
|
})
|
|
|
|
expect(result.trustedMintsAdded).toBe(0)
|
|
expect(readTrustedMints(OWNER_ID)).toEqual([])
|
|
expect(localStorage.getItem('sattle_trusted_mints')).toBeNull()
|
|
})
|
|
|
|
it('does not attach file mints to a malformed owner marker', async () => {
|
|
const result = await applyBackup({
|
|
...backup('file-mint.example', KEY_A),
|
|
ownerId: 'malformed-owner',
|
|
})
|
|
|
|
expect(result.trustedMintsAdded).toBe(0)
|
|
expect(readTrustedMints(OWNER_ID)).toEqual([])
|
|
})
|
|
|
|
it('does not resolve before the trusted-mint merge commits', async () => {
|
|
await installProvenOwner()
|
|
const locks = new DeferredLocks()
|
|
vi.stubGlobal('navigator', {locks})
|
|
|
|
let settled = false
|
|
const restoring = applyBackup(backup('backup-mint.example', KEY_A), OWNER_ID).then((result) => {
|
|
settled = true
|
|
return result
|
|
})
|
|
await vi.waitFor(() => expect(locks.requests).toHaveLength(1))
|
|
|
|
expect(settled).toBe(false)
|
|
await locks.releaseNext()
|
|
|
|
expect((await restoring).trustedMintsAdded).toBe(1)
|
|
const restored = readTrustedMints(OWNER_ID)
|
|
expect(restored).toEqual([
|
|
expect.objectContaining({
|
|
server: 'backup-mint.example',
|
|
mintPubkey: KEY_A,
|
|
locked: false,
|
|
unconfirmed: true,
|
|
}),
|
|
])
|
|
expect(restored[0]?.pendingMintPubkey).toBeUndefined()
|
|
})
|
|
|
|
it('keeps a local locked pin and pending rekey authoritative', async () => {
|
|
await installProvenOwner()
|
|
localStorage.setItem(
|
|
'sattle_trusted_mints',
|
|
JSON.stringify({
|
|
version: 1,
|
|
ownerId: OWNER_ID,
|
|
mints: [
|
|
{
|
|
server: 'local.example',
|
|
mintPubkey: KEY_A,
|
|
addedAt: 1,
|
|
locked: true,
|
|
pendingMintPubkey: KEY_B,
|
|
},
|
|
],
|
|
}),
|
|
)
|
|
|
|
const result = await applyBackup(backup('local.example', KEY_B), OWNER_ID)
|
|
|
|
expect(result.trustedMintsAdded).toBe(0)
|
|
expect(readTrustedMints(OWNER_ID)).toEqual([
|
|
expect.objectContaining({
|
|
mintPubkey: KEY_A,
|
|
locked: true,
|
|
pendingMintPubkey: KEY_B,
|
|
}),
|
|
])
|
|
})
|
|
})
|