// Package store is the in-memory mock data layer for the OpenYield web UI. // // It instantiates the real x/*/types structs (Reach, Stash, StashActivity) // from in-memory fixtures and provides create/get/list methods. This is the // app-layer consumption of protocol types (D-070), NOT a cross-x/ production // import — web/ is NOT an x/ module. No keeper, no Cosmos runtime, no app.go // (G-003 boundary enforced by import_test.go / G-025). package store import ( "fmt" "strings" "sync" "time" identitytypes "github.com/oy/openyield/x/identity/types" stashtypes "github.com/oy/openyield/x/stash/types" windowtypes "github.com/oy/openyield/x/window/types" ) // seedBalanceGrain is the test balance seeded to a new Stash at signup (D-071 // example: 500000 Grain = 50 Bread per GrainsPerBread=10000). const seedBalanceGrain int64 = 500000 // Store is the in-memory mock store. All methods are goroutine-safe (mu). type Store struct { mu sync.Mutex reaches map[string]identitytypes.Reach stashes map[string]stashtypes.Stash stashActivities map[string]stashtypes.StashActivity windows map[string]windowtypes.Window auditLogs map[string][]windowtypes.AuditEntry } // NewStore constructs a Store seeded from fixtures (fixtures.go). func NewStore() *Store { s := &Store{ reaches: map[string]identitytypes.Reach{}, stashes: map[string]stashtypes.Stash{}, stashActivities: map[string]stashtypes.StashActivity{}, windows: map[string]windowtypes.Window{}, auditLogs: map[string][]windowtypes.AuditEntry{}, } s.seed() return s } // CreateReach atomically creates a Reach (IsNomad=true) + a Stash (D-071). // G-027: HolderID and PublicKey are validated (non-empty, <=128 bytes, no // path separators, no template syntax) before any map write. Returns the // created Reach + Stash. func (s *Store) CreateReach(holderID, publicKey string) (identitytypes.Reach, stashtypes.Stash, error) { if err := validateReachInput(holderID, publicKey); err != nil { return identitytypes.Reach{}, stashtypes.Stash{}, err } s.mu.Lock() defer s.mu.Unlock() if _, dup := s.reaches[holderID]; dup { return identitytypes.Reach{}, stashtypes.Stash{}, fmt.Errorf("holder %q already has a Reach", holderID) } now := time.Now().Unix() reachID := "reach-" + holderID stashID := "stash-" + holderID reach := identitytypes.Reach{ ReachID: reachID, HolderID: holderID, CreatedAt: now, PublicKey: publicKey, IsNomad: true, } stash := stashtypes.Stash{ HolderID: holderID, StashID: stashID, CreatedAt: now, LastActive: now, BalanceGrain: seedBalanceGrain, } activity := stashtypes.StashActivity{ StashID: stashID, ActiveDays: 1, MaxGapDays: 1, LastActivityDay: now, } s.reaches[holderID] = reach s.stashes[holderID] = stash s.stashActivities[stashID] = activity return reach, stash, nil } // ListReaches returns all seeded + created Reaches. func (s *Store) ListReaches() []identitytypes.Reach { s.mu.Lock() defer s.mu.Unlock() out := make([]identitytypes.Reach, 0, len(s.reaches)) for _, r := range s.reaches { out = append(out, r) } return out } // GetReach returns the Reach for a holderID (by HolderID, the stable key). func (s *Store) GetReach(holderID string) (identitytypes.Reach, bool) { s.mu.Lock() defer s.mu.Unlock() r, ok := s.reaches[holderID] return r, ok } // GetStash returns the Stash for a holderID. func (s *Store) GetStash(holderID string) (stashtypes.Stash, bool) { s.mu.Lock() defer s.mu.Unlock() st, ok := s.stashes[holderID] return st, ok } // GetStashActivity returns the StashActivity for a stashID. func (s *Store) GetStashActivity(stashID string) (stashtypes.StashActivity, bool) { s.mu.Lock() defer s.mu.Unlock() a, ok := s.stashActivities[stashID] return a, ok } // OpenWindow creates a new Window in the Open status (REQ-042) with an initial // AuditEntry. Returns the created Window. The Window is keyed by a generated // windowID derived from the grantor + a counter (mock; not cryptographic). func (s *Store) OpenWindow(grantorHolder, grantee string, scope windowtypes.Scope, start, end int64, rateLimit windowtypes.RateLimit) (windowtypes.Window, error) { if grantorHolder == "" { return windowtypes.Window{}, fmt.Errorf("grantor holder is required") } if grantee == "" { return windowtypes.Window{}, fmt.Errorf("grantee is required") } s.mu.Lock() defer s.mu.Unlock() windowID := fmt.Sprintf("window-%s-%d", grantorHolder, len(s.windows)+1) now := time.Now().Unix() w := windowtypes.Window{ WindowID: windowID, GrantorHolder: grantorHolder, Grantee: grantee, Scope: scope, Start: start, End: end, RateLimit: rateLimit, Status: windowtypes.StatusOpen, } s.windows[windowID] = w entry := windowtypes.AuditEntry{ EntryID: windowID + "-audit-1", Timestamp: now, Action: "open", Result: "created", GranterRef: grantorHolder, } s.auditLogs[windowID] = []windowtypes.AuditEntry{entry} w.AuditLogRefs = []string{entry.EntryID} s.windows[windowID] = w return w, nil } // ActivateWindow transitions a Window from Open to Active by calling the real // x/window/types.Window.Activate() method (not a reimplementation). Appends an // AuditEntry. Returns an error if the Window is not in the Open status. func (s *Store) ActivateWindow(windowID string) error { s.mu.Lock() defer s.mu.Unlock() w, ok := s.windows[windowID] if !ok { return fmt.Errorf("window %q not found", windowID) } if err := w.Activate(); err != nil { return err } s.windows[windowID] = w s.appendAuditLocked(windowID, "activate", "active", w.GrantorHolder) return nil } // RevokeWindow transitions a Window to Revoked by calling the real // x/window/types.Window.Revoke() method. Idempotent on already-revoked; // no-op on Expired (terminal state wins — v0.2 type contract). Appends an // AuditEntry only if the status actually changed. func (s *Store) RevokeWindow(windowID string) error { s.mu.Lock() defer s.mu.Unlock() w, ok := s.windows[windowID] if !ok { return fmt.Errorf("window %q not found", windowID) } prevStatus := w.Status if err := w.Revoke(); err != nil { return err } s.windows[windowID] = w if w.Status != prevStatus { s.appendAuditLocked(windowID, "revoke", "revoked", w.GrantorHolder) } return nil } // ExpireWindow transitions a Window to Expired by calling the real // x/window/types.Window.Expire() method. Appends an AuditEntry. func (s *Store) ExpireWindow(windowID string) error { s.mu.Lock() defer s.mu.Unlock() w, ok := s.windows[windowID] if !ok { return fmt.Errorf("window %q not found", windowID) } prevStatus := w.Status w.Expire() s.windows[windowID] = w if w.Status != prevStatus { s.appendAuditLocked(windowID, "expire", "expired", w.GrantorHolder) } return nil } // ListWindows returns all Windows for a grantor holder. func (s *Store) ListWindows(grantorHolder string) []windowtypes.Window { s.mu.Lock() defer s.mu.Unlock() out := []windowtypes.Window{} for _, w := range s.windows { if w.GrantorHolder == grantorHolder { out = append(out, w) } } return out } // GetWindow returns the Window for a windowID. func (s *Store) GetWindow(windowID string) (windowtypes.Window, bool) { s.mu.Lock() defer s.mu.Unlock() w, ok := s.windows[windowID] return w, ok } // GetAuditLog returns the audit-log entries for a windowID. func (s *Store) GetAuditLog(windowID string) []windowtypes.AuditEntry { s.mu.Lock() defer s.mu.Unlock() return s.auditLogs[windowID] } // appendAuditLocked appends an AuditEntry to the window's audit log. Caller // MUST hold s.mu. func (s *Store) appendAuditLocked(windowID, action, result, granterRef string) { logs := s.auditLogs[windowID] now := time.Now().Unix() entry := windowtypes.AuditEntry{ EntryID: fmt.Sprintf("%s-audit-%d", windowID, len(logs)+1), Timestamp: now, Action: action, Result: result, GranterRef: granterRef, } s.auditLogs[windowID] = append(logs, entry) w := s.windows[windowID] w.AuditLogRefs = append(w.AuditLogRefs, entry.EntryID) s.windows[windowID] = w } // validateReachInput enforces G-027: HolderID and PublicKey must be non-empty, // <=128 bytes, and contain no path separators or template syntax. This is a // prototype-robustness gate (the mock store uses holderID as a map key). func validateReachInput(holderID, publicKey string) error { if holderID == "" { return fmt.Errorf("holder id is required") } if len(holderID) > 128 { return fmt.Errorf("holder id too long (max 128)") } if strings.ContainsAny(holderID, "/\\") { return fmt.Errorf("holder id must not contain path separators") } if strings.Contains(holderID, "{{") { return fmt.Errorf("holder id must not contain template syntax") } if publicKey == "" { return fmt.Errorf("public key is required") } if len(publicKey) > 128 { return fmt.Errorf("public key too long (max 128)") } if strings.ContainsAny(publicKey, "/\\") { return fmt.Errorf("public key must not contain path separators") } return nil }