keychain vault + secrets cache + env separated keychains
This commit is contained in:
@@ -3,4 +3,5 @@
|
||||
- Never commit changes unless explicitly told to do so. Always ask first or wait for the user's command.
|
||||
- Never manually edit `.router/router.ts` — sv-router auto-generates it from the filesystem. Just create the route file in the correct filesystem location.
|
||||
- Only commit when explicitly told to do so. When asked to commit, always write a single simple line commit message.
|
||||
- When asked to publish, always rebuild the DMG first (`bun run build:stable` from `apps/desktop`), wait for notarization to complete, verify with `xcrun stapler validate`, then publish the fresh DMG. Never publish a stale build.
|
||||
- At the end of every response, always surface a "Pending from you" section listing anything you need from the user (decisions, permissions, credentials, confirming a plan, etc.). If nothing is pending, say "Nothing pending."
|
||||
|
||||
@@ -13,7 +13,13 @@ import { createMoneroHandlers } from './rpc/monero'
|
||||
import { createXrpHandlers } from './rpc/xrp'
|
||||
import type { MoneroWalletState } from './lib/monero'
|
||||
import { canPromptTouchID, promptTouchID } from './lib/biometric'
|
||||
import { getXrpWalletList } from './lib/xrp'
|
||||
import {
|
||||
getSecret,
|
||||
setSecret,
|
||||
deleteSecret,
|
||||
clearSecretsCache,
|
||||
VAULT_SERVICE,
|
||||
} from './lib/secrets-cache'
|
||||
import type { RPC } from '../lib/rpc-schema'
|
||||
import { log } from './lib/logger'
|
||||
|
||||
@@ -89,23 +95,8 @@ const rpc = BrowserView.defineRPC<RPC>({
|
||||
rpc.setRequestHandler({
|
||||
resetApp: async () => {
|
||||
try {
|
||||
const wallets = await getXrpWalletList()
|
||||
await Promise.all([
|
||||
Bun.secrets.delete({
|
||||
service: 'koins',
|
||||
name: 'xrp_wallets',
|
||||
}),
|
||||
...wallets.flatMap((w) => [
|
||||
Bun.secrets.delete({
|
||||
service: 'koins',
|
||||
name: w.vaultKey,
|
||||
}),
|
||||
Bun.secrets.delete({
|
||||
service: 'koins',
|
||||
name: `xrp_auth_${w.id}`,
|
||||
}),
|
||||
]),
|
||||
])
|
||||
await deleteSecret(VAULT_SERVICE, 'vault')
|
||||
clearSecretsCache()
|
||||
console.log('[rpc] resetApp complete')
|
||||
return true
|
||||
} catch (e) {
|
||||
@@ -131,10 +122,10 @@ rpc.setRequestHandler({
|
||||
}
|
||||
},
|
||||
getSecret: async ({ name, service }) => {
|
||||
return await Bun.secrets.get({ name, service })
|
||||
return await getSecret(service, name)
|
||||
},
|
||||
setSecret: async ({ name, service, value }) => {
|
||||
await Bun.secrets.set({ name, service, value })
|
||||
await setSecret(service, name, value)
|
||||
},
|
||||
openExternal: async ({ url }) => {
|
||||
Utils.openExternal(url)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { getENV } from './get-env'
|
||||
|
||||
const env = getENV()
|
||||
export const VAULT_SERVICE = env.appName
|
||||
const VAULT_NAME = 'vault'
|
||||
|
||||
const cache = new Map<string, string>()
|
||||
const key = (service: string, name: string) => `${service}:${name}`
|
||||
|
||||
let loaded = false
|
||||
let loadPromise: Promise<void> | null = null
|
||||
let writeChain: Promise<void> = Promise.resolve()
|
||||
|
||||
async function ensureLoaded(): Promise<void> {
|
||||
if (loaded) return
|
||||
if (!loadPromise) {
|
||||
loadPromise = (async () => {
|
||||
const raw = await Bun.secrets.get({
|
||||
service: VAULT_SERVICE,
|
||||
name: VAULT_NAME,
|
||||
})
|
||||
if (raw) {
|
||||
try {
|
||||
const data = JSON.parse(raw) as Record<string, string>
|
||||
for (const [k, v] of Object.entries(data)) cache.set(k, v)
|
||||
} catch (e) {
|
||||
console.log('[secrets] vault parse error:', e)
|
||||
}
|
||||
}
|
||||
loaded = true
|
||||
})()
|
||||
}
|
||||
await loadPromise
|
||||
}
|
||||
|
||||
async function persist(): Promise<void> {
|
||||
const raw = JSON.stringify(Object.fromEntries(cache))
|
||||
writeChain = writeChain.then(() =>
|
||||
Bun.secrets.set({
|
||||
service: VAULT_SERVICE,
|
||||
name: VAULT_NAME,
|
||||
value: raw,
|
||||
}),
|
||||
)
|
||||
return writeChain
|
||||
}
|
||||
|
||||
export async function getSecret(
|
||||
service: string,
|
||||
name: string,
|
||||
): Promise<string | null> {
|
||||
const k = key(service, name)
|
||||
await ensureLoaded()
|
||||
return cache.get(k) ?? null
|
||||
}
|
||||
|
||||
export async function setSecret(
|
||||
service: string,
|
||||
name: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
await ensureLoaded()
|
||||
cache.set(key(service, name), value)
|
||||
await persist()
|
||||
}
|
||||
|
||||
export async function deleteSecret(
|
||||
service: string,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
if (service === VAULT_SERVICE && name === VAULT_NAME) {
|
||||
cache.clear()
|
||||
loaded = false
|
||||
loadPromise = null
|
||||
await Bun.secrets.delete({ service, name })
|
||||
return
|
||||
}
|
||||
await ensureLoaded()
|
||||
cache.delete(key(service, name))
|
||||
await persist()
|
||||
}
|
||||
|
||||
export function clearSecretsCache(): void {
|
||||
cache.clear()
|
||||
loaded = false
|
||||
loadPromise = null
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
XrpTxEntry,
|
||||
XrpWalletInfo,
|
||||
} from '../../../lib/rpc-schema'
|
||||
import { getSecret, setSecret } from '../secrets-cache'
|
||||
|
||||
const SERVER = 'wss://s1.ripple.com'
|
||||
const RIPPLE_EPOCH = 946684800
|
||||
@@ -54,7 +55,7 @@ export function walletForSigning(secret: string): Wallet {
|
||||
}
|
||||
|
||||
export async function getXrpWalletList(): Promise<XrpWalletInfo[]> {
|
||||
const raw = await Bun.secrets.get({ service: SERVICE, name: WALLET_LIST_KEY })
|
||||
const raw = await getSecret(SERVICE, WALLET_LIST_KEY)
|
||||
if (!raw) return []
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
@@ -66,11 +67,7 @@ export async function getXrpWalletList(): Promise<XrpWalletInfo[]> {
|
||||
export async function saveXrpWalletList(
|
||||
wallets: XrpWalletInfo[],
|
||||
): Promise<void> {
|
||||
await Bun.secrets.set({
|
||||
service: SERVICE,
|
||||
name: WALLET_LIST_KEY,
|
||||
value: JSON.stringify(wallets),
|
||||
})
|
||||
await setSecret(SERVICE, WALLET_LIST_KEY, JSON.stringify(wallets))
|
||||
}
|
||||
|
||||
export async function getBalance(address: string): Promise<XrpBalance> {
|
||||
|
||||
Binary file not shown.
@@ -10,6 +10,7 @@ import {
|
||||
getFee,
|
||||
} from '../../lib/xrp'
|
||||
import type { XrpWalletInfo } from '../../../lib/rpc-schema'
|
||||
import { getSecret, setSecret, deleteSecret } from '../../lib/secrets-cache'
|
||||
|
||||
const SERVICE = 'koins'
|
||||
|
||||
@@ -30,7 +31,7 @@ export function createXrpHandlers() {
|
||||
const { address, seed } = generateWallet()
|
||||
const id = crypto.randomUUID()
|
||||
const vaultKey = `xrp_seed_${id}`
|
||||
await Bun.secrets.set({ service: SERVICE, name: vaultKey, value: seed })
|
||||
await setSecret(SERVICE, vaultKey, seed)
|
||||
const wallets = await getXrpWalletList()
|
||||
const createdAt = new Date().toISOString()
|
||||
const info: XrpWalletInfo = {
|
||||
@@ -58,11 +59,7 @@ export function createXrpHandlers() {
|
||||
const { address } = walletFromSecret(secret)
|
||||
const id = crypto.randomUUID()
|
||||
const vaultKey = `xrp_seed_${id}`
|
||||
await Bun.secrets.set({
|
||||
service: SERVICE,
|
||||
name: vaultKey,
|
||||
value: secret.trim(),
|
||||
})
|
||||
await setSecret(SERVICE, vaultKey, secret.trim())
|
||||
const wallets = await getXrpWalletList()
|
||||
const createdAt = new Date().toISOString()
|
||||
const info: XrpWalletInfo = {
|
||||
@@ -83,15 +80,15 @@ export function createXrpHandlers() {
|
||||
const wallet = wallets.find((w) => w.id === id)
|
||||
if (!wallet) throw new Error('Wallet not found')
|
||||
await Promise.all([
|
||||
Bun.secrets.delete({ service: SERVICE, name: wallet.vaultKey }),
|
||||
Bun.secrets.delete({ service: SERVICE, name: `xrp_auth_${id}` }),
|
||||
deleteSecret(SERVICE, wallet.vaultKey),
|
||||
deleteSecret(SERVICE, `xrp_auth_${id}`),
|
||||
])
|
||||
await saveXrpWalletList(wallets.filter((w) => w.id !== id))
|
||||
console.log('[rpc] xrpDeleteWallet complete')
|
||||
},
|
||||
xrpGetSeed: async ({ vaultKey }: { vaultKey: string }) => {
|
||||
console.log('[rpc] xrpGetSeed:', vaultKey)
|
||||
const seed = await Bun.secrets.get({ service: SERVICE, name: vaultKey })
|
||||
const seed = await getSecret(SERVICE, vaultKey)
|
||||
if (!seed) throw new Error('Seed not found in keychain')
|
||||
return seed
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user