8d823000d1
- digest queue no longer dropped on failed dispatch (retry next tick) - collapsed reminders marked fired/rescheduled only after digest delivers - /tools step-up gate skipped when WebAuthn is not configured (was 403 forever) - passkey credential store rolls back memory on persist failure - auth_test fake updated for TickTrace (branch build break) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASstMtsZWLSRcD1Tq8T68Q
90 lines
2.0 KiB
Go
90 lines
2.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
)
|
|
|
|
type credentialStore struct {
|
|
mu sync.RWMutex
|
|
path string
|
|
creds map[string]localCred
|
|
}
|
|
|
|
func newCredentialStore(path string) (*credentialStore, error) {
|
|
cs := &credentialStore{
|
|
path: path,
|
|
creds: make(map[string]localCred),
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return cs, nil
|
|
}
|
|
return nil, fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
if len(data) > 0 {
|
|
if err := json.Unmarshal(data, &cs.creds); err != nil {
|
|
return nil, fmt.Errorf("parse %s: %w", path, err)
|
|
}
|
|
}
|
|
return cs, nil
|
|
}
|
|
|
|
func (cs *credentialStore) persist() error {
|
|
data, err := json.MarshalIndent(cs.creds, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshal: %w", err)
|
|
}
|
|
if err := os.WriteFile(cs.path, data, 0600); err != nil {
|
|
return fmt.Errorf("write %s: %w", cs.path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cs *credentialStore) Save(id string, publicKey []byte) error {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
if _, exists := cs.creds[id]; exists {
|
|
return fmt.Errorf("credential already exists")
|
|
}
|
|
cs.creds[id] = localCred{PublicKey: publicKey}
|
|
if err := cs.persist(); err != nil {
|
|
// roll back so memory matches disk — otherwise the credential looks
|
|
// registered until restart, then silently vanishes, and a retry hits
|
|
// "already exists".
|
|
delete(cs.creds, id)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cs *credentialStore) Lookup(id string) (publicKey []byte, signCount int64, err error) {
|
|
cs.mu.RLock()
|
|
defer cs.mu.RUnlock()
|
|
cred, ok := cs.creds[id]
|
|
if !ok {
|
|
return nil, 0, fmt.Errorf("credential not found")
|
|
}
|
|
return cred.PublicKey, cred.SignCount, nil
|
|
}
|
|
|
|
func (cs *credentialStore) UpdateSignCount(id string, count int64) error {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
prev, ok := cs.creds[id]
|
|
if !ok {
|
|
return fmt.Errorf("credential not found")
|
|
}
|
|
cred := prev
|
|
cred.SignCount = count
|
|
cs.creds[id] = cred
|
|
if err := cs.persist(); err != nil {
|
|
cs.creds[id] = prev // keep memory consistent with disk
|
|
return err
|
|
}
|
|
return nil
|
|
}
|