Files
amethyst/cli/tests/headless/helpers.sh
T
Claude b507cb5986 feat(cli): move private key out of identity.json into keychain / NIP-49
identity.json previously stored `privKeyHex` and `nsec` as plaintext fields.
0600 file perms keep other OS users out, but not another app running as the
same user — and that is the threat model the CLI actually cares about.

Introduce a SecretStore indirection:

 * identity.json now persists only the public parts plus a typed
   `secret: IdentitySecret` envelope (keychain | ncryptsec | plaintext).
 * macOS uses `/usr/bin/security` (Keychain ACLs bind the item to the binary
   that stored it, so other same-user apps need user consent).
 * Linux uses `secret-tool` if a Secret Service is running on the session
   D-Bus; the gain there is at-rest encryption while the keyring is locked.
 * On any platform without a keychain, auto falls back to NIP-49
   (scrypt + XChaCha20) with the passphrase read from --passphrase-file,
   then $AMY_PASSPHRASE, then a TTY prompt. Another same-user app can read
   the blob but cannot decrypt it without the passphrase.
 * `--secret-backend=plaintext` is an explicit opt-in for dev scripts and
   the interop test harness.

Legacy identity.json files that still carry top-level privKeyHex/nsec are
read transparently and auto-migrate on the next save.

whoami, `create` / `login` existence checks, and init-re-run now use a
metadata-only load path so they do not trigger a keychain prompt or ask
for a passphrase just to echo the npub.

Tests: cli/tests/ setups wire --secret-backend=plaintext through the amy_a
/ amy_d wrappers so headless CI runs do not stall on a TTY passphrase
prompt.

https://claude.ai/code/session_01SqdMfLdXvb3GskFLcEj739
2026-04-24 21:06:18 +00:00

59 lines
2.0 KiB
Bash

# shellcheck shell=bash
#
# helpers.sh — thin wrappers that keep the per-test code tight.
# --- amy wrapper -------------------------------------------------------------
# `--secret-backend=plaintext` keeps these throwaway interop runs headless —
# the default `auto` would try the OS keychain (not available in CI) and then
# ask for a NIP-49 passphrase. Plaintext still writes 0600-owner-only.
amy_a() { "$AMY_BIN" --data-dir "$A_DIR" --secret-backend plaintext "$@"; }
# Run amy, log stderr, surface JSON on stdout, remember last result.
amy_json() {
local out
if ! out=$(amy_a "$@" 2>>"$LOG_FILE"); then
fail_msg "amy $*: exit $? (see $LOG_FILE)"
printf '%s\n' "$out" >>"$LOG_FILE"
return 1
fi
printf '%s' "$out"
}
# Convenience extractors — the CLI emits one JSON object per success so we can
# jq with impunity.
amy_field() {
# usage: amy_field '.group_id' init [args...]
local path="$1"; shift
amy_json "$@" | jq -r "$path"
}
# --- assertion helpers -------------------------------------------------------
# Assert a substring is present in a variable; append a failed result on miss
# and return 1. Positive case just logs.
assert_contains() {
local haystack="$1" needle="$2" test_id="$3" note="${4:-}"
if [[ "$haystack" == *"$needle"* ]]; then
info "assertion hit: $test_id contains \"$needle\""
return 0
fi
fail_msg "$test_id: missing \"$needle\" (${note:-no note})"
info "actual: $haystack"
record_result "$test_id" fail "${note:-missing \"$needle\"}"
return 1
}
# Assert two strings are equal (leniently trimmed).
assert_eq() {
local actual="$1" expected="$2" test_id="$3" note="${4:-}"
if [[ "${actual// /}" == "${expected// /}" ]]; then
info "assertion hit: $test_id \"$actual\" == \"$expected\""
return 0
fi
fail_msg "$test_id: expected \"$expected\", got \"$actual\" (${note:-})"
record_result "$test_id" fail "${note:-mismatch}"
return 1
}
# --- wn-side pollers (delegates to lib.sh) -----------------------------------
# Both exist in lib.sh already; this file only adds headless-specific niceties.