Codex-ready backlog for the standalone embedded signer infrastructure (no marketplace/games/product language).
Scope
- Create repo as a TS library (not an app)
- Tooling: pnpm, tsup (or Vite lib mode), eslint, prettier, vitest
- Folder layout:
src/
crypto/
vault/
storage/
auth/
sessions/
permissions/
signer/
types/
index.ts
test/
Definition of Done
pnpm i && pnpm lint && pnpm test && pnpm buildsucceed- Build outputs ESM + CJS + types
Notes
.npmrcpins the registry to https://registry.npmmirror.com to keep installs working in this environment.
Scope
AGENTS.mdwith hard rules:- no TODOs
- no “optional”
- no placeholder crypto
- all public functions typed
SECURITY.mdwith invariants:- no plaintext secrets at rest
- decrypt only in memory
- strict origin binding
- canonical challenge serialization
- no blind signing (challenge must be structured)
- Add CI check to fail on TODO/FIXME strings
Definition of Done
- Docs exist and match implementation
- CI fails on TODO/FIXME strings
Notes
AGENTS.md+SECURITY.mddocument the guardrails and invariants.pnpm lintnow runsscripts/check-no-todo.mjs, which scans source/config files (non-docs) for TODO/FIXME strings.
Scope
- Create
src/types/core.tsdefining:Hex32,Hex64,B64Origin(normalizedscheme://host:port)VaultBlobV1ChallengeV1Scope,PermissionRecord,SessionRecordUnsignedEvent,SignedEvent(generic event signing)
Definition of Done
- Types exported from
src/index.ts - Compile passes strict TypeScript
Notes
- Core type exports live in
src/types/core.tsand are re-exported viasrc/index.ts. test/core-types.test.tsinstantiates representative values so Vitest protects against accidental shape regressions.
Scope
- Create
src/types/canonical.tswith:canonicalJsonStringify(obj)(stable keys, no whitespace)utf8Encode(str)helper
Definition of Done
- Unit test: identical objects stringify identically
- Unit test: key order differences don’t change output
Notes
src/types/canonical.tsincludescanonicalJsonStringify+utf8Encodeplus exportedJsonValuehelper type.test/canonical.test.tscovers determinism, key ordering, and utf8 encoding behavior.
Scope
sha256(bytes) -> Uint8Arraysha256Hex(utf8String) -> hex
Definition of Done
- Unit tests against known SHA-256 outputs
Notes
src/crypto/sha256.tsexposessha256(bytes)andsha256Hex(string)(Hex32, 32-byte digest) and is re-exported viasrc/index.ts.test/sha256.test.tsuses standard vectors (empty string,abc, pangram) to lock in both byte + hex outputs.
Scope
- Using
@noble/secp256k1implement:schnorrSign(hash32Hex, privkeyHex) -> sigHexschnorrVerify(hash32Hex, sigHex, pubkeyHex) -> booleanpubkeyFromPrivkey(privkeyHex) -> pubkeyHex(x-only 32 bytes)
Definition of Done
- Tests include:
- 1 valid BIP340 vector
- 1 invalid verify vector
- Verify returns false on malformed lengths
Notes
src/crypto/schnorr.tsimplementsschnorrSign,schnorrVerify, andpubkeyFromPrivkeywith strict hex validation and optional deterministic aux randomness for tests.test/schnorr.test.tsencodes the BIP340 reference vector (all-zero hash/private + zero aux) plus tampered / malformed cases.
Scope
randomBytes(n)usingcrypto.getRandomValues(browser)- Add fallback error if not available
Definition of Done
- Test in jsdom environment verifies correct length and non-zero probability (basic sanity)
Notes
src/crypto/random.tsexposesrandomBytes(length)that enforces integer length and requirescrypto.getRandomValues.test/random.test.tspolyfillsglobalThis.cryptovia Nodewebcryptoand asserts length + non-zero bytes; also covers error paths (missing crypto, invalid lengths).
Scope
- Choose a WASM Argon2id library usable in browser
- Implement:
deriveKey(password, salt, params) -> CryptoKey/bytes- PBKDF2-SHA256 fallback if Argon2 not available (explicit flag)
Definition of Done
- Unit test: same inputs → same derived key
- Unit test: different salt → different derived key
- KDF params persisted in vault blob
Notes
- Tests mock
argon2-browserto keep the suite fast; production builds continue using the real WASM implementation.
Scope
encryptAesGcm(keyBytes, plaintextBytes, iv) -> ciphertextBytesdecryptAesGcm(...)throws on failure
Definition of Done
- Roundtrip test passes
- Wrong key fails
- Wrong IV fails
Notes
- Helpers live in
src/vault/aes.tsand leverage WebCrypto; tests rely on Node'swebcryptoshim.
Scope
- Implement
VaultBlobV1:
{
"version": 1,
"pubkey": "hex",
"kdf": { "algo": "argon2id|pbkdf2", "params": {...} },
"salt": "base64",
"iv": "base64",
"ciphertext": "base64"
}- Ciphertext contains canonical JSON payload:
{ "v": 1, "privkey": "hex", "created_at": 1700000000 }Definition of Done
- Encode/decode roundtrip
- Blob includes version + KDF params
- No plaintext secret in blob
Notes
src/vault/blob.tsexposes encode/decode helpers plus base64 utilities so later vault APIs can reuse the same logic; payload JSON is always canonicalized before encryption.
Scope
createIdentity(password, meta?) -> { pubkey, vaultBlob }unlockIdentity(password, vaultBlob) -> { pubkey, privkey }changePassword(oldPw, newPw, vaultBlob) -> vaultBlob
Definition of Done
- Unlock with correct password works
- Unlock with wrong password fails
- Password change preserves pubkey
Notes
src/vault/api.tsimplementscreateIdentity,unlockIdentity, andchangePasswordatop the Argon2/PBKDF2 + AES helpers; tests exercise success/wrong password + password rotation flows with deterministic Argon params.
Scope
- Implement
StorageAdapterinterface andIndexedDbAdapter - Tables:
- vaults (id, pubkey, vaultBlob, created_at, updated_at)
- permissions (origin, pubkey, scopes, created_at, updated_at)
- sessions (session_id, origin, pubkey, scopes, expires_at, revoked_at?)
Definition of Done
- CRUD tests run in fake-indexeddb env
- Migrations versioned (v1)
Notes
IndexedDbAdapterinsrc/storage/indexeddb.tsimplements theStorageAdapterinterface with v1 stores for vaults, permissions, and sessions;test/storage-indexeddb.test.tsexercises CRUD via fake-indexeddb.
Scope
normalizeOrigin(inputUrlOrOrigin) -> Origin- Rules:
- exact
scheme://host:port - if port missing: infer 443 for https, 80 for http
- exact
Definition of Done
- Tests cover:
- https default port
- http default port
- preserves explicit ports
- rejects invalid inputs
Notes
normalizeOriginlives insrc/auth/origin.tsand is exported viasrc/index.ts;test/origin.test.tsverifies canonicalization, custom ports, localhost support, and invalid inputs.
Scope
ChallengeV1:{ "v": 1, "origin": "scheme://host:port", "nonce": "base64|hex", "timestamp": 1700000000 }createChallenge(origin) -> ChallengeV1(nonce random, timestamp now)hashChallenge(challenge) -> hash32Hexusing canonical JSON stringify + sha256
Definition of Done
- Unit tests:
- hashing is deterministic
- changing any field changes hash
Notes
src/auth/challenge.tsexposescreateChallengeandhashChallenge, usingnormalizeOrigin, canonical JSON, and SHA-256;test/challenge.test.tscovers deterministic hashing, tamper detection, and normalized challenge creation via injected entropy.
Scope
signChallenge(privkey, challenge) -> sigverifyChallenge(pubkey, challenge, sig) -> boolean
Definition of Done
- Valid signature verifies true
- Tampered challenge fails verification
Notes
signChallenge/verifyChallengelive insrc/auth/challenge.ts, reuse canonical hashing + Schnorr helpers, and are covered bytest/challenge-sign.test.tsfor success, tamper, and invalid signature cases.
Scope
- Implement
Scopeunion:auth.sign_challengenostr.sign_eventwithkinds: number[]session.refresh
- Validation:
- reject unknown types
- reject empty kind arrays for
nostr.sign_event
Definition of Done
- Unit tests for validation
- Unit tests for “is scope allowed” matcher
Notes
- Scope validation + matcher live in
src/permissions/scope.tswith helpers exported via the main entry point;test/permissions-scope.test.tscovers happy paths and failure cases for validation plus kind matching logic.
Scope
- Implement callback interface
ConsentUI.requestApproval(origin, pubkey, requestedScopes) -> approvedScopes | throw - Library persists approved scopes in storage
- Default implementation throws “no consent UI provided”
Definition of Done
- Tests use a mock consent UI to approve scopes and persist them
Notes
PermissionManager+ConsentUIlive insrc/permissions/consent.ts; it normalizes origins, validates/matches scopes, persists grants via the storage adapter, and throws when no consent UI is configured.test/permissions-consent.test.tscovers consent-required errors, persistence, and insufficient approvals.
Scope
createSession(origin, pubkey, scopes, ttlSeconds) -> SessionRecord- Persist to storage
- Enforce origin-bound sessions
Definition of Done
- Session created and stored
- Fetching by id returns correct record
Notes
- Tests:
pnpm test test/sessions-manager.test.ts
Scope
requireSession(origin, pubkey, requiredScope) -> SessionRecord- Reject missing/expired/revoked sessions
- Reject scope mismatch
Definition of Done
- Tests cover:
- expired denied
- revoked denied
- wrong origin denied
- missing scope denied
Notes
SessionManager.requireSessionhandles origin/pubkey binding, expiry, revocation, and scope checks;test/sessions-manager.test.tsincludes coverage for success and all rejection paths.
Scope
refreshSession(session_id):- requires
session.refreshscope - re-issues expiry (sliding window)
- uses re-signed
ChallengeV1 - add
shouldRefresh(session, now)helper (refresh when <20% TTL remains)
- requires
Definition of Done
- Refresh extends expiry
- Refresh fails if locked / no signer available
- Refresh signs new challenge and verifies internally
Notes
- Tests:
pnpm test test/sessions-manager.test.ts
Scope
- Implement:
serializeEventForId([0, pubkey, created_at, kind, tags, content])getEventId(unsigned) -> idsignEvent(privkey, unsigned) -> SignedEvent- Ensure id always recomputed internally
Definition of Done
- Test vector for event ID hashing
- Signed event verifies with schnorr verify over id hash
Notes
- Tests:
pnpm test test/event-signing.test.ts
Scope
- Constructor requires:
storage: StorageAdapterconsentUI?: ConsentUItimeProvider?: () => number(for tests)
- Public methods:
createIdentity({ password, username? }) -> pubkeyimportIdentityMnemonic({ password, mnemonic }) -> pubkeyimportIdentityKeyfile({ password, keyfileBlob }) -> pubkeyexportEncryptedKeyfile({ password }) -> keyfileBlobexportMnemonic({ password }) -> mnemonicunlock({ password })lock()getPublicKey()requestScopes({ origin, scopes })signChallenge({ origin, challenge })signEvent({ origin, unsignedEvent, requiredKinds })createSession({ origin, scopes, ttl })refreshSession({ origin, sessionId })revokeSession({ origin, sessionId })
Definition of Done
- Locked signer refuses signing/refresh
- Origin normalized everywhere
- Permission checks enforced for every privileged method
Notes
- Tests:
pnpm test test/embedded-signer.test.ts test/mnemonic.test.ts
Scope
- BIP340 valid + invalid vectors
- SHA-256 vectors
Definition of Done
- All pass in CI
Scope
- create/unlock/change password
- wrong password fails
- vault blob contains no plaintext secret
Definition of Done
- Passes
Scope
- challenge hash determinism
- verify fails on tamper
- refresh extends expiry
- refresh denied if expired/revoked/no scope
Definition of Done
- Passes
Scope
- approval persists
- origin separation
- scope enforcement
Definition of Done
- Passes
Scope
- Add
examples/with a tiny script showing:- create identity
- unlock
- create challenge
- sign + verify
- create session + refresh
- Command:
pnpm example
Definition of Done
- Example runs and prints expected outputs
If you share your preferred environment target (browser-only vs browser+node) we can tighten the KDF/crypto choices accordingly (Argon2 wasm packaging differs).