diff --git a/clavis/clavis-vault/_old/api/handlers.go b/clavis/clavis-vault/_old/api/handlers.go deleted file mode 100644 index 6e466dd..0000000 --- a/clavis/clavis-vault/_old/api/handlers.go +++ /dev/null @@ -1,1518 +0,0 @@ -package api - -import ( - "crypto/rand" - "crypto/sha256" - "encoding/base32" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "log" - "net/http" - "path/filepath" - "strconv" - "strings" - "sync" - "time" - - "github.com/go-chi/chi/v5" - "github.com/johanj/clavitor/lib" - "github.com/pquerna/otp/totp" -) - -// challenge holds an in-memory WebAuthn challenge. -type challenge struct { - Data []byte - Type string - CreatedAt time.Time -} - -// Handlers holds dependencies for HTTP handlers. -type Handlers struct { - Cfg *lib.Config - - mu sync.Mutex - challenges map[string]challenge // in-memory challenge store (key = hex of challenge bytes) -} - -// NewHandlers creates a new Handlers instance. -func NewHandlers(cfg *lib.Config) *Handlers { - h := &Handlers{Cfg: cfg, challenges: make(map[string]challenge)} - go func() { - for { - time.Sleep(60 * time.Second) - h.cleanChallenges() - } - }() - return h -} - -func (h *Handlers) storeChallenge(data []byte, typ string) { - h.mu.Lock() - defer h.mu.Unlock() - h.challenges[hex.EncodeToString(data)] = challenge{Data: data, Type: typ, CreatedAt: time.Now()} -} - -func (h *Handlers) consumeChallenge(data []byte, typ string) error { - h.mu.Lock() - defer h.mu.Unlock() - key := hex.EncodeToString(data) - c, ok := h.challenges[key] - if !ok || c.Type != typ || time.Since(c.CreatedAt) > 5*time.Minute { - return fmt.Errorf("challenge not found or expired") - } - delete(h.challenges, key) - return nil -} - -func (h *Handlers) cleanChallenges() { - h.mu.Lock() - defer h.mu.Unlock() - for k, c := range h.challenges { - if time.Since(c.CreatedAt) > 5*time.Minute { - delete(h.challenges, k) - } - } -} - -// db returns the vault DB for this request (from context, set by L1Middleware). -func (h *Handlers) db(r *http.Request) *lib.DB { - return DBFromContext(r.Context()) -} - -// vk returns the vault key for this request (from context, set by L1Middleware). -func (h *Handlers) vk(r *http.Request) []byte { - return VaultKeyFromContext(r.Context()) -} - -// agent returns the agent for this request (nil = vault owner via legacy L1). -func (h *Handlers) agent(r *http.Request) *lib.Agent { - return AgentFromContext(r.Context()) -} - -// l1Raw returns the raw (non-normalized) L1 key from the vault key context. -// Needed for minting agent tokens that embed L1. -func (h *Handlers) l1Raw(r *http.Request) []byte { - vk := h.vk(r) - if len(vk) == 16 { - return vk[:8] // first 8 bytes of normalized key = original L1 - } - return vk -} - -// requireOwner rejects agent requests. Returns true if blocked (caller should return). -// Admin operations are HUMANS ONLY — hardware key required, agents forbidden. -func (h *Handlers) requireOwner(w http.ResponseWriter, r *http.Request) bool { - if IsAgentRequest(r) { - ErrorResponse(w, http.StatusForbidden, "owner_only", "This operation requires web authentication") - return true - } - return false -} - -// filterByScope removes entries the agent cannot access. -func filterByScope(agent *lib.Agent, entries []lib.Entry) []lib.Entry { - if agent == nil || agent.AllAccess { - return entries - } - var filtered []lib.Entry - for _, e := range entries { - if lib.AgentCanAccess(agent, e.Scopes) { - filtered = append(filtered, e) - } - } - if filtered == nil { - return []lib.Entry{} - } - return filtered -} - -// --------------------------------------------------------------------------- -// Health & Auth -// --------------------------------------------------------------------------- - -// VaultInfo returns the vault ID for the current request. -func (h *Handlers) VaultInfo(w http.ResponseWriter, r *http.Request) { - db := h.db(r) - if db == nil { - ErrorResponse(w, http.StatusNotFound, "no_vault", "No vault initialized") - return - } - base := filepath.Base(db.DBPath) - vaultID := strings.TrimSuffix(base, ".db") - JSONResponse(w, http.StatusOK, map[string]string{"vault_id": vaultID}) -} - -// Version is set by main via ldflags at build time. -var Version = "dev" - -// Health returns server status. -func (h *Handlers) Health(w http.ResponseWriter, r *http.Request) { - var count int - if db := h.db(r); db != nil { - count, _ = lib.EntryCount(db) - } - JSONResponse(w, http.StatusOK, map[string]any{ - "status": "ok", - "version": Version, - "entries": count, - "time": time.Now().UTC().Format(time.RFC3339), - }) -} - -// Setup creates a session (test-only endpoint). -func (h *Handlers) Setup(w http.ResponseWriter, r *http.Request) { - session, err := lib.SessionCreate(h.db(r), h.Cfg.SessionTTL, lib.ActorWeb) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "session_failed", "Failed to create session") - return - } - - lib.AuditLog(h.db(r), &lib.AuditEvent{ - Action: "setup", - Actor: lib.ActorWeb, - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusOK, map[string]string{ - "token": session.Token, - }) -} - -// AuthStatus returns whether the vault is fresh (no credentials) or locked. -func (h *Handlers) AuthStatus(w http.ResponseWriter, r *http.Request) { - db := h.db(r) - if db == nil { - JSONResponse(w, http.StatusOK, map[string]any{"state": "fresh", "credentials": 0}) - return - } - count, err := lib.WebAuthnCredentialCount(db) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "db_error", "Failed to check vault status") - return - } - state := "fresh" - if count > 0 { - state = "locked" - } - JSONResponse(w, http.StatusOK, map[string]any{ - "state": state, - "credentials": count, - }) -} - -// rpID returns the RP ID for WebAuthn — the hostname from the Host header, port stripped. -func rpID(r *http.Request) string { - host := r.Host - if idx := strings.Index(host, ":"); idx != -1 { - host = host[:idx] - } - return host -} - -// hasValidSession checks if the request has a valid Bearer token. -func (h *Handlers) hasValidSession(r *http.Request) bool { - auth := r.Header.Get("Authorization") - if !strings.HasPrefix(auth, "Bearer ") { - return false - } - db := h.db(r) - if db == nil { - return false - } - session, err := lib.SessionGet(db, strings.TrimPrefix(auth, "Bearer ")) - return err == nil && session != nil -} - -// AuthRegisterBegin starts WebAuthn registration. -// First passkey: no auth needed (DB may not exist yet). Additional passkeys: valid session required. -func (h *Handlers) AuthRegisterBegin(w http.ResponseWriter, r *http.Request) { - db := h.db(r) - if db != nil { - count, _ := lib.WebAuthnCredentialCount(db) - if count > 0 && !h.hasValidSession(r) { - ErrorResponse(w, http.StatusForbidden, "already_registered", "Vault already has credentials. Authenticate first to add more.") - return - } - } - - challengeBytes := make([]byte, 32) - rand.Read(challengeBytes) - h.storeChallenge(challengeBytes, "register") - - JSONResponse(w, http.StatusOK, map[string]any{ - "publicKey": map[string]any{ - "challenge": challengeBytes, - "rp": map[string]string{"name": "Clavitor", "id": rpID(r)}, - "user": map[string]any{ - "id": []byte("clavitor-owner"), - "name": "vault-owner", - "displayName": "Vault Owner", - }, - "pubKeyCredParams": []map[string]any{ - {"type": "public-key", "alg": -7}, - {"type": "public-key", "alg": -257}, - }, - "authenticatorSelection": map[string]any{ - "residentKey": "preferred", - "userVerification": "required", - }, - "attestation": "none", - "extensions": map[string]any{ - "prf": map[string]any{}, - }, - }, - }) -} - -// AuthRegisterComplete finishes WebAuthn registration and creates a session. -func (h *Handlers) AuthRegisterComplete(w http.ResponseWriter, r *http.Request) { - db := h.db(r) - if db != nil { - count, _ := lib.WebAuthnCredentialCount(db) - if count > 0 && !h.hasValidSession(r) { - ErrorResponse(w, http.StatusForbidden, "already_registered", "Vault already has credentials. Authenticate first to add more.") - return - } - } - - var req struct { - Challenge []byte `json:"challenge"` - CredentialID []byte `json:"credential_id"` - PublicKey []byte `json:"public_key"` - PRFSalt []byte `json:"prf_salt"` - Name string `json:"name"` - L1Key []byte `json:"l1_key"` // first 8 bytes of master, for vault DB naming - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - - if err := h.consumeChallenge(req.Challenge, "register"); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_challenge", "Challenge verification failed") - return - } - - // First passkey → create DB - if db == nil && len(req.PublicKey) > 0 { - // DB named from L1 key: clavitor-XXXXXX (base64url of first 4 bytes, no extension) - var dbName string - if len(req.L1Key) >= 4 { - dbName = "clavitor-" + base64UrlEncode(req.L1Key[:4]) - } else { - // Fallback: derive from public key hash - hash := sha256.Sum256(req.PublicKey) - dbName = "clavitor-" + base64UrlEncode(hash[:4]) - } - dbPath := filepath.Join(h.Cfg.DataDir, dbName) - newDB, err := lib.OpenDB(dbPath) - if err != nil { - log.Printf("DB create failed: path=%s err=%v", dbPath, err) - ErrorResponse(w, http.StatusInternalServerError, "db_create_failed", "Failed to create vault database") - return - } - defer newDB.Close() - if err := lib.MigrateDB(newDB); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "db_migrate_failed", "Failed to initialize vault database") - return - } - db = newDB - log.Printf("Vault created: %s", dbPath) - } - - if req.Name == "" { - req.Name = "Primary Passkey" - } - cred := &lib.WebAuthnCredential{ - CredID: lib.HexID(lib.NewID()), - Name: req.Name, - CredentialID: req.CredentialID, - PublicKey: req.PublicKey, - PRFSalt: req.PRFSalt, - } - if err := lib.StoreWebAuthnCredential(db, cred); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "store_failed", "Failed to store credential") - return - } - - session, err := lib.SessionCreate(db, h.Cfg.SessionTTL, lib.ActorWeb) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "session_failed", "Failed to create session") - return - } - - lib.AuditLog(db, &lib.AuditEvent{ - Action: "register", - Actor: lib.ActorWeb, - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusCreated, map[string]any{ - "token": session.Token, - "cred_id": cred.CredID, - }) -} - -// AuthLoginBegin starts WebAuthn authentication. -func (h *Handlers) AuthLoginBegin(w http.ResponseWriter, r *http.Request) { - creds, err := lib.GetWebAuthnCredentials(h.db(r)) - if err != nil || len(creds) == 0 { - ErrorResponse(w, http.StatusNotFound, "no_credentials", "No credentials registered") - return - } - - challenge := make([]byte, 32) - rand.Read(challenge) - h.storeChallenge(challenge, "login") - - var allowCreds []map[string]any - var prfSalt []byte - for _, c := range creds { - allowCreds = append(allowCreds, map[string]any{ - "type": "public-key", - "id": c.CredentialID, - }) - if len(c.PRFSalt) > 0 { - prfSalt = c.PRFSalt - } - } - - prfExt := map[string]any{} - if len(prfSalt) > 0 { - prfExt["eval"] = map[string]any{"first": prfSalt} - } - - JSONResponse(w, http.StatusOK, map[string]any{ - "publicKey": map[string]any{ - "challenge": challenge, - "rpId": rpID(r), - "allowCredentials": allowCreds, - "userVerification": "required", - "extensions": map[string]any{"prf": prfExt}, - }, - }) -} - -// AuthLoginComplete finishes WebAuthn authentication and creates a session. -func (h *Handlers) AuthLoginComplete(w http.ResponseWriter, r *http.Request) { - var req struct { - Challenge []byte `json:"challenge"` - CredentialID []byte `json:"credential_id"` - SignCount int `json:"sign_count"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - - if err := h.consumeChallenge(req.Challenge, "login"); err != nil { - ErrorResponse(w, http.StatusUnauthorized, "invalid_challenge", "Challenge verification failed") - return - } - - cred, err := lib.GetWebAuthnCredentialByRawID(h.db(r), req.CredentialID) - if err != nil { - ErrorResponse(w, http.StatusUnauthorized, "unknown_credential", "Credential not recognized") - return - } - - // Cloned authenticator detection - if req.SignCount > 0 && req.SignCount <= cred.SignCount { - lib.AuditLog(h.db(r), &lib.AuditEvent{ - Action: "cloned_authenticator_warning", - Actor: lib.ActorWeb, - IPAddr: realIP(r), - }) - } - lib.UpdateWebAuthnSignCount(h.db(r), int64(cred.CredID), req.SignCount) - - session, err := lib.SessionCreate(h.db(r), h.Cfg.SessionTTL, lib.ActorWeb) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "session_failed", "Failed to create session") - return - } - - lib.AuditLog(h.db(r), &lib.AuditEvent{ - Action: "login", - Actor: lib.ActorWeb, - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusOK, map[string]any{ - "token": session.Token, - }) -} - -// --------------------------------------------------------------------------- -// Entry CRUD -// --------------------------------------------------------------------------- - -// ListEntries returns all entries, filtered by agent scope. -func (h *Handlers) ListEntries(w http.ResponseWriter, r *http.Request) { - agent := h.agent(r) - actor := ActorFromContext(r.Context()) - - // Metadata-only mode - if r.URL.Query().Get("meta") == "1" { - entries, err := lib.EntryListMeta(h.db(r)) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "list_failed", "Failed to list entries") - return - } - if entries == nil { - entries = []lib.Entry{} - } - entries = filterByScope(agent, entries) - JSONResponse(w, http.StatusOK, entries) - return - } - - var parent *int64 - if pidStr := r.URL.Query().Get("parent_id"); pidStr != "" { - pid, err := lib.HexToID(pidStr) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid parent_id") - return - } - parent = &pid - } - - entries, err := lib.EntryList(h.db(r), h.vk(r), parent) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "list_failed", "Failed to list entries") - return - } - if entries == nil { - entries = []lib.Entry{} - } - - entries = filterByScope(agent, entries) - - if actor == lib.ActorAgent { - for i := range entries { - if entries[i].VaultData != nil { - stripL2Fields(entries[i].VaultData) - } - } - } - - JSONResponse(w, http.StatusOK, entries) -} - -// GetEntry returns a single entry (scope-checked). -func (h *Handlers) GetEntry(w http.ResponseWriter, r *http.Request) { - agent := h.agent(r) - actor := ActorFromContext(r.Context()) - entryID, err := lib.HexToID(chi.URLParam(r, "id")) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid entry ID") - return - } - - entry, err := lib.EntryGet(h.db(r), h.vk(r), entryID) - if err == lib.ErrNotFound { - ErrorResponse(w, http.StatusNotFound, "not_found", "Entry not found") - return - } - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "get_failed", "Failed to get entry") - return - } - - if entry.DeletedAt != nil { - ErrorResponse(w, http.StatusNotFound, "deleted", "Entry has been deleted") - return - } - - if !lib.AgentCanAccess(agent, entry.Scopes) { - ErrorResponse(w, http.StatusForbidden, "forbidden", "Access denied") - return - } - - if actor == lib.ActorAgent && entry.VaultData != nil { - stripL2Fields(entry.VaultData) - } - - lib.AuditLog(h.db(r), &lib.AuditEvent{ - EntryID: entry.EntryID, - Title: entry.Title, - Action: lib.ActionRead, - Actor: actor, - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusOK, entry) -} - -// CreateEntry creates a new entry. -func (h *Handlers) CreateEntry(w http.ResponseWriter, r *http.Request) { - actor := ActorFromContext(r.Context()) - - var req struct { - Type string `json:"type"` - Title string `json:"title"` - ParentID lib.HexID `json:"parent_id"` - Data *lib.VaultData `json:"data"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - - if req.Title == "" { - ErrorResponse(w, http.StatusBadRequest, "missing_title", "Title is required") - return - } - if req.Type == "" { - req.Type = lib.TypeCredential - } - - entry := &lib.Entry{ - Type: req.Type, - Title: req.Title, - ParentID: req.ParentID, - DataLevel: lib.DataLevelL1, - VaultData: req.Data, - } - - if err := lib.EntryCreate(h.db(r), h.vk(r), entry); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "create_failed", "Failed to create entry") - return - } - - lib.AuditLog(h.db(r), &lib.AuditEvent{ - EntryID: entry.EntryID, - Title: entry.Title, - Action: lib.ActionCreate, - Actor: actor, - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusCreated, entry) -} - -// UpsertEntry creates or updates an entry by title match (case-insensitive). -// If an entry with the same title exists, it is updated. Otherwise, a new entry is created. -func (h *Handlers) UpsertEntry(w http.ResponseWriter, r *http.Request) { - actor := ActorFromContext(r.Context()) - - var req struct { - Type string `json:"type"` - Title string `json:"title"` - ParentID lib.HexID `json:"parent_id"` - Data *lib.VaultData `json:"data"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - if req.Title == "" { - ErrorResponse(w, http.StatusBadRequest, "missing_title", "Title is required") - return - } - if req.Type == "" { - req.Type = lib.TypeCredential - } - - // Search for existing entry with same title - existing, err := lib.EntrySearchFuzzy(h.db(r), h.vk(r), req.Title) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "search_failed", "Failed to search entries") - return - } - - // Find exact title match (case-insensitive) - var match *lib.Entry - for i := range existing { - if strings.EqualFold(existing[i].Title, req.Title) { - match = &existing[i] - break - } - } - - if match != nil { - // Update existing - match.Title = req.Title - match.Type = req.Type - match.ParentID = req.ParentID - if req.Data != nil { - match.VaultData = req.Data - } - if err := lib.EntryUpdate(h.db(r), h.vk(r), match); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "update_failed", "Failed to update entry") - return - } - lib.AuditLog(h.db(r), &lib.AuditEvent{ - EntryID: match.EntryID, - Title: match.Title, - Action: lib.ActionUpdate, - Actor: actor, - IPAddr: realIP(r), - }) - JSONResponse(w, http.StatusOK, match) - } else { - // Create new - entry := &lib.Entry{ - Type: req.Type, - Title: req.Title, - ParentID: req.ParentID, - DataLevel: lib.DataLevelL1, - VaultData: req.Data, - } - if err := lib.EntryCreate(h.db(r), h.vk(r), entry); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "create_failed", "Failed to create entry") - return - } - lib.AuditLog(h.db(r), &lib.AuditEvent{ - EntryID: entry.EntryID, - Title: entry.Title, - Action: lib.ActionCreate, - Actor: actor, - IPAddr: realIP(r), - }) - JSONResponse(w, http.StatusCreated, entry) - } -} - -// UpdateEntry updates an existing entry. -func (h *Handlers) UpdateEntry(w http.ResponseWriter, r *http.Request) { - actor := ActorFromContext(r.Context()) - entryID, err := lib.HexToID(chi.URLParam(r, "id")) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid entry ID") - return - } - - var req struct { - Type string `json:"type"` - Title string `json:"title"` - ParentID lib.HexID `json:"parent_id"` - Version int `json:"version"` - Data *lib.VaultData `json:"data"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - - // Get existing entry - existing, err := lib.EntryGet(h.db(r), h.vk(r), entryID) - if err == lib.ErrNotFound { - ErrorResponse(w, http.StatusNotFound, "not_found", "Entry not found") - return - } - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "get_failed", "Failed to get entry") - return - } - - // Update fields - if req.Title != "" { - existing.Title = req.Title - } - if req.Type != "" { - existing.Type = req.Type - } - existing.ParentID = req.ParentID - existing.Version = req.Version - if req.Data != nil { - existing.VaultData = req.Data - } - - if err := lib.EntryUpdate(h.db(r), h.vk(r), existing); err != nil { - if err == lib.ErrVersionConflict { - ErrorResponse(w, http.StatusConflict, "version_conflict", err.Error()) - return - } - ErrorResponse(w, http.StatusInternalServerError, "update_failed", "Failed to update entry") - return - } - - lib.AuditLog(h.db(r), &lib.AuditEvent{ - EntryID: existing.EntryID, - Title: existing.Title, - Action: lib.ActionUpdate, - Actor: actor, - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusOK, existing) -} - -// DeleteEntry soft-deletes an entry. -func (h *Handlers) DeleteEntry(w http.ResponseWriter, r *http.Request) { - actor := ActorFromContext(r.Context()) - entryID, err := lib.HexToID(chi.URLParam(r, "id")) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid entry ID") - return - } - - // Get entry for audit log - entry, _ := lib.EntryGet(h.db(r), h.vk(r), entryID) - - if err := lib.EntryDelete(h.db(r), entryID); err != nil { - if err == lib.ErrNotFound { - ErrorResponse(w, http.StatusNotFound, "not_found", "Entry not found") - return - } - ErrorResponse(w, http.StatusInternalServerError, "delete_failed", "Failed to delete entry") - return - } - - title := "" - if entry != nil { - title = entry.Title - } - - lib.AuditLog(h.db(r), &lib.AuditEvent{ - EntryID: lib.HexID(entryID), - Title: title, - Action: lib.ActionDelete, - Actor: actor, - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusOK, map[string]string{"status": "deleted"}) -} - -// --------------------------------------------------------------------------- -// Search -// --------------------------------------------------------------------------- - -// SearchEntries searches entries by title (scope-filtered). -func (h *Handlers) SearchEntries(w http.ResponseWriter, r *http.Request) { - agent := h.agent(r) - actor := ActorFromContext(r.Context()) - query := r.URL.Query().Get("q") - if query == "" { - ErrorResponse(w, http.StatusBadRequest, "missing_query", "Query parameter 'q' is required") - return - } - - entries, err := lib.EntrySearchFuzzy(h.db(r), h.vk(r), query) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "search_failed", "Search failed") - return - } - if entries == nil { - entries = []lib.Entry{} - } - - entries = filterByScope(agent, entries) - - if actor == lib.ActorAgent { - for i := range entries { - if entries[i].VaultData != nil { - stripL2Fields(entries[i].VaultData) - } - } - } - - JSONResponse(w, http.StatusOK, entries) -} - -// --------------------------------------------------------------------------- -// Password Generator -// --------------------------------------------------------------------------- - -// GeneratePassword generates a random password. -func (h *Handlers) GeneratePassword(w http.ResponseWriter, r *http.Request) { - lengthStr := r.URL.Query().Get("length") - length := 20 - if lengthStr != "" { - if l, err := strconv.Atoi(lengthStr); err == nil && l > 0 && l <= 128 { - length = l - } - } - - symbols := r.URL.Query().Get("symbols") != "false" - wordsParam := r.URL.Query().Get("words") - wordsN := 0 - if wordsParam == "true" { - wordsN = 4 - } else if n, err := strconv.Atoi(wordsParam); err == nil && n > 0 { - wordsN = n - } - - var password string - if wordsN > 0 { - password = generatePassphrase(wordsN) - } else { - password = generatePassword(length, symbols) - } - - JSONResponse(w, http.StatusOK, map[string]string{ - "password": password, - }) -} - -func generatePassword(length int, symbols bool) string { - const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - const digits = "0123456789" - const syms = "!@#$%^&*()_+-=[]{}|;:,.<>?" - - charset := letters + digits - if symbols { - charset += syms - } - - b := make([]byte, length) - rand.Read(b) - for i := range b { - b[i] = charset[int(b[i])%len(charset)] - } - return string(b) -} - -func generatePassphrase(words int) string { - wordList := []string{ - "correct", "horse", "battery", "staple", "cloud", "mountain", - "river", "forest", "castle", "dragon", "phoenix", "crystal", - "shadow", "thunder", "whisper", "harvest", "journey", "compass", - "anchor", "beacon", "bridge", "canyon", "desert", "empire", - } - b := make([]byte, words) - rand.Read(b) - parts := make([]string, words) - for i := range parts { - parts[i] = wordList[int(b[i])%len(wordList)] - } - return strings.Join(parts, "-") -} - -// --------------------------------------------------------------------------- -// Extension API -// --------------------------------------------------------------------------- - -// GetTOTP generates a live TOTP code for an entry (scope-checked). -func (h *Handlers) GetTOTP(w http.ResponseWriter, r *http.Request) { - agent := h.agent(r) - actor := ActorFromContext(r.Context()) - entryID, err := lib.HexToID(chi.URLParam(r, "id")) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid entry ID") - return - } - - entry, err := lib.EntryGet(h.db(r), h.vk(r), entryID) - if err == lib.ErrNotFound { - ErrorResponse(w, http.StatusNotFound, "not_found", "Entry not found") - return - } - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "get_failed", "Failed to get entry") - return - } - - if !lib.AgentCanAccess(agent, entry.Scopes) { - ErrorResponse(w, http.StatusForbidden, "forbidden", "Access denied") - return - } - - if entry.VaultData == nil { - ErrorResponse(w, http.StatusBadRequest, "no_totp", "Entry has no TOTP field") - return - } - - // Find TOTP field - var totpSeed string - var isL2 bool - for _, field := range entry.VaultData.Fields { - if field.Kind == "totp" { - if field.L2 { - isL2 = true - } else { - totpSeed = field.Value - } - break - } - } - - if isL2 { - JSONResponse(w, http.StatusOK, map[string]any{ - "l2": true, - }) - return - } - - if totpSeed == "" { - ErrorResponse(w, http.StatusBadRequest, "no_totp", "Entry has no TOTP field") - return - } - - // Normalize seed (remove spaces, uppercase) - totpSeed = strings.ToUpper(strings.ReplaceAll(totpSeed, " ", "")) - - // Generate TOTP code - code, err := totp.GenerateCode(totpSeed, time.Now()) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_totp", "Invalid TOTP seed") - return - } - - // Calculate time until expiry (30 second window) - now := time.Now().Unix() - expiresIn := 30 - (now % 30) - - lib.AuditLog(h.db(r), &lib.AuditEvent{ - EntryID: entry.EntryID, - Title: entry.Title, - Action: "totp", - Actor: actor, - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusOK, map[string]any{ - "code": code, - "expires_in": expiresIn, - "l2": false, - }) -} - -// MatchURL finds credentials matching a URL (scope-filtered). -func (h *Handlers) MatchURL(w http.ResponseWriter, r *http.Request) { - agent := h.agent(r) - urlStr := r.URL.Query().Get("url") - if urlStr == "" { - ErrorResponse(w, http.StatusBadRequest, "missing_url", "URL parameter required") - return - } - - domain := extractDomain(urlStr) - - entries, err := lib.EntryList(h.db(r), h.vk(r), nil) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "list_failed", "Failed to list entries") - return - } - - var matches []lib.Entry - for _, entry := range entries { - if !lib.AgentCanAccess(agent, entry.Scopes) { - continue - } - if entry.VaultData == nil { - continue - } - for _, u := range entry.VaultData.URLs { - if strings.Contains(u, domain) || strings.Contains(domain, extractDomain(u)) { - matches = append(matches, entry) - break - } - } - } - - if matches == nil { - matches = []lib.Entry{} - } - - JSONResponse(w, http.StatusOK, matches) -} - - -// --------------------------------------------------------------------------- -// Audit Log -// --------------------------------------------------------------------------- - -// GetAuditLog returns recent audit events. -func (h *Handlers) GetAuditLog(w http.ResponseWriter, r *http.Request) { - limitStr := r.URL.Query().Get("limit") - limit := 100 - if limitStr != "" { - if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { - limit = l - } - } - - events, err := lib.AuditList(h.db(r), limit) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "list_failed", "Failed to list audit events") - return - } - if events == nil { - events = []lib.AuditEvent{} - } - - JSONResponse(w, http.StatusOK, events) -} - -// --------------------------------------------------------------------------- -// WebAuthn PRF -// --------------------------------------------------------------------------- - -// HandleWebAuthnRegisterBegin starts WebAuthn registration. -func (h *Handlers) HandleWebAuthnRegisterBegin(w http.ResponseWriter, r *http.Request) { - // Generate a challenge - challenge := make([]byte, 32) - rand.Read(challenge) - - options := map[string]any{ - "publicKey": map[string]any{ - "challenge": challenge, - "rp": map[string]string{"name": "Clavitor", "id": rpID(r)}, - "user": map[string]any{ - "id": []byte("clavitor-owner"), - "name": "vault-owner", - "displayName": "Clavitor Owner", - }, - "pubKeyCredParams": []map[string]any{ - {"type": "public-key", "alg": -7}, // ES256 - {"type": "public-key", "alg": -257}, // RS256 - }, - "authenticatorSelection": map[string]any{ - "residentKey": "preferred", - "userVerification": "required", - }, - "extensions": map[string]any{ - "prf": map[string]any{}, - }, - }, - } - JSONResponse(w, http.StatusOK, options) -} - -// HandleWebAuthnRegisterComplete finishes WebAuthn registration. -func (h *Handlers) HandleWebAuthnRegisterComplete(w http.ResponseWriter, r *http.Request) { - var req struct { - CredID string `json:"cred_id"` - PublicKey []byte `json:"public_key"` - PRFSalt []byte `json:"prf_salt"` - Name string `json:"name"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - - // Decode the base64url credential ID from the browser - credentialID, err := base64.RawURLEncoding.DecodeString(req.CredID) - if err != nil { - credentialID = []byte{} - } - - cred := &lib.WebAuthnCredential{ - CredID: lib.HexID(lib.NewID()), - Name: req.Name, - PublicKey: req.PublicKey, - CredentialID: credentialID, - PRFSalt: req.PRFSalt, - } - if err := lib.StoreWebAuthnCredential(h.db(r), cred); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "store_failed", "Failed to store credential") - return - } - - JSONResponse(w, http.StatusCreated, map[string]any{"status": "registered", "cred_id": cred.CredID}) -} - -// HandleWebAuthnAuthBegin starts WebAuthn authentication with PRF extension. -func (h *Handlers) HandleWebAuthnAuthBegin(w http.ResponseWriter, r *http.Request) { - creds, err := lib.GetWebAuthnCredentials(h.db(r)) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "get_failed", "Failed to get credentials") - return - } - - challenge := make([]byte, 32) - rand.Read(challenge) - - var allowCreds []map[string]any - var prfSalt []byte - for _, c := range creds { - allowCreds = append(allowCreds, map[string]any{ - "type": "public-key", - "id": c.CredID, - }) - if len(c.PRFSalt) > 0 { - prfSalt = c.PRFSalt - } - } - - prfExt := map[string]any{} - if len(prfSalt) > 0 { - prfExt["eval"] = map[string]any{ - "first": prfSalt, - } - } - - options := map[string]any{ - "publicKey": map[string]any{ - "challenge": challenge, - "allowCredentials": allowCreds, - "userVerification": "required", - "extensions": map[string]any{ - "prf": prfExt, - }, - }, - } - JSONResponse(w, http.StatusOK, options) -} - -// HandleWebAuthnAuthComplete finishes WebAuthn authentication. -func (h *Handlers) HandleWebAuthnAuthComplete(w http.ResponseWriter, r *http.Request) { - var req struct { - CredID lib.HexID `json:"cred_id"` - SignCount int `json:"sign_count"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - - if err := lib.UpdateWebAuthnSignCount(h.db(r), int64(req.CredID), req.SignCount); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "update_failed", "Failed to update sign count") - return - } - - JSONResponse(w, http.StatusOK, map[string]string{"status": "authenticated"}) -} - -// HandleListWebAuthnCredentials returns all registered WebAuthn credentials. -func (h *Handlers) HandleListWebAuthnCredentials(w http.ResponseWriter, r *http.Request) { - creds, err := lib.GetWebAuthnCredentials(h.db(r)) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "list_failed", "Failed to list credentials") - return - } - if creds == nil { - creds = []lib.WebAuthnCredential{} - } - JSONResponse(w, http.StatusOK, creds) -} - -// HandleDeleteWebAuthnCredential removes a WebAuthn credential. -func (h *Handlers) HandleDeleteWebAuthnCredential(w http.ResponseWriter, r *http.Request) { - id, err := lib.HexToID(chi.URLParam(r, "id")) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid credential ID") - return - } - if err := lib.DeleteWebAuthnCredential(h.db(r), id); err != nil { - if err == lib.ErrNotFound { - ErrorResponse(w, http.StatusNotFound, "not_found", "Credential not found") - return - } - ErrorResponse(w, http.StatusInternalServerError, "delete_failed", "Failed to delete credential") - return - } - JSONResponse(w, http.StatusOK, map[string]string{"status": "deleted"}) -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -func stripL2Fields(vd *lib.VaultData) { - for i := range vd.Fields { - if vd.Fields[i].L2 { - vd.Fields[i].Value = "[REDACTED — not available to agents]" - } - } -} - -func extractDomain(urlStr string) string { - // Simple domain extraction - urlStr = strings.TrimPrefix(urlStr, "https://") - urlStr = strings.TrimPrefix(urlStr, "http://") - urlStr = strings.TrimPrefix(urlStr, "www.") - if idx := strings.Index(urlStr, "/"); idx > 0 { - urlStr = urlStr[:idx] - } - if idx := strings.Index(urlStr, ":"); idx > 0 { - urlStr = urlStr[:idx] - } - return urlStr -} - -// generateTOTPSecret generates a new TOTP secret. -func generateTOTPSecret() string { - b := make([]byte, 20) - rand.Read(b) - return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b) -} - -// --- Backup / Restore --- - -// ListBackups returns all available backup files. -func (h *Handlers) ListBackups(w http.ResponseWriter, r *http.Request) { - backups := lib.ListBackups(h.Cfg.DataDir) - JSONResponse(w, http.StatusOK, backups) -} - -// CreateBackup triggers an immediate backup. -func (h *Handlers) CreateBackup(w http.ResponseWriter, r *http.Request) { - lib.RunBackups(h.Cfg.DataDir) - backups := lib.ListBackups(h.Cfg.DataDir) - JSONResponse(w, http.StatusOK, backups) -} - -// RestoreBackup restores from a named backup file. -func (h *Handlers) RestoreBackup(w http.ResponseWriter, r *http.Request) { - var req struct { - Name string `json:"name"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" { - ErrorResponse(w, http.StatusBadRequest, "invalid_request", "Backup name required") - return - } - if err := lib.RestoreBackup(h.Cfg.DataDir, req.Name); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "restore_error", err.Error()) - return - } - JSONResponse(w, http.StatusOK, map[string]string{"status": "restored", "name": req.Name}) -} - -// --------------------------------------------------------------------------- -// Agent Management -// --------------------------------------------------------------------------- - -// HandleCreateAgent creates a new agent. OWNER ONLY. -// POST /api/agents -func (h *Handlers) HandleCreateAgent(w http.ResponseWriter, r *http.Request) { - if h.requireOwner(w, r) { - return - } - - var req struct { - Name string `json:"name"` - Scopes string `json:"scopes"` // "auto" or comma-separated hex, default auto - AllAccess bool `json:"all_access"` - Admin bool `json:"admin"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - if req.Name == "" { - ErrorResponse(w, http.StatusBadRequest, "missing_name", "Name is required") - return - } - if len(req.Name) > 100 { - ErrorResponse(w, http.StatusBadRequest, "name_too_long", "Name must be 100 characters or less") - return - } - - db := h.db(r) - scopes := req.Scopes - if scopes == "" { - scopes = "auto" - } - - agent, rawToken, err := lib.AgentCreate(db, req.Name, scopes, req.AllAccess, req.Admin, h.l1Raw(r)) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "create_failed", "Failed to create agent") - return - } - - lib.AuditLog(db, &lib.AuditEvent{ - Action: lib.ActionAgentCreate, - Actor: ActorFromContext(r.Context()), - IPAddr: realIP(r), - Title: agent.Name, - }) - - // Return agent info + raw token (shown ONCE, never retrievable again) - JSONResponse(w, http.StatusCreated, map[string]any{ - "id": agent.ID, - "scope": agent.ScopeHex(), - "name": agent.Name, - "scopes": agent.Scopes, - "all_access": agent.AllAccess, - "admin": agent.Admin, - "token": rawToken, - }) -} - -// HandleListAgents lists all agents. OWNER ONLY. -// GET /api/agents -func (h *Handlers) HandleListAgents(w http.ResponseWriter, r *http.Request) { - if h.requireOwner(w, r) { - return - } - agents, err := lib.AgentList(h.db(r)) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "list_failed", "Failed to list agents") - return - } - if agents == nil { - agents = []lib.Agent{} - } - JSONResponse(w, http.StatusOK, agents) -} - -// HandleGetAgent returns a single agent. OWNER ONLY. -// GET /api/agents/{id} -func (h *Handlers) HandleGetAgent(w http.ResponseWriter, r *http.Request) { - if h.requireOwner(w, r) { - return - } - id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid agent ID") - return - } - a, err := lib.AgentGet(h.db(r), id) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "get_failed", "Failed to get agent") - return - } - if a == nil { - ErrorResponse(w, http.StatusNotFound, "not_found", "Agent not found") - return - } - JSONResponse(w, http.StatusOK, a) -} - -// HandleUpdateAgent updates an agent's name, scopes, flags. OWNER ONLY. -// PUT /api/agents/{id} -func (h *Handlers) HandleUpdateAgent(w http.ResponseWriter, r *http.Request) { - if h.requireOwner(w, r) { - return - } - id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid agent ID") - return - } - - var req struct { - Name string `json:"name"` - Scopes string `json:"scopes"` - AllAccess bool `json:"all_access"` - Admin bool `json:"admin"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - - db := h.db(r) - a, err := lib.AgentGet(db, id) - if err != nil || a == nil { - ErrorResponse(w, http.StatusNotFound, "not_found", "Agent not found") - return - } - - if err := lib.AgentUpdate(db, id, req.Name, req.Scopes, req.AllAccess, req.Admin); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "update_failed", "Failed to update agent") - return - } - - lib.AuditLog(db, &lib.AuditEvent{ - Action: "agent_update", - Actor: ActorFromContext(r.Context()), - IPAddr: realIP(r), - Title: a.Name, - }) - - JSONResponse(w, http.StatusOK, map[string]string{"status": "updated"}) -} - -// HandleDeleteAgent deletes an agent. OWNER ONLY. -// DELETE /api/agents/{id} -func (h *Handlers) HandleDeleteAgent(w http.ResponseWriter, r *http.Request) { - if h.requireOwner(w, r) { - return - } - id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid agent ID") - return - } - - db := h.db(r) - a, err := lib.AgentGet(db, id) - if err != nil || a == nil { - ErrorResponse(w, http.StatusNotFound, "not_found", "Agent not found") - return - } - - if err := lib.AgentDelete(db, id); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "delete_failed", "Failed to delete agent") - return - } - - lib.AuditLog(db, &lib.AuditEvent{ - Action: lib.ActionAgentRevoke, - Actor: ActorFromContext(r.Context()), - IPAddr: realIP(r), - Title: a.Name, - }) - - JSONResponse(w, http.StatusOK, map[string]string{"status": "deleted"}) -} - -// HandleUpdateEntryScopes updates only the scopes of an entry. OWNER ONLY. -// PUT /api/entries/{id}/scopes -func (h *Handlers) HandleUpdateEntryScopes(w http.ResponseWriter, r *http.Request) { - if h.requireOwner(w, r) { - return - } - entryID, err := lib.HexToID(chi.URLParam(r, "id")) - if err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid entry ID") - return - } - - var req struct { - Scopes string `json:"scopes"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") - return - } - - if err := lib.EntryUpdateScopes(h.db(r), entryID, req.Scopes); err != nil { - if err == lib.ErrNotFound { - ErrorResponse(w, http.StatusNotFound, "not_found", "Entry not found") - return - } - ErrorResponse(w, http.StatusInternalServerError, "update_failed", "Failed to update scopes") - return - } - - lib.AuditLog(h.db(r), &lib.AuditEvent{ - EntryID: lib.HexID(entryID), - Action: "entry_scopes_update", - Actor: ActorFromContext(r.Context()), - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusOK, map[string]string{"status": "updated"}) -} - -// --------------------------------------------------------------------------- -// Vault Lock -// --------------------------------------------------------------------------- - -// HandleVaultLockStatus returns the vault lock state. -// GET /api/vault-lock -func (h *Handlers) HandleVaultLockStatus(w http.ResponseWriter, r *http.Request) { - vl, err := lib.VaultLockGet(h.db(r)) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "get_failed", "Failed to get vault lock status") - return - } - JSONResponse(w, http.StatusOK, vl) -} - -// HandleVaultUnlock unlocks the vault. -// POST /api/vault-unlock -func (h *Handlers) HandleVaultUnlock(w http.ResponseWriter, r *http.Request) { - db := h.db(r) - if err := lib.VaultLockSet(db, false, ""); err != nil { - ErrorResponse(w, http.StatusInternalServerError, "unlock_failed", "Failed to unlock vault") - return - } - - lib.AuditLog(db, &lib.AuditEvent{ - Action: "vault_unlock", - Actor: ActorFromContext(r.Context()), - IPAddr: realIP(r), - }) - - JSONResponse(w, http.StatusOK, map[string]string{"status": "unlocked"}) -} diff --git a/clavis/clavis-vault/_old/api/integration_test.go b/clavis/clavis-vault/_old/api/integration_test.go deleted file mode 100644 index d1a5780..0000000 --- a/clavis/clavis-vault/_old/api/integration_test.go +++ /dev/null @@ -1,882 +0,0 @@ -package api - -// Integration tests for the Clavitor vault API. -// -// The test client authenticates exactly as production does: -// - 8-byte L1 key sent as base64url Bearer on every request -// - DB filename derived from L1[:4]: clavitor-{base64url(l1[:4])} -// - L1 normalized to 16 bytes for AES-128 vault encryption -// -// Each test gets an isolated vault (temp dir + fresh DB). -// Run: go test ./api/... -v - -import ( - "bytes" - "embed" - "encoding/base64" - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/johanj/clavitor/lib" -) - -// --- test client --- - -type tc struct { - srv *httptest.Server - bearer string // base64url-encoded L1 key (8 bytes) - t *testing.T -} - -// newTestClient creates an isolated vault and test server. -// The L1 key is deterministic so tests are reproducible. -func newTestClient(t *testing.T) *tc { - t.Helper() - - tmpDir := t.TempDir() - cfg := &lib.Config{ - Port: "0", - DataDir: tmpDir, - SessionTTL: 86400, - } - - // Fixed 8-byte L1 key for testing. - l1Raw := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44} - bearer := base64.RawURLEncoding.EncodeToString(l1Raw) - - // DB filename must match what L1Middleware derives: clavitor-{base64url(l1[:4])} - prefix := base64.RawURLEncoding.EncodeToString(l1Raw[:4]) - dbPath := tmpDir + "/clavitor-" + prefix - - db, err := lib.OpenDB(dbPath) - if err != nil { - t.Fatalf("opendb: %v", err) - } - if err := lib.MigrateDB(db); err != nil { - t.Fatalf("migrate: %v", err) - } - db.Close() - - var emptyFS embed.FS - srv := httptest.NewServer(NewRouter(cfg, emptyFS)) - t.Cleanup(srv.Close) - - return &tc{srv: srv, bearer: bearer, t: t} -} - -// req sends an authenticated HTTP request. -func (c *tc) req(method, path string, body any) *http.Response { - c.t.Helper() - var r io.Reader - if body != nil { - b, _ := json.Marshal(body) - r = bytes.NewReader(b) - } - req, _ := http.NewRequest(method, c.srv.URL+path, r) - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - req.Header.Set("Authorization", "Bearer "+c.bearer) - resp, err := c.srv.Client().Do(req) - if err != nil { - c.t.Fatalf("req %s %s: %v", method, path, err) - } - return resp -} - -// reqNoAuth sends an unauthenticated request. -func (c *tc) reqNoAuth(method, path string, body any) *http.Response { - c.t.Helper() - var r io.Reader - if body != nil { - b, _ := json.Marshal(body) - r = bytes.NewReader(b) - } - req, _ := http.NewRequest(method, c.srv.URL+path, r) - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - resp, err := c.srv.Client().Do(req) - if err != nil { - c.t.Fatalf("req %s %s: %v", method, path, err) - } - return resp -} - -// must asserts status code and returns parsed JSON object. -func (c *tc) must(resp *http.Response, wantStatus int) map[string]any { - c.t.Helper() - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != wantStatus { - c.t.Fatalf("expected %d, got %d: %s", wantStatus, resp.StatusCode, body) - } - var out map[string]any - json.Unmarshal(body, &out) - return out -} - -// mustList asserts status code and returns parsed JSON array. -func (c *tc) mustList(resp *http.Response, wantStatus int) []map[string]any { - c.t.Helper() - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != wantStatus { - c.t.Fatalf("expected %d, got %d: %s", wantStatus, resp.StatusCode, body) - } - var out []map[string]any - json.Unmarshal(body, &out) - return out -} - -// --- test data --- - -func credentialEntry(title, username, password string, urls []string) map[string]any { - return map[string]any{ - "title": title, - "type": "credential", - "data": map[string]any{ - "title": title, - "type": "credential", - "fields": []map[string]any{ - {"label": "username", "value": username, "kind": "text"}, - {"label": "password", "value": password, "kind": "password"}, - }, - "urls": urls, - }, - } -} - -// --------------------------------------------------------------------------- -// Health & Ping -// --------------------------------------------------------------------------- - -func TestHealth(t *testing.T) { - c := newTestClient(t) - result := c.must(c.reqNoAuth("GET", "/health", nil), 200) - if result["status"] != "ok" { - t.Errorf("status = %v, want ok", result["status"]) - } -} - -func TestPing(t *testing.T) { - c := newTestClient(t) - result := c.must(c.reqNoAuth("GET", "/ping", nil), 200) - if result["ok"] != true { - t.Errorf("ok = %v, want true", result["ok"]) - } - if result["node"] == nil || result["node"] == "" { - t.Error("node should not be empty") - } - if result["ts"] == nil { - t.Error("ts should be present") - } -} - -// --------------------------------------------------------------------------- -// L1 Auth -// --------------------------------------------------------------------------- - -func TestL1Auth_valid_key(t *testing.T) { - c := newTestClient(t) - // Should return empty array, not an auth error - c.mustList(c.req("GET", "/api/entries?meta=1", nil), 200) -} - -func TestL1Auth_bad_bearer_rejected(t *testing.T) { - c := newTestClient(t) - req, _ := http.NewRequest("GET", c.srv.URL+"/api/entries", nil) - req.Header.Set("Authorization", "Bearer not-valid-base64") - resp, _ := c.srv.Client().Do(req) - defer resp.Body.Close() - if resp.StatusCode != 401 { - t.Errorf("bad bearer should return 401, got %d", resp.StatusCode) - } -} - -func TestL1Auth_wrong_key_vault_not_found(t *testing.T) { - c := newTestClient(t) - // Valid base64url but points to a non-existent vault - wrongL1 := base64.RawURLEncoding.EncodeToString([]byte{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8}) - req, _ := http.NewRequest("GET", c.srv.URL+"/api/entries", nil) - req.Header.Set("Authorization", "Bearer "+wrongL1) - resp, _ := c.srv.Client().Do(req) - defer resp.Body.Close() - if resp.StatusCode != 404 { - t.Errorf("wrong L1 key should return 404 (vault not found), got %d", resp.StatusCode) - } -} - -// --------------------------------------------------------------------------- -// Entry CRUD -// --------------------------------------------------------------------------- - -func TestCreateEntry(t *testing.T) { - c := newTestClient(t) - result := c.must(c.req("POST", "/api/entries", credentialEntry("GitHub", "octocat", "hunter2", []string{"https://github.com"})), 201) - if result["entry_id"] == nil || result["entry_id"] == "" { - t.Fatal("create should return entry_id") - } -} - -func TestCreateEntry_missing_title(t *testing.T) { - c := newTestClient(t) - resp := c.req("POST", "/api/entries", map[string]any{"type": "credential"}) - defer resp.Body.Close() - if resp.StatusCode != 400 { - t.Errorf("missing title should return 400, got %d", resp.StatusCode) - } -} - -func TestReadEntry_roundtrip(t *testing.T) { - c := newTestClient(t) - - created := c.must(c.req("POST", "/api/entries", credentialEntry("GitHub", "octocat", "hunter2", nil)), 201) - id := created["entry_id"].(string) - - got := c.must(c.req("GET", "/api/entries/"+id, nil), 200) - data := got["data"].(map[string]any) - fields := data["fields"].([]any) - - found := map[string]string{} - for _, f := range fields { - fm := f.(map[string]any) - found[fm["label"].(string)] = fm["value"].(string) - } - if found["username"] != "octocat" { - t.Errorf("username = %q, want octocat", found["username"]) - } - if found["password"] != "hunter2" { - t.Errorf("password = %q, want hunter2", found["password"]) - } -} - -func TestUpdateEntry(t *testing.T) { - c := newTestClient(t) - - created := c.must(c.req("POST", "/api/entries", credentialEntry("Old", "user", "pass", nil)), 201) - id := created["entry_id"].(string) - - updated := c.must(c.req("PUT", "/api/entries/"+id, map[string]any{ - "title": "New", - "version": 1, - "data": map[string]any{ - "title": "New", "type": "credential", - "fields": []map[string]any{{"label": "username", "value": "newuser", "kind": "text"}}, - }, - }), 200) - - if updated["title"] != "New" { - t.Errorf("title = %v, want New", updated["title"]) - } -} - -func TestUpdateEntry_version_conflict(t *testing.T) { - c := newTestClient(t) - - created := c.must(c.req("POST", "/api/entries", credentialEntry("Test", "u", "p", nil)), 201) - id := created["entry_id"].(string) - - c.must(c.req("PUT", "/api/entries/"+id, map[string]any{ - "title": "V2", "version": 1, - "data": map[string]any{"title": "V2", "type": "credential"}, - }), 200) - - resp := c.req("PUT", "/api/entries/"+id, map[string]any{ - "title": "Stale", "version": 1, - "data": map[string]any{"title": "Stale", "type": "credential"}, - }) - defer resp.Body.Close() - if resp.StatusCode != 409 { - t.Errorf("stale version should return 409, got %d", resp.StatusCode) - } -} - -func TestDeleteEntry(t *testing.T) { - c := newTestClient(t) - - created := c.must(c.req("POST", "/api/entries", credentialEntry("ToDelete", "u", "p", nil)), 201) - id := created["entry_id"].(string) - - c.must(c.req("DELETE", "/api/entries/"+id, nil), 200) - - entries := c.mustList(c.req("GET", "/api/entries?meta=1", nil), 200) - for _, e := range entries { - if e["entry_id"] == id { - t.Error("deleted entry should not appear in list") - } - } -} - -func TestListEntries_meta(t *testing.T) { - c := newTestClient(t) - c.must(c.req("POST", "/api/entries", credentialEntry("One", "u", "p", nil)), 201) - c.must(c.req("POST", "/api/entries", credentialEntry("Two", "u", "p", nil)), 201) - - entries := c.mustList(c.req("GET", "/api/entries?meta=1", nil), 200) - if len(entries) != 2 { - t.Errorf("expected 2 entries, got %d", len(entries)) - } - for _, e := range entries { - if e["data"] != nil { - t.Error("meta mode should not include field data") - } - } -} - -// --------------------------------------------------------------------------- -// Upsert -// --------------------------------------------------------------------------- - -func TestUpsert_creates_when_new(t *testing.T) { - c := newTestClient(t) - result := c.must(c.req("PUT", "/api/entries", map[string]any{ - "title": "NewEntry", "type": "credential", - "data": map[string]any{ - "title": "NewEntry", "type": "credential", - "fields": []map[string]any{{"label": "password", "value": "abc", "kind": "password"}}, - }, - }), 201) - if result["entry_id"] == nil { - t.Fatal("upsert create should return entry_id") - } -} - -func TestUpsert_updates_when_exists(t *testing.T) { - c := newTestClient(t) - - // Create - c.must(c.req("PUT", "/api/entries", map[string]any{ - "title": "GitHub", "type": "credential", - "data": map[string]any{ - "title": "GitHub", "type": "credential", - "fields": []map[string]any{{"label": "password", "value": "old", "kind": "password"}}, - }, - }), 201) - - // Upsert same title → update - result := c.must(c.req("PUT", "/api/entries", map[string]any{ - "title": "GitHub", "type": "credential", - "data": map[string]any{ - "title": "GitHub", "type": "credential", - "fields": []map[string]any{{"label": "password", "value": "new", "kind": "password"}}, - }, - }), 200) - - // Verify updated - data := result["data"].(map[string]any) - fields := data["fields"].([]any) - pw := fields[0].(map[string]any)["value"].(string) - if pw != "new" { - t.Errorf("password should be 'new' after upsert, got %q", pw) - } - - // Verify only one entry exists - entries := c.mustList(c.req("GET", "/api/entries?meta=1", nil), 200) - if len(entries) != 1 { - t.Errorf("expected 1 entry after upsert, got %d", len(entries)) - } -} - -func TestUpsert_case_insensitive(t *testing.T) { - c := newTestClient(t) - - c.must(c.req("PUT", "/api/entries", map[string]any{ - "title": "GitHub", "type": "credential", - "data": map[string]any{"title": "GitHub", "type": "credential", "fields": []map[string]any{{"label": "user", "value": "v1", "kind": "text"}}}, - }), 201) - - // Upsert with different case → should update, not create - c.must(c.req("PUT", "/api/entries", map[string]any{ - "title": "github", "type": "credential", - "data": map[string]any{"title": "github", "type": "credential", "fields": []map[string]any{{"label": "user", "value": "v2", "kind": "text"}}}, - }), 200) - - entries := c.mustList(c.req("GET", "/api/entries?meta=1", nil), 200) - if len(entries) != 1 { - t.Errorf("case-insensitive upsert should match existing, got %d entries", len(entries)) - } -} - -// --------------------------------------------------------------------------- -// Entry Types — note, card, identity -// --------------------------------------------------------------------------- - -func TestUpsert_note(t *testing.T) { - c := newTestClient(t) - result := c.must(c.req("PUT", "/api/entries", map[string]any{ - "title": "Meeting Notes", "type": "note", - "data": map[string]any{ - "title": "Meeting Notes", "type": "note", - "fields": []map[string]any{{"label": "Content", "value": "Discussed Q3 roadmap", "kind": "text"}}, - "notes": "Follow up next week", - }, - }), 201) - id := result["entry_id"].(string) - - got := c.must(c.req("GET", "/api/entries/"+id, nil), 200) - data := got["data"].(map[string]any) - if data["notes"] != "Follow up next week" { - t.Errorf("notes = %v", data["notes"]) - } -} - -func TestUpsert_card(t *testing.T) { - c := newTestClient(t) - result := c.must(c.req("PUT", "/api/entries", map[string]any{ - "title": "Amex Platinum", "type": "card", - "data": map[string]any{ - "title": "Amex Platinum", "type": "card", - "fields": []map[string]any{ - {"label": "Cardholder", "value": "Johan Jongsma", "kind": "text"}, - {"label": "Number", "value": "ENC_BLOB_378282246310005", "kind": "text", "l2": true}, - {"label": "CVV", "value": "ENC_BLOB_1234", "kind": "text", "l2": true}, - {"label": "Expiry", "value": "09/28", "kind": "text"}, - }, - }, - }), 201) - id := result["entry_id"].(string) - - got := c.must(c.req("GET", "/api/entries/"+id, nil), 200) - data := got["data"].(map[string]any) - fields := data["fields"].([]any) - if len(fields) != 4 { - t.Fatalf("card should have 4 fields, got %d", len(fields)) - } - // L2 blobs should survive roundtrip - number := fields[1].(map[string]any)["value"].(string) - if number != "ENC_BLOB_378282246310005" { - t.Errorf("card number blob changed: %q", number) - } -} - -func TestUpsert_identity(t *testing.T) { - c := newTestClient(t) - result := c.must(c.req("PUT", "/api/entries", map[string]any{ - "title": "Home Address", "type": "identity", - "data": map[string]any{ - "title": "Home Address", "type": "identity", - "fields": []map[string]any{ - {"label": "Street", "value": "123 Main St", "kind": "text"}, - {"label": "City", "value": "Springfield", "kind": "text"}, - {"label": "State", "value": "IL", "kind": "text"}, - {"label": "ZIP", "value": "62704", "kind": "text"}, - {"label": "Country", "value": "US", "kind": "text"}, - }, - }, - }), 201) - id := result["entry_id"].(string) - - got := c.must(c.req("GET", "/api/entries/"+id, nil), 200) - data := got["data"].(map[string]any) - fields := data["fields"].([]any) - if len(fields) != 5 { - t.Fatalf("identity should have 5 fields, got %d", len(fields)) - } - city := fields[1].(map[string]any)["value"].(string) - if city != "Springfield" { - t.Errorf("city = %q", city) - } -} - -// --------------------------------------------------------------------------- -// Search -// --------------------------------------------------------------------------- - -func TestSearch(t *testing.T) { - c := newTestClient(t) - c.must(c.req("POST", "/api/entries", credentialEntry("GitHub", "u", "p", nil)), 201) - c.must(c.req("POST", "/api/entries", credentialEntry("GitLab", "u", "p", nil)), 201) - c.must(c.req("POST", "/api/entries", credentialEntry("AWS", "u", "p", nil)), 201) - - entries := c.mustList(c.req("GET", "/api/search?q=Git", nil), 200) - if len(entries) != 2 { - t.Errorf("search for 'Git' should return 2, got %d", len(entries)) - } -} - -func TestSearch_no_query(t *testing.T) { - c := newTestClient(t) - resp := c.req("GET", "/api/search", nil) - defer resp.Body.Close() - if resp.StatusCode != 400 { - t.Errorf("missing query should return 400, got %d", resp.StatusCode) - } -} - -// --------------------------------------------------------------------------- -// TOTP -// --------------------------------------------------------------------------- - -func TestTOTP_valid_code(t *testing.T) { - c := newTestClient(t) - entry := map[string]any{ - "title": "2FA Test", "type": "credential", - "data": map[string]any{ - "title": "2FA Test", "type": "credential", - "fields": []map[string]any{{"label": "totp", "value": "JBSWY3DPEHPK3PXP", "kind": "totp"}}, - }, - } - created := c.must(c.req("POST", "/api/entries", entry), 201) - id := created["entry_id"].(string) - - result := c.must(c.req("GET", "/api/ext/totp/"+id, nil), 200) - code, _ := result["code"].(string) - if len(code) != 6 { - t.Errorf("TOTP code = %q, want 6 digits", code) - } - expiresIn, _ := result["expires_in"].(float64) - if expiresIn <= 0 || expiresIn > 30 { - t.Errorf("expires_in = %v, want 1-30", expiresIn) - } -} - -func TestTOTP_L2_returns_locked(t *testing.T) { - c := newTestClient(t) - entry := map[string]any{ - "title": "L2 TOTP", "type": "credential", - "data": map[string]any{ - "title": "L2 TOTP", "type": "credential", - "fields": []map[string]any{{"label": "totp", "value": "JBSWY3DPEHPK3PXP", "kind": "totp", "l2": true}}, - }, - } - created := c.must(c.req("POST", "/api/entries", entry), 201) - id := created["entry_id"].(string) - - result := c.must(c.req("GET", "/api/ext/totp/"+id, nil), 200) - if result["l2"] != true { - t.Error("L2 TOTP should return l2:true") - } -} - -// --------------------------------------------------------------------------- -// URL Match -// --------------------------------------------------------------------------- - -func TestURLMatch(t *testing.T) { - c := newTestClient(t) - c.must(c.req("POST", "/api/entries", credentialEntry("GitHub", "u", "p", []string{"https://github.com"})), 201) - - matches := c.mustList(c.req("GET", "/api/ext/match?url=https://github.com/login", nil), 200) - if len(matches) == 0 { - t.Error("should match github.com for github.com/login") - } -} - -func TestURLMatch_no_match(t *testing.T) { - c := newTestClient(t) - c.must(c.req("POST", "/api/entries", credentialEntry("GitHub", "u", "p", []string{"https://github.com"})), 201) - - matches := c.mustList(c.req("GET", "/api/ext/match?url=https://example.com", nil), 200) - if len(matches) != 0 { - t.Errorf("should not match, got %d", len(matches)) - } -} - -// --------------------------------------------------------------------------- -// Password Generator -// --------------------------------------------------------------------------- - -func TestPasswordGenerator(t *testing.T) { - c := newTestClient(t) - result := c.must(c.req("GET", "/api/generate?length=24", nil), 200) - pw, _ := result["password"].(string) - if len(pw) != 24 { - t.Errorf("password length = %d, want 24", len(pw)) - } -} - -func TestPasswordGenerator_passphrase(t *testing.T) { - c := newTestClient(t) - result := c.must(c.req("GET", "/api/generate?words=4", nil), 200) - pw, _ := result["password"].(string) - words := strings.Split(pw, "-") - if len(words) != 4 { - t.Errorf("passphrase should have 4 words, got %d: %q", len(words), pw) - } -} - -// --------------------------------------------------------------------------- -// Audit Log -// --------------------------------------------------------------------------- - -func TestAuditLog(t *testing.T) { - c := newTestClient(t) - - created := c.must(c.req("POST", "/api/entries", credentialEntry("Audited", "u", "p", nil)), 201) - id := created["entry_id"].(string) - c.must(c.req("GET", "/api/entries/"+id, nil), 200) - - events := c.mustList(c.req("GET", "/api/audit", nil), 200) - if len(events) < 2 { - t.Errorf("expected at least 2 events (create + read), got %d", len(events)) - } - actions := map[string]bool{} - for _, e := range events { - if a, ok := e["action"].(string); ok { - actions[a] = true - } - } - if !actions["create"] { - t.Error("missing 'create' in audit log") - } - if !actions["read"] { - t.Error("missing 'read' in audit log") - } -} - -// --------------------------------------------------------------------------- -// WebAuthn Auth Flow -// --------------------------------------------------------------------------- - -func TestAuthStatus_fresh(t *testing.T) { - c := newTestClient(t) - result := c.must(c.reqNoAuth("GET", "/api/auth/status", nil), 200) - if result["state"] != "fresh" { - t.Errorf("state = %v, want fresh", result["state"]) - } -} - -func TestAuthRegisterBegin_fresh(t *testing.T) { - c := newTestClient(t) - resp := c.reqNoAuth("POST", "/api/auth/register/begin", map[string]any{}) - defer resp.Body.Close() - if resp.StatusCode != 200 { - b, _ := io.ReadAll(resp.Body) - t.Fatalf("expected 200, got %d: %s", resp.StatusCode, b) - } - var result map[string]any - json.NewDecoder(resp.Body).Decode(&result) - pk := result["publicKey"].(map[string]any) - if pk["challenge"] == nil { - t.Fatal("response should contain a challenge") - } -} - -// --------------------------------------------------------------------------- -// Tier Isolation -// --------------------------------------------------------------------------- - -// TestTierIsolation verifies that L2/L3 encrypted blobs survive the L1 -// envelope encrypt/decrypt roundtrip intact. The server packs all fields -// into a single AES-GCM envelope (L1). L2/L3 field values are opaque -// ciphertext — the server stores them, never inspects them. -func TestTierIsolation(t *testing.T) { - c := newTestClient(t) - - l2Blob := "AQIDBAUGB5iL2EncryptedBlob+test==" - l3Blob := "AQIDBAUGB5iL3EncryptedBlob+test==" - - created := c.must(c.req("POST", "/api/entries", map[string]any{ - "type": "credential", "title": "TierTest", - "data": map[string]any{ - "title": "TierTest", "type": "credential", - "fields": []map[string]any{ - {"label": "Username", "value": "testuser", "kind": "text"}, - {"label": "Password", "value": l2Blob, "kind": "password", "tier": 2}, - {"label": "SSN", "value": l3Blob, "kind": "text", "tier": 3, "l2": true}, - }, - }, - }), 201) - id := created["entry_id"].(string) - - got := c.must(c.req("GET", "/api/entries/"+id, nil), 200) - data := got["data"].(map[string]any) - fields := data["fields"].([]any) - - found := map[string]string{} - for _, raw := range fields { - f := raw.(map[string]any) - found[f["label"].(string)], _ = f["value"].(string) - } - - if found["Username"] != "testuser" { - t.Errorf("L1 Username = %q, want testuser", found["Username"]) - } - if found["Password"] != l2Blob { - t.Errorf("L2 Password blob changed: %q", found["Password"]) - } - if found["SSN"] != l3Blob { - t.Errorf("L3 SSN blob changed: %q", found["SSN"]) - } -} - -// --------------------------------------------------------------------------- -// Scoped Access -// --------------------------------------------------------------------------- - -// reqAgent sends an authenticated request using a cvt_ agent token. -func (c *tc) reqAgent(token, method, path string, body any) *http.Response { - c.t.Helper() - var r io.Reader - if body != nil { - b, _ := json.Marshal(body) - r = bytes.NewReader(b) - } - req, _ := http.NewRequest(method, c.srv.URL+path, r) - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - req.Header.Set("Authorization", "Bearer "+token) - resp, err := c.srv.Client().Do(req) - if err != nil { - c.t.Fatalf("req %s %s: %v", method, path, err) - } - return resp -} - -// createAgent creates an agent via the owner API and returns the raw token. -func (c *tc) createAgent(name string, allAccess bool) string { - c.t.Helper() - resp := c.req("POST", "/api/agents", map[string]any{ - "name": name, - "all_access": allAccess, - }) - result := c.must(resp, 201) - token, ok := result["token"].(string) - if !ok || token == "" { - c.t.Fatalf("createAgent: no token in response: %v", result) - } - return token -} - -func TestScopedAccess_agent_sees_only_scoped_entries(t *testing.T) { - c := newTestClient(t) - c.must(c.req("POST", "/api/auth/setup", nil), 200) - - // Create agent (gets scope "0001" since it's agent id=1) - token := c.createAgent("Claude Code", false) - - // Create two entries: one scoped to agent 0001, one owner-only - c.must(c.req("POST", "/api/entries", map[string]any{ - "title": "Scoped Entry", - "type": "credential", - "data": map[string]any{"title": "Scoped Entry", "type": "credential"}, - }), 201) - c.must(c.req("POST", "/api/entries", map[string]any{ - "title": "Owner Only", - "type": "credential", - "data": map[string]any{"title": "Owner Only", "type": "credential"}, - }), 201) - - // Get all entries via owner — should see both - ownerEntries := c.mustList(c.req("GET", "/api/entries", nil), 200) - if len(ownerEntries) != 2 { - t.Fatalf("owner should see 2 entries, got %d", len(ownerEntries)) - } - - // Find the scoped entry by title and assign scope "0001" - var scopedEntryID string - for _, e := range ownerEntries { - if e["title"] == "Scoped Entry" { - scopedEntryID = e["entry_id"].(string) - break - } - } - c.must(c.req("PUT", "/api/entries/"+scopedEntryID+"/scopes", map[string]any{ - "scopes": "0001", - }), 200) - - // Agent should see only the scoped entry - agentEntries := c.mustList(c.reqAgent(token, "GET", "/api/entries", nil), 200) - if len(agentEntries) != 1 { - t.Fatalf("agent should see 1 entry, got %d", len(agentEntries)) - } - if agentEntries[0]["title"] != "Scoped Entry" { - t.Errorf("agent saw wrong entry: %s", agentEntries[0]["title"]) - } -} - -func TestScopedAccess_agent_forbidden_on_unscoped_entry(t *testing.T) { - c := newTestClient(t) - c.must(c.req("POST", "/api/auth/setup", nil), 200) - - token := c.createAgent("Test Agent", false) - - // Create owner-only entry (no scopes) - resp := c.req("POST", "/api/entries", map[string]any{ - "title": "Secret", - "type": "note", - "data": map[string]any{"title": "Secret", "type": "note"}, - }) - entry := c.must(resp, 201) - entryID := entry["entry_id"].(string) - - // Agent tries to GET it — should be 403 - agentResp := c.reqAgent(token, "GET", "/api/entries/"+entryID, nil) - if agentResp.StatusCode != 403 { - t.Fatalf("expected 403, got %d", agentResp.StatusCode) - } - agentResp.Body.Close() -} - -func TestScopedAccess_all_access_agent_sees_everything(t *testing.T) { - c := newTestClient(t) - c.must(c.req("POST", "/api/auth/setup", nil), 200) - - token := c.createAgent("Full Access Agent", true) - - // Create owner-only entry - c.must(c.req("POST", "/api/entries", map[string]any{ - "title": "Owner Secret", - "type": "note", - "data": map[string]any{"title": "Owner Secret", "type": "note"}, - }), 201) - - // all_access agent should see it - agentEntries := c.mustList(c.reqAgent(token, "GET", "/api/entries", nil), 200) - if len(agentEntries) != 1 { - t.Fatalf("all_access agent should see 1 entry, got %d", len(agentEntries)) - } -} - -func TestScopedAccess_agent_cannot_manage_agents(t *testing.T) { - c := newTestClient(t) - c.must(c.req("POST", "/api/auth/setup", nil), 200) - - token := c.createAgent("Evil Agent", false) - - // Agent tries to create another agent — should be 403 - resp := c.reqAgent(token, "POST", "/api/agents", map[string]any{ - "name": "Backdoor", - }) - if resp.StatusCode != 403 { - t.Fatalf("agent should not create agents: got %d", resp.StatusCode) - } - resp.Body.Close() - - // Agent tries to list agents — should be 403 - resp = c.reqAgent(token, "GET", "/api/agents", nil) - if resp.StatusCode != 403 { - t.Fatalf("agent should not list agents: got %d", resp.StatusCode) - } - resp.Body.Close() -} - -func TestScopedAccess_agent_cannot_modify_scopes(t *testing.T) { - c := newTestClient(t) - c.must(c.req("POST", "/api/auth/setup", nil), 200) - - token := c.createAgent("Scope Hijacker", false) - - // Create an entry - entry := c.must(c.req("POST", "/api/entries", map[string]any{ - "title": "Target", - "type": "credential", - "data": map[string]any{"title": "Target", "type": "credential"}, - }), 201) - entryID := entry["entry_id"].(string) - - // Agent tries to modify scopes — should be 403 - resp := c.reqAgent(token, "PUT", "/api/entries/"+entryID+"/scopes", map[string]any{ - "scopes": "0001", - }) - if resp.StatusCode != 403 { - t.Fatalf("agent should not modify scopes: got %d", resp.StatusCode) - } - resp.Body.Close() -} diff --git a/clavis/clavis-vault/_old/api/middleware.go b/clavis/clavis-vault/_old/api/middleware.go deleted file mode 100644 index 1f534e6..0000000 --- a/clavis/clavis-vault/_old/api/middleware.go +++ /dev/null @@ -1,390 +0,0 @@ -package api - -import ( - "context" - "encoding/base64" - "encoding/json" - "log" - "net/http" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/johanj/clavitor/lib" -) - -// base64Decode handles both standard and url-safe base64 (with or without padding). -func base64Decode(s string) ([]byte, error) { - // Try url-safe first (no padding), then standard - s = strings.TrimRight(s, "=") - b, err := base64.RawURLEncoding.DecodeString(s) - if err != nil { - b, err = base64.RawStdEncoding.DecodeString(s) - } - return b, err -} - -// base64UrlEncode encodes bytes as base64url without padding. -func base64UrlEncode(b []byte) string { - return base64.RawURLEncoding.EncodeToString(b) -} - -type contextKey string - -const ( - ctxActor contextKey = "actor" - ctxSession contextKey = "session" - ctxAgent contextKey = "agent" - ctxDB contextKey = "db" - ctxVaultKey contextKey = "vault_key" - ctxVaultID contextKey = "vault_id" -) - -// ActorFromContext returns the actor type from request context. -func ActorFromContext(ctx context.Context) string { - v, ok := ctx.Value(ctxActor).(string) - if !ok { - return lib.ActorWeb - } - return v -} - -// SessionFromContext returns the session from request context. -func SessionFromContext(ctx context.Context) *lib.Session { - v, _ := ctx.Value(ctxSession).(*lib.Session) - return v -} - -// AgentFromContext returns the agent from request context (nil if not an agent request). -func AgentFromContext(ctx context.Context) *lib.Agent { - v, _ := ctx.Value(ctxAgent).(*lib.Agent) - return v -} - -// DBFromContext returns the vault DB from request context (nil in self-hosted mode). -func DBFromContext(ctx context.Context) *lib.DB { - v, _ := ctx.Value(ctxDB).(*lib.DB) - return v -} - -// VaultKeyFromContext returns the derived vault key from request context (nil in self-hosted mode). -func VaultKeyFromContext(ctx context.Context) []byte { - v, _ := ctx.Value(ctxVaultKey).([]byte) - return v -} - -// VaultIDFromContext returns the vault ID from request context (0 in self-hosted mode). -func VaultIDFromContext(ctx context.Context) int64 { - v, _ := ctx.Value(ctxVaultID).(int64) - return v -} - -// L1Middleware extracts L1 from Bearer token and opens the vault DB. -// Fully stateless: L1 arrives with every request, is used, then forgotten. -// No sessions, no stored keys. The server has zero keys of its own. -// -// Self-hosted mode: finds vault DB by globbing clavitor-* files. -// Hosted mode: finds vault DB by base64url(L1[0:4]) → filename. -func L1Middleware(dataDir string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - auth := r.Header.Get("Authorization") - - // No auth = unauthenticated request (registration, login begin, etc.) - if auth == "" || !strings.HasPrefix(auth, "Bearer ") { - // Try to open vault DB without L1 (for unauthenticated endpoints) - matches, _ := filepath.Glob(filepath.Join(dataDir, "clavitor-*")) - if len(matches) > 0 { - db, err := lib.OpenDB(matches[0]) - if err == nil { - defer db.Close() - ctx := context.WithValue(r.Context(), ctxDB, db) - next.ServeHTTP(w, r.WithContext(ctx)) - return - } - } - // Also try legacy .db files for migration - matches, _ = filepath.Glob(filepath.Join(dataDir, "????????.db")) - if len(matches) > 0 { - db, err := lib.OpenDB(matches[0]) - if err == nil { - defer db.Close() - ctx := context.WithValue(r.Context(), ctxDB, db) - next.ServeHTTP(w, r.WithContext(ctx)) - return - } - } - next.ServeHTTP(w, r) - return - } - - bearerVal := strings.TrimPrefix(auth, "Bearer ") - - var l1Raw []byte - var agent *lib.Agent - - if strings.HasPrefix(bearerVal, "cvt_") { - // --- Agent token: cvt_ prefix --- - // Extract L1 and look up agent by token hash. - var hash string - var err error - l1Raw, hash, err = lib.ParseToken(bearerVal) - if err != nil { - ErrorResponse(w, http.StatusUnauthorized, "invalid_token", "Invalid agent token") - return - } - - // Open vault DB from L1 - l1Key := lib.NormalizeKey(l1Raw) - vaultPrefix := base64UrlEncode(l1Raw[:4]) - dbPath := filepath.Join(dataDir, "clavitor-"+vaultPrefix) - - db, err := lib.OpenDB(dbPath) - if err != nil { - ErrorResponse(w, http.StatusNotFound, "vault_not_found", "Vault not found") - return - } - defer db.Close() - - // Look up agent - agent, err = lib.AgentGetByToken(db, hash) - if err != nil { - ErrorResponse(w, http.StatusInternalServerError, "agent_error", "Agent lookup failed") - return - } - if agent == nil { - ErrorResponse(w, http.StatusUnauthorized, "unknown_token", "Invalid or revoked token") - return - } - - ctx := context.WithValue(r.Context(), ctxDB, db) - ctx = context.WithValue(ctx, ctxVaultKey, l1Key) - ctx = context.WithValue(ctx, ctxActor, lib.ActorAgent) - ctx = context.WithValue(ctx, ctxAgent, agent) - next.ServeHTTP(w, r.WithContext(ctx)) - - } else { - // --- Legacy L1 bearer (web UI / extension) --- - // 8 bytes base64url = vault owner, full access, no agent. - l1Raw, err := base64Decode(bearerVal) - if err != nil || len(l1Raw) != 8 { - ErrorResponse(w, http.StatusUnauthorized, "invalid_l1", "Invalid L1 key in Bearer") - return - } - - l1Key := lib.NormalizeKey(l1Raw) - vaultPrefix := base64UrlEncode(l1Raw[:4]) - dbPath := filepath.Join(dataDir, "clavitor-"+vaultPrefix) - - var db *lib.DB - if _, err := os.Stat(dbPath); err == nil { - db, err = lib.OpenDB(dbPath) - if err != nil { - log.Printf("vault open error (%s): %v", dbPath, err) - ErrorResponse(w, http.StatusInternalServerError, "db_error", "Failed to open vault") - return - } - } - if db == nil { - ErrorResponse(w, http.StatusNotFound, "vault_not_found", "Vault not found") - return - } - defer db.Close() - - ctx := context.WithValue(r.Context(), ctxDB, db) - ctx = context.WithValue(ctx, ctxVaultKey, l1Key) - ctx = context.WithValue(ctx, ctxActor, lib.ActorWeb) - // No agent in context = vault owner (full access) - - next.ServeHTTP(w, r.WithContext(ctx)) - } - }) - } -} - -// IsAgentRequest returns true if the request was made with a cvt_ agent token. -func IsAgentRequest(r *http.Request) bool { - return AgentFromContext(r.Context()) != nil -} - -// LoggingMiddleware logs HTTP requests. -func LoggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - wrapped := &statusWriter{ResponseWriter: w, status: 200} - next.ServeHTTP(wrapped, r) - log.Printf("%s %s %d %s", r.Method, r.URL.Path, wrapped.status, time.Since(start)) - }) -} - -type statusWriter struct { - http.ResponseWriter - status int -} - -func (w *statusWriter) WriteHeader(code int) { - w.status = code - w.ResponseWriter.WriteHeader(code) -} - -// RateLimitMiddleware implements per-IP rate limiting. -func RateLimitMiddleware(requestsPerMinute int) func(http.Handler) http.Handler { - var mu sync.Mutex - clients := make(map[string]*rateLimitEntry) - - go func() { - for { - time.Sleep(time.Minute) - mu.Lock() - now := time.Now() - for ip, entry := range clients { - if now.Sub(entry.windowStart) > time.Minute { - delete(clients, ip) - } - } - mu.Unlock() - } - }() - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ip := realIP(r) - - mu.Lock() - entry, exists := clients[ip] - now := time.Now() - if !exists || now.Sub(entry.windowStart) > time.Minute { - entry = &rateLimitEntry{windowStart: now, count: 0} - clients[ip] = entry - } - entry.count++ - count := entry.count - mu.Unlock() - - if count > requestsPerMinute { - ErrorResponse(w, http.StatusTooManyRequests, "rate_limited", "Too many requests") - return - } - - next.ServeHTTP(w, r) - }) - } -} - -type rateLimitEntry struct { - windowStart time.Time - count int -} - -// CORSMiddleware handles CORS headers. -func CORSMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - origin := r.Header.Get("Origin") - - // Allow localhost and 127.0.0.1 for development - if origin != "" && (strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1")) { - w.Header().Set("Access-Control-Allow-Origin", origin) - w.Header().Set("Vary", "Origin") - } - - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") - w.Header().Set("Access-Control-Max-Age", "86400") - - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusNoContent) - return - } - - next.ServeHTTP(w, r) - }) -} - -// SecurityHeadersMiddleware adds security headers to all responses. -func SecurityHeadersMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-Frame-Options", "DENY") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("X-XSS-Protection", "1; mode=block") - w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") - // CSP allowing localhost and 127.0.0.1 for development - w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' localhost 127.0.0.1") - - next.ServeHTTP(w, r) - }) -} - -// ErrorResponse sends a standard JSON error response. -func ErrorResponse(w http.ResponseWriter, status int, code, message string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(map[string]string{ - "error": message, - "code": code, - }) -} - -// JSONResponse sends a standard JSON success response. -func JSONResponse(w http.ResponseWriter, status int, data any) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(data) -} - -// tarpitHandler holds unrecognized requests for 30 seconds. -// Drips one byte per second to keep the connection alive and waste -// scanner resources. Capped at 1000 concurrent tarpit slots — -// beyond that, connections are dropped immediately. -var ( - tarpitSem = make(chan struct{}, 1000) -) - -func tarpitHandler(w http.ResponseWriter, r *http.Request) { - select { - case tarpitSem <- struct{}{}: - defer func() { <-tarpitSem }() - default: - // Tarpit full — drop immediately, no response - if hj, ok := w.(http.Hijacker); ok { - conn, _, err := hj.Hijack() - if err == nil { - conn.Close() - } - } - return - } - - log.Printf("tarpit: %s %s from %s", r.Method, r.URL.Path, realIP(r)) - - // Chunked response: drip one space per second for 30s - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(200) - flusher, canFlush := w.(http.Flusher) - for i := 0; i < 30; i++ { - _, err := w.Write([]byte(" ")) - if err != nil { - return // client gave up - } - if canFlush { - flusher.Flush() - } - time.Sleep(time.Second) - } -} - -func realIP(r *http.Request) string { - if xff := r.Header.Get("X-Forwarded-For"); xff != "" { - parts := strings.SplitN(xff, ",", 2) - return strings.TrimSpace(parts[0]) - } - if xri := r.Header.Get("X-Real-IP"); xri != "" { - return xri - } - addr := r.RemoteAddr - if idx := strings.LastIndex(addr, ":"); idx != -1 { - return addr[:idx] - } - return addr -} diff --git a/clavis/clavis-vault/_old/api/routes.go b/clavis/clavis-vault/_old/api/routes.go deleted file mode 100644 index 566c808..0000000 --- a/clavis/clavis-vault/_old/api/routes.go +++ /dev/null @@ -1,170 +0,0 @@ -package api - -import ( - "embed" - "fmt" - "io/fs" - "net/http" - "os" - "time" - - "github.com/go-chi/chi/v5" - "github.com/johanj/clavitor/lib" -) - -// NewRouter creates the main router with all routes registered. -func NewRouter(cfg *lib.Config, webFS embed.FS) *chi.Mux { - r := chi.NewRouter() - h := NewHandlers(cfg) - - // Global middleware - r.Use(LoggingMiddleware) - r.Use(CORSMiddleware) - r.Use(SecurityHeadersMiddleware) - r.Use(RateLimitMiddleware(120)) // 120 req/min per IP - r.Use(L1Middleware(cfg.DataDir)) // stateless: extract L1 from Bearer, open DB, forget - - // Health check (unauthenticated — no Bearer needed) - r.Get("/health", h.Health) - - // Ping — minimal latency probe for looking glass (no DB, no auth) - node, _ := os.Hostname() - r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - body := fmt.Sprintf(`{"ok":true,"node":"%s","ts":%d}`, node, time.Now().Unix()) - w.Write([]byte(body)) - }) - - // Auth endpoints (unauthenticated — no Bearer, DB found by glob) - r.Get("/api/auth/status", h.AuthStatus) - r.Post("/api/auth/register/begin", h.AuthRegisterBegin) - r.Post("/api/auth/register/complete", h.AuthRegisterComplete) - r.Post("/api/auth/login/begin", h.AuthLoginBegin) - r.Post("/api/auth/login/complete", h.AuthLoginComplete) - - // Legacy setup (only works when no credentials exist — for tests) - r.Post("/api/auth/setup", h.Setup) - - // API routes (authenticated — L1 in Bearer, already validated by L1Middleware) - r.Route("/api", func(r chi.Router) { - mountAPIRoutes(r, h) - }) - - // --- Vault App UI at /app/* --- - appRoot, err := fs.Sub(webFS, "web") - if err == nil { - appServer := http.FileServer(http.FS(appRoot)) - r.Get("/app", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/app/", http.StatusMovedPermanently) - }) - r.Handle("/app/*", http.StripPrefix("/app", appServer)) - } - - // --- Root-level: minimal, disclose nothing --- - // Legitimate browser/crawler requests get a fast, empty response. - // Everything else hits the tarpit (30s slow drain). - - favicon, _ := fs.ReadFile(webFS, "web/favicon.svg") - serveFavicon := func(w http.ResponseWriter, r *http.Request) { - if favicon != nil { - w.Header().Set("Content-Type", "image/svg+xml") - w.Header().Set("Cache-Control", "public, max-age=86400") - w.Write(favicon) - } else { - w.WriteHeader(204) - } - } - nothing := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } - disallow := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - w.Write([]byte("User-agent: *\nDisallow: /\n")) - } - - r.Get("/", nothing) - r.Get("/favicon.ico", serveFavicon) - r.Get("/favicon.svg", serveFavicon) - r.Get("/robots.txt", disallow) - r.Get("/sitemap.xml", nothing) - r.Get("/sitemap.xml.gz", nothing) - r.Get("/sitemap-index.xml", nothing) - r.Get("/ads.txt", nothing) - r.Get("/app-ads.txt", nothing) - r.Get("/manifest.json", nothing) - r.Get("/browserconfig.xml", nothing) - r.Get("/crossdomain.xml", nothing) - r.Get("/humans.txt", nothing) - r.Get("/security.txt", nothing) - r.Get("/apple-touch-icon.png", nothing) - r.Get("/apple-touch-icon-precomposed.png", nothing) - r.Get("/.well-known/security.txt", nothing) - r.Get("/.well-known/acme-challenge/*", nothing) - r.Get("/.well-known/change-password", nothing) - r.Get("/.well-known/openid-configuration", nothing) - r.Get("/.well-known/webfinger", nothing) - r.Get("/.well-known/assetlinks.json", nothing) - r.Get("/.well-known/apple-app-site-association", nothing) - r.Get("/.well-known/mta-sts.txt", nothing) - r.Get("/.well-known/nodeinfo", nothing) - - // Tarpit: everything not registered above. - // Hold the connection for 30s, drip slowly, waste scanner resources. - r.NotFound(tarpitHandler) - r.MethodNotAllowed(tarpitHandler) - - return r -} - -// mountAPIRoutes registers the authenticated API handlers on the given router. -func mountAPIRoutes(r chi.Router, h *Handlers) { - // Vault info (for Tokens page config snippets) - r.Get("/vault-info", h.VaultInfo) - - // Entries CRUD - r.Get("/entries", h.ListEntries) - r.Post("/entries", h.CreateEntry) - r.Put("/entries", h.UpsertEntry) - r.Get("/entries/{id}", h.GetEntry) - r.Put("/entries/{id}", h.UpdateEntry) - r.Delete("/entries/{id}", h.DeleteEntry) - - // Search - r.Get("/search", h.SearchEntries) - - // Password generator - r.Get("/generate", h.GeneratePassword) - - // Audit log - r.Get("/audit", h.GetAuditLog) - - // Extension API - r.Get("/ext/totp/{id}", h.GetTOTP) - r.Get("/ext/match", h.MatchURL) - - // Backups - r.Get("/backups", h.ListBackups) - r.Post("/backups", h.CreateBackup) - r.Post("/backups/restore", h.RestoreBackup) - - // Agent management (owner-only — handlers reject agent tokens) - r.Post("/agents", h.HandleCreateAgent) - r.Get("/agents", h.HandleListAgents) - r.Get("/agents/{id}", h.HandleGetAgent) - r.Put("/agents/{id}", h.HandleUpdateAgent) - r.Delete("/agents/{id}", h.HandleDeleteAgent) - - // Entry scope management (owner-only) - r.Put("/entries/{id}/scopes", h.HandleUpdateEntryScopes) - - // Vault lock - r.Get("/vault-lock", h.HandleVaultLockStatus) - r.Post("/vault-unlock", h.HandleVaultUnlock) - - // WebAuthn - r.Post("/webauthn/register/begin", h.HandleWebAuthnRegisterBegin) - r.Post("/webauthn/register/complete", h.HandleWebAuthnRegisterComplete) - r.Post("/webauthn/auth/begin", h.HandleWebAuthnAuthBegin) - r.Post("/webauthn/auth/complete", h.HandleWebAuthnAuthComplete) - r.Get("/webauthn/credentials", h.HandleListWebAuthnCredentials) - r.Delete("/webauthn/credentials/{id}", h.HandleDeleteWebAuthnCredential) -} diff --git a/clavis/clavis-vault/_old/cmd/clavitor/main.go b/clavis/clavis-vault/_old/cmd/clavitor/main.go deleted file mode 100644 index 13c6765..0000000 --- a/clavis/clavis-vault/_old/cmd/clavitor/main.go +++ /dev/null @@ -1,113 +0,0 @@ -package main - -import ( - "embed" - "flag" - "log" - "os" - "strconv" - - "github.com/johanj/clavitor/api" - "github.com/johanj/clavitor/lib" - "github.com/johanj/clavitor/proxy" -) - -//go:embed web -var webFS embed.FS - -// Set via -ldflags at build time. -var ( - version = "dev" - commit = "unknown" - buildDate = "unknown" -) - -func main() { - api.Version = version + " (" + commit + " " + buildDate + ")" - - port := flag.Int("port", envInt("PORT", 443), "Listen port") - telemetryFreq := flag.Int("telemetry-freq", envInt("TELEMETRY_FREQ", 0), "Telemetry POST interval in seconds (0 = disabled)") - telemetryHost := flag.String("telemetry-host", envStr("TELEMETRY_HOST", ""), "Telemetry endpoint URL") - telemetryToken := flag.String("telemetry-token", envStr("TELEMETRY_TOKEN", ""), "Bearer token for telemetry endpoint") - - // Proxy mode flags - proxyEnabled := flag.Bool("proxy", envBool("PROXY_ENABLED", false), "Enable MITM proxy mode (set HTTP_PROXY=http://127.0.0.1:19840 in agent)") - proxyAddr := flag.String("proxy-addr", envStr("PROXY_ADDR", "127.0.0.1:19840"), "Proxy listen address") - proxyLLM := flag.Bool("proxy-llm", envBool("PROXY_LLM", false), "Enable LLM policy evaluation in proxy") - proxyLLMURL := flag.String("proxy-llm-url", envStr("PROXY_LLM_URL", ""), "LLM API base URL for proxy policy (OpenAI-compatible)") - proxyLLMKey := flag.String("proxy-llm-key", envStr("PROXY_LLM_KEY", ""), "LLM API key for proxy policy") - proxyLLMModel := flag.String("proxy-llm-model", envStr("PROXY_LLM_MODEL", ""), "LLM model for proxy policy evaluation") - - flag.Parse() - - cfg, err := lib.LoadConfig() - if err != nil { - log.Fatalf("config: %v", err) - } - cfg.Port = strconv.Itoa(*port) - - // Start telemetry reporter if configured. - lib.StartTelemetry(lib.TelemetryConfig{ - FreqSeconds: *telemetryFreq, - Host: *telemetryHost, - Token: *telemetryToken, - DataDir: cfg.DataDir, - Version: version, - }) - - // Start automatic backup scheduler (3 weekly + 3 monthly, rotated) - lib.StartBackupTimer(cfg.DataDir) - - // Start proxy if enabled - if *proxyEnabled { - px, err := proxy.New(proxy.Config{ - ListenAddr: *proxyAddr, - DataDir: cfg.DataDir, - LLMEnabled: *proxyLLM, - LLMBaseURL: *proxyLLMURL, - LLMAPIKey: *proxyLLMKey, - LLMModel: *proxyLLMModel, - }) - if err != nil { - log.Fatalf("proxy: %v", err) - } - go func() { - if err := px.ListenAndServe(); err != nil { - log.Printf("proxy: stopped: %v", err) - } - }() - log.Printf("proxy: CA cert at %s — install in OS trust store", px.CACertPath()) - log.Printf("proxy: set HTTP_PROXY=http://%s HTTPS_PROXY=http://%s in agent environment", *proxyAddr, *proxyAddr) - } - - router := api.NewRouter(cfg, webFS) - - addr := ":" + cfg.Port - tlsCfg := lib.LoadTLSConfig() - if err := lib.ListenAndServeTLS(addr, router, tlsCfg); err != nil { - log.Fatalf("server: %v", err) - } -} - -func envStr(key, fallback string) string { - if v := os.Getenv(key); v != "" { - return v - } - return fallback -} - -func envBool(key string, fallback bool) bool { - if v := os.Getenv(key); v != "" { - return v == "1" || v == "true" || v == "yes" - } - return fallback -} - -func envInt(key string, fallback int) int { - if v := os.Getenv(key); v != "" { - if n, err := strconv.Atoi(v); err == nil { - return n - } - } - return fallback -} diff --git a/clavis/clavis-vault/_old/cmd/clavitor/web/clavitor-app.css b/clavis/clavis-vault/_old/cmd/clavitor/web/clavitor-app.css deleted file mode 100644 index 047dfcd..0000000 --- a/clavis/clavis-vault/_old/cmd/clavitor/web/clavitor-app.css +++ /dev/null @@ -1,457 +0,0 @@ -/* ============================================================ - clavitor — app stylesheet - Design tokens from clavitor.css (website styleguide). - App-specific component classes below. - ============================================================ */ - -/* === TOKENS (shared with website) === */ -:root { - --pad: 2rem; - --radius: 1rem; - --radius-sm: 0.5rem; - --gap: 1.25rem; - - --bg: #0d1520; - --surface: #142542; - --surface-alt: #0e2414; - --surface-gold: #2a1f00; - - --border: rgba(255,255,255,0.09); - --border-gold: rgba(212,175,55,0.3); - - --text: #f1f5f9; - --muted: #94a3b8; - --subtle: #64748b; - - --accent: #4ade80; - --gold: #D4AF37; - --red: #EF4444; - - --font-sans: Inter, system-ui, sans-serif; - --font-mono: 'JetBrains Mono', monospace; -} - -/* --- Themes --- */ - -body.theme-light { - --bg: #f8f9fb; - --surface: #ffffff; - --surface-alt: #f0fdf4; - --surface-gold: #fffbeb; - --border: rgba(0,0,0,0.1); - --border-gold: rgba(180,140,20,0.3); - --text: #1e293b; - --muted: #64748b; - --subtle: #94a3b8; - --accent: #16a34a; - --gold: #b8860b; - --red: #dc2626; -} - -body.theme-midnight { - --bg: #020617; - --surface: #0f172a; - --surface-alt: #022c22; - --surface-gold: #1c1500; - --border: rgba(255,255,255,0.06); - --border-gold: rgba(212,175,55,0.25); - --text: #e2e8f0; - --muted: #64748b; - --subtle: #475569; - --accent: #22c55e; - --gold: #eab308; - --red: #f87171; -} - -body.theme-light .topbar { background: rgba(255,255,255,0.9); } -body.theme-light .vault-lock-banner { background: rgba(239,68,68,0.08); } -} - -/* === RESET === */ -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -html, body { height: 100%; } -body { background: var(--bg); color: var(--text); font-family: var(--font-sans); font-size: 0.875rem; line-height: 1.6; } -a { color: inherit; text-decoration: none; } -button { font-family: inherit; font-size: inherit; cursor: pointer; border: none; background: none; color: inherit; } -input, select, textarea { font-family: inherit; font-size: inherit; color: var(--text); } - -/* === TYPOGRAPHY === */ -h1 { font-size: 1.875rem; font-weight: 800; line-height: 1.1; color: var(--text); } -h2 { font-size: 1.25rem; font-weight: 700; line-height: 1.2; color: var(--text); } -h3 { font-size: 1.125rem; font-weight: 700; line-height: 1.3; color: var(--text); } -p { color: var(--muted); line-height: 1.75; } -.label { font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--subtle); } - -/* === VAULTNAME (from website) === */ -.vaultname { font-family: var(--font-mono); font-weight: 700; color: var(--text); } -.vaultname .n { color: var(--accent); } - -/* === BUTTONS === */ -.btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.375rem; font-family: var(--font-sans); font-size: 0.875rem; font-weight: 600; padding: 0.625rem 1.25rem; border-radius: var(--radius-sm); border: 1px solid transparent; cursor: pointer; transition: opacity 0.15s, transform 0.15s; text-align: center; line-height: 1.4; } -.btn:hover { opacity: 0.85; } -.btn:active { transform: scale(0.97); } -.btn-primary { background: var(--accent); color: var(--bg); border-color: var(--accent); } -.btn-ghost { background: transparent; color: var(--text); border-color: var(--border); } -.btn-gold { background: rgba(212,175,55,0.15); color: var(--gold); border-color: rgba(212,175,55,0.3); } -.btn-red { background: rgba(239,68,68,0.15); color: var(--red); border-color: rgba(239,68,68,0.3); } -.btn-accent { background: rgba(34,197,94,0.15); color: var(--accent); border-color: rgba(34,197,94,0.3); } -.btn-block { display: flex; width: 100%; } -.btn-lg { padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: var(--radius); } -.btn-row { display: flex; flex-wrap: wrap; gap: 1rem; } - -/* === CARDS === */ -.card { background: rgba(100,140,200,0.12); border: 1px solid rgba(148,163,184,0.15); border-radius: var(--radius); padding: 1.75rem; } -.card.alt { background: rgba(34,197,94,0.15); border-color: rgba(34,197,94,0.3); } -.card.gold { background: rgba(212,175,55,0.15); border-color: rgba(212,175,55,0.35); } -.card.red { background: rgba(239,68,68,0.15); border-color: rgba(239,68,68,0.3); } -.card-hover { transition: transform 0.2s, box-shadow 0.2s; border-color: rgba(255,255,255,0.12); } -.card-hover:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.3); } - -/* === BADGES === */ -.badge { display: inline-block; font-family: var(--font-mono); font-size: 0.7rem; font-weight: 600; padding: 0.25rem 0.625rem; border-radius: 9999px; } -.badge.accent { background: rgba(34,197,94,0.15); color: var(--accent); border: 1px solid rgba(34,197,94,0.3); } -.badge.gold { background: rgba(212,175,55,0.15); color: var(--gold); border: 1px solid rgba(212,175,55,0.3); } -.badge.red { background: rgba(239,68,68,0.15); color: var(--red); border: 1px solid rgba(239,68,68,0.3); } -.badge.blue { background: rgba(96,165,250,0.15); color: #60a5fa; border: 1px solid rgba(96,165,250,0.3); } -.badge.muted { background: rgba(100,116,139,0.15); color: var(--subtle); border: 1px solid rgba(100,116,139,0.3); } - -/* === SPACING (from website) === */ -.mt-2 { margin-top: 0.5rem; } .mb-2 { margin-bottom: 0.5rem; } -.mt-3 { margin-top: 0.75rem; } .mb-3 { margin-bottom: 0.75rem; } -.mt-4 { margin-top: 1rem; } .mb-4 { margin-bottom: 1rem; } -.mt-6 { margin-top: 1.5rem; } .mb-6 { margin-bottom: 1.5rem; } -.mt-8 { margin-top: 2rem; } .mb-8 { margin-bottom: 2rem; } - -/* ============================================================ - APP — Layout - ============================================================ */ - -.app-shell { display: flex; height: 100%; } - -/* --- Centered column --- */ -.app-column { max-width: 52rem; margin: 0 auto; width: 100%; } - -/* --- Top bar --- */ -.vault-lock-banner { background: rgba(239,68,68,0.15); border-bottom: 1px solid rgba(239,68,68,0.4); color: var(--red, #ef4444); padding: 0.6rem 1rem; display: flex; align-items: center; justify-content: space-between; font-weight: 600; font-size: 0.8125rem; } -.topbar { position: sticky; top: 0; z-index: 40; background: rgba(10,22,40,0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); padding: 0 1rem; } -.topbar-inner { display: flex; align-items: center; justify-content: space-between; height: 56px; } -.topbar-logo { font-family: var(--font-mono); font-weight: 700; font-size: 1.25rem; color: var(--text); line-height: 1; letter-spacing: -0.02em; } -.topbar-logo .n { color: var(--accent); } -.topbar-links { display: flex; align-items: center; gap: 1.25rem; font-size: 0.875rem; } -.topbar-links a, -.topbar-links button { color: var(--muted); transition: color 0.15s; font-size: 0.875rem; } -.topbar-links a:hover, -.topbar-links button:hover { color: var(--text); } -.topbar-links a.topbar-active { color: var(--text); } -.topbar-lock { color: var(--gold) !important; } - -/* --- Toolbar (search + actions) --- */ -.toolbar { background: rgba(10,22,40,0.5); border-bottom: 1px solid var(--border); padding: 0.75rem 1rem; } -.toolbar-inner { display: flex; align-items: center; gap: 0.75rem; } -.toolbar-search { flex: 0 1 20rem; padding: 0.5rem 0.875rem; background: rgba(255,255,255,0.05); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); outline: none; transition: border-color 0.15s, box-shadow 0.15s; font-size: 0.875rem; } -.toolbar-search:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(74,222,128,0.15); } -.toolbar-search::placeholder { color: var(--subtle); } - -/* --- Main area --- */ -.main-area { display: flex; flex-direction: column; height: 100%; overflow: hidden; } -.main-content { flex: 1; overflow: hidden; display: flex; } - -/* --- Split panes --- */ -.split-list { width: 26rem; min-width: 22rem; max-width: 32rem; overflow-y: auto; border-right: 1px solid var(--border); flex-shrink: 0; } -.split-detail { flex: 1; overflow-y: auto; } -.split-detail-empty { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--subtle); font-size: 0.875rem; } - -/* Active row in split list */ -.entry-row.active { background: rgba(74,222,128,0.08); border-right: 2px solid var(--accent); } - -/* ============================================================ - APP — Entry List - ============================================================ */ - -/* Stats bar (between toolbar and content) */ -.list-stats { padding: 0.5rem 1rem; background: rgba(10,22,40,0.6); border-bottom: 1px solid var(--border); display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; } -.list-badge { font-size: 0.75rem; font-weight: 600; font-family: var(--font-mono); color: var(--text); padding: 0.25rem 0.625rem; border-radius: var(--radius-sm); background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.06); white-space: nowrap; cursor: pointer; transition: opacity 0.15s; } -.list-badge:hover { opacity: 0.8; } -.list-badge.active { outline: 1.5px solid currentColor; outline-offset: 1px; } -.list-badge.type-credential { color: var(--accent); background: rgba(74,222,128,0.12); border-color: rgba(74,222,128,0.15); } -.list-badge.type-card { color: var(--gold); background: rgba(212,175,55,0.12); border-color: rgba(212,175,55,0.15); } -.list-badge.type-note { color: var(--muted); background: rgba(148,163,184,0.1); border-color: rgba(148,163,184,0.12); } -.list-badge.type-identity { color: #60a5fa; background: rgba(96,165,250,0.12); border-color: rgba(96,165,250,0.15); } -.list-badge.type-ssh_key { color: var(--red); background: rgba(239,68,68,0.12); border-color: rgba(239,68,68,0.15); } -.list-badge.type-totp { color: #a855f7; background: rgba(168,85,247,0.12); border-color: rgba(168,85,247,0.15); } - -.entry-row { display: flex; align-items: center; gap: 0.875rem; padding: 0.75rem 1rem; border-bottom: 1px solid rgba(255,255,255,0.04); cursor: pointer; transition: background 0.15s, transform 0.15s; } -.entry-row:hover { background: rgba(255,255,255,0.05); } -.entry-row:active { transform: scale(0.995); } -.entry-icon { width: 2.75rem; height: 1.375rem; border-radius: 0.25rem; background: rgba(100,140,200,0.12); display: flex; align-items: center; justify-content: center; font-size: 0.5rem; font-weight: 600; color: var(--muted); flex-shrink: 0; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.05em; } -.entry-icon.type-credential { background: rgba(74,222,128,0.1); color: var(--accent); } -.entry-icon.type-card { background: rgba(212,175,55,0.1); color: var(--gold); } -.entry-icon.type-identity { background: rgba(96,165,250,0.1); color: #60a5fa; } -.entry-icon.type-note { background: rgba(148,163,184,0.1); color: var(--muted); } -.entry-icon.type-ssh_key { background: rgba(239,68,68,0.1); color: var(--red); } -.entry-icon.type-totp { background: rgba(168,85,247,0.1); color: #a855f7; } -.entry-icon.type-folder { background: rgba(212,175,55,0.1); color: var(--gold); } -.entry-domain { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); } -.entry-user { color: var(--subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.8125rem; } -.entry-user::before { content: '·'; margin: 0 0.5rem; color: var(--subtle); } -.entry-l2 { flex-shrink: 0; } -.entry-empty { text-align: center; color: var(--muted); padding: 5rem 1rem; } -.entry-empty-icon { font-size: 2.5rem; margin-bottom: 1rem; opacity: 0.5; } - -/* ============================================================ - APP — Entry Detail - ============================================================ */ - -.detail-wrap { max-width: 48rem; padding: var(--pad); } -.detail-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--border); } -.detail-back { color: var(--muted); transition: color 0.15s; font-size: 0.875rem; display: inline-flex; align-items: center; gap: 0.375rem; } -.detail-back:hover { color: var(--text); } -.detail-icon { width: 3.25rem; height: 1.75rem; border-radius: 0.25rem; background: rgba(100,140,200,0.12); display: flex; align-items: center; justify-content: center; font-size: 0.625rem; font-weight: 600; color: var(--muted); flex-shrink: 0; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.05em; } -.detail-icon.type-credential { background: rgba(74,222,128,0.1); color: var(--accent); } -.detail-icon.type-card { background: rgba(212,175,55,0.1); color: var(--gold); } -.detail-icon.type-identity { background: rgba(96,165,250,0.1); color: #60a5fa; } -.detail-icon.type-note { background: rgba(148,163,184,0.1); color: var(--muted); } -.detail-icon.type-ssh_key { background: rgba(239,68,68,0.1); color: var(--red); } -.detail-icon.type-totp { background: rgba(168,85,247,0.1); color: #a855f7; } -.detail-icon.type-folder { background: rgba(212,175,55,0.1); color: var(--gold); } -.detail-title { font-size: 1.25rem; font-weight: 700; } -.detail-type { font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--subtle); } -.detail-urls { background: rgba(100,140,200,0.08); border: 1px solid rgba(148,163,184,0.1); border-radius: var(--radius-sm); padding: 0.875rem; } -.detail-urls a { color: var(--gold); display: block; font-size: 0.875rem; transition: color 0.15s; } -.detail-urls a:hover { text-decoration: underline; opacity: 0.85; } -.detail-fields { display: flex; flex-direction: column; gap: 0.5rem; } -.detail-actions { display: flex; gap: 0.75rem; } - -/* --- Field box --- */ -.field-box { background: rgba(100,140,200,0.08); border: 1px solid rgba(148,163,184,0.1); border-radius: var(--radius-sm); padding: 0.875rem; transition: border-color 0.15s; } -.field-box:hover { border-color: rgba(148,163,184,0.25); } -.field-box.field-password { border-left: 3px solid var(--accent); } -.field-box.field-totp { border-left: 3px solid #a855f7; } -.field-box.field-l2 { border-left: 3px solid var(--gold); } -.field-label { font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--subtle); margin-bottom: 0.375rem; display: flex; align-items: center; gap: 0.5rem; } -.field-value { display: flex; align-items: center; gap: 0.5rem; } -.field-l2-locked { color: var(--gold); font-style: italic; font-size: 0.875rem; } -.field-l2-toggle { cursor: pointer; transition: opacity 0.15s; font-size: 0.625rem; } -.field-l2-toggle:hover { opacity: 0.7; } -.field-action { color: var(--subtle); font-size: 0.75rem; cursor: pointer; transition: color 0.15s; background: none; border: none; padding: 0.25rem; border-radius: 0.25rem; } -.field-action:hover { color: var(--text); background: rgba(255,255,255,0.05); } -.password-masked { font-family: var(--font-mono); letter-spacing: 0.1em; color: var(--muted); } -.notes-box { background: rgba(100,140,200,0.08); border: 1px solid rgba(148,163,184,0.1); border-radius: var(--radius-sm); padding: 0.875rem; white-space: pre-wrap; font-size: 0.875rem; color: var(--muted); line-height: 1.7; } -.notes-label { font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--subtle); margin-bottom: 0.375rem; } - -/* ============================================================ - APP — Modal - ============================================================ */ - -.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 50; } -.modal-box { background: var(--surface); border: 1px solid rgba(148,163,184,0.15); border-radius: var(--radius); box-shadow: 0 25px 50px rgba(0,0,0,0.5); max-width: 42rem; width: calc(100% - 2rem); max-height: 90vh; overflow-y: auto; } -.modal-body { padding: 1.75rem; } -.modal-title { font-size: 1.125rem; font-weight: 700; margin-bottom: 1.25rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border); } -.modal-actions { display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--border); } - -/* ============================================================ - APP — Forms - ============================================================ */ - -.form-group { margin-bottom: 1rem; } -.form-label { display: block; font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--subtle); margin-bottom: 0.375rem; } -.form-input, -.form-select, -.form-textarea { width: 100%; padding: 0.5rem 0.75rem; background: rgba(255,255,255,0.05); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); outline: none; transition: border-color 0.15s, box-shadow 0.15s; } -.form-input:focus, -.form-select:focus, -.form-textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(74,222,128,0.1); } -.form-input-sm { padding: 0.375rem 0.5rem; font-size: 0.8125rem; } -.form-row { display: flex; gap: 0.5rem; align-items: flex-start; } - -/* ============================================================ - APP — Drop Zone - ============================================================ */ - -.drop-zone { border: 2px dashed rgba(148,163,184,0.2); border-radius: var(--radius); padding: 2.5rem; text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; background: rgba(100,140,200,0.04); } -.drop-zone:hover { border-color: rgba(148,163,184,0.35); background: rgba(100,140,200,0.08); } -.drop-zone.active { border-color: var(--gold); background: rgba(212,175,55,0.05); } -.drop-zone-icon { font-size: 2.5rem; margin-bottom: 0.75rem; opacity: 0.7; } -.drop-zone-text { color: var(--muted); font-size: 0.875rem; } -.drop-zone-hint { color: var(--subtle); font-size: 0.75rem; margin-top: 0.5rem; } - -/* ============================================================ - APP — QR Scanner & TOTP Import - ============================================================ */ - -.import-divider { display: flex; align-items: center; gap: 1rem; margin: 0.5rem 0; color: var(--subtle); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; } -.import-divider::before, -.import-divider::after { content: ''; flex: 1; height: 1px; background: rgba(148,163,184,0.15); } - -.btn-qr-scan { display: flex; align-items: center; justify-content: center; gap: 0.5rem; width: 100%; padding: 0.875rem; background: rgba(100,140,200,0.08); border: 1px solid rgba(100,140,200,0.2); border-radius: var(--radius); color: var(--text); font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: background 0.2s, border-color 0.2s; } -.btn-qr-scan:hover { background: rgba(100,140,200,0.15); border-color: rgba(100,140,200,0.35); } -.btn-qr-scan svg { opacity: 0.7; } - -.qr-viewfinder { position: relative; border-radius: var(--radius); overflow: hidden; background: #000; aspect-ratio: 4/3; } -.qr-viewfinder video { width: 100%; height: 100%; object-fit: cover; } -.qr-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; } -.qr-frame { width: 60%; aspect-ratio: 1; border: 2px solid rgba(212,175,55,0.6); border-radius: 12px; box-shadow: 0 0 0 9999px rgba(0,0,0,0.4); } -.qr-status { text-align: center; padding: 0.75rem; font-size: 0.8rem; color: var(--muted); } - -.totp-import-list { display: flex; flex-direction: column; gap: 0.25rem; margin-bottom: 1rem; max-height: 300px; overflow-y: auto; } -.totp-import-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.625rem 0.75rem; border-radius: var(--radius); transition: opacity 0.2s; } -.totp-import-item.skipped { opacity: 0.35; } -.totp-import-info { flex: 1; min-width: 0; } -.totp-import-name { font-size: 0.875rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.totp-import-sub { font-size: 0.75rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - -.totp-tier-select { display: flex; gap: 2px; flex-shrink: 0; } -.tier-btn { padding: 0.2rem 0.55rem; font-size: 0.7rem; font-weight: 600; border: 1px solid rgba(148,163,184,0.15); border-radius: 4px; background: transparent; color: var(--muted); cursor: pointer; transition: all 0.15s; letter-spacing: 0.02em; } -.tier-btn:hover { border-color: rgba(148,163,184,0.3); color: var(--text); } -.tier-btn.active { background: rgba(100,140,200,0.15); border-color: rgba(100,140,200,0.4); color: var(--text); } -.tier-btn.tier-skip.active { background: rgba(148,163,184,0.08); border-color: rgba(148,163,184,0.2); color: var(--subtle); } - -/* ============================================================ - APP — Live TOTP Code - ============================================================ */ - -.totp-code { font-family: 'JetBrains Mono', monospace; font-size: 1.5rem; font-weight: 700; letter-spacing: 0.15em; color: var(--gold, #d4af37); } -.totp-countdown { font-size: 0.75rem; font-weight: 600; color: var(--muted); margin-left: 0.75rem; min-width: 2rem; } -.totp-countdown.totp-expiring { color: var(--red, #ef4444); animation: totp-pulse 0.5s ease-in-out infinite alternate; } -@keyframes totp-pulse { from { opacity: 1; } to { opacity: 0.4; } } -.field-totp .field-value { display: flex; align-items: center; } - -/* ============================================================ - APP — Import Preview - ============================================================ */ - -.import-summary { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; padding: 0.75rem; background: rgba(100,140,200,0.06); border-radius: var(--radius-sm); border: 1px solid var(--border); } -.import-summary label { cursor: pointer; user-select: none; display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.8125rem; } -.import-list { max-height: 18rem; overflow-y: auto; display: flex; flex-direction: column; gap: 0.375rem; } -.import-item { display: flex; align-items: center; gap: 0.625rem; padding: 0.625rem 0.75rem; background: rgba(100,140,200,0.08); border: 1px solid rgba(148,163,184,0.08); border-radius: var(--radius-sm); transition: background 0.15s; } -.import-item:hover { background: rgba(100,140,200,0.12); } -.import-item.faded { opacity: 0.35; } -.import-item-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; } - -/* ============================================================ - APP — Onboarding / Unlock - ============================================================ */ - -.onboard-wrap { min-height: 100%; display: flex; align-items: center; justify-content: center; padding: 1.5rem; } -.onboard-inner { max-width: 28rem; width: 100%; } -.onboard-heading { text-align: center; margin-bottom: 2rem; } -.onboard-heading h1 { margin-bottom: 0.5rem; } -.onboard-card { background: rgba(100,140,200,0.08); border: 1px solid rgba(148,163,184,0.15); border-radius: var(--radius); padding: 1.75rem; margin-bottom: 1.5rem; } -.onboard-card h2 { margin-bottom: 0.5rem; } -.device-list { display: flex; flex-direction: column; gap: 0.75rem; } -.device-option { width: 100%; display: flex; align-items: center; gap: 1rem; padding: 1rem; border-radius: var(--radius-sm); background: rgba(255,255,255,0.04); border: 1px solid var(--border); transition: border-color 0.2s, background 0.2s, transform 0.15s; cursor: pointer; text-align: left; } -.device-option:hover { border-color: var(--accent); background: rgba(74,222,128,0.04); transform: translateY(-1px); } -.device-option-icon { font-size: 1.5rem; width: 2.5rem; height: 2.5rem; text-align: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: rgba(100,140,200,0.1); border-radius: var(--radius-sm); } -.device-option strong { display: block; color: var(--text); } -.device-option span { color: var(--muted); font-size: 0.8125rem; } -.onboard-footer { text-align: center; font-size: 0.75rem; color: var(--subtle); } - -.unlock-wrap { min-height: 100%; display: flex; align-items: center; justify-content: center; padding: 1.5rem; } -.unlock-inner { max-width: 24rem; width: 100%; text-align: center; } -.unlock-inner h1 { margin-bottom: 0.5rem; } - -/* ============================================================ - APP — Toast - ============================================================ */ - -.toast { position: fixed; top: 1rem; right: 1rem; padding: 0.75rem 1.25rem; border-radius: var(--radius-sm); z-index: 60; font-weight: 600; font-size: 0.875rem; animation: slideIn 0.3s ease; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 8px 24px rgba(0,0,0,0.3); } -.toast.success { background: rgba(34,197,94,0.85); color: #fff; border: 1px solid rgba(34,197,94,0.5); } -.toast.error { background: rgba(239,68,68,0.85); color: #fff; border: 1px solid rgba(239,68,68,0.5); } -@keyframes slideIn { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } - -/* ============================================================ - APP — Audit Table - ============================================================ */ - -.audit-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; } -.audit-table th { text-align: left; color: var(--subtle); padding: 0.5rem 0.75rem; font-weight: 600; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; border-bottom: 1px solid var(--border); } -.audit-table td { padding: 0.625rem 0.75rem; color: var(--muted); } -.audit-table tr + tr { border-top: 1px solid rgba(255,255,255,0.04); } -.audit-table tbody tr:hover { background: rgba(255,255,255,0.03); } -.audit-scroll { max-height: 24rem; overflow-y: auto; } - -/* ============================================================ - APP — Utilities - ============================================================ */ - -.hidden { display: none !important; } -.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.text-gold { color: var(--gold); } -.text-red { color: var(--red); } -.text-accent { color: var(--accent); } -.text-muted { color: var(--muted); } -.text-subtle { color: var(--subtle); } -.text-center { text-align: center; } -.text-right { text-align: right; } -.cursor-pointer { cursor: pointer; } -.italic { font-style: italic; } -.font-mono { font-family: var(--font-mono); } -.select-all { user-select: all; } -.disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; } - -/* Alert boxes */ -.alert { padding: 0.75rem; border-radius: var(--radius-sm); font-size: 0.8125rem; } -.alert-error { background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.25); color: var(--red); } -.alert-warning { background: rgba(245,158,11,0.12); border: 1px solid rgba(245,158,11,0.25); color: #f59e0b; } - -/* ============================================================ - APP — Config Snippets (token page) - ============================================================ */ - -.config-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 0; } -.config-tab { padding: 0.5rem 1rem; font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em; color: var(--subtle); background: none; border: none; border-bottom: 2px solid transparent; cursor: pointer; transition: color 0.15s, border-color 0.15s; } -.config-tab:hover { color: var(--text); } -.config-tab.active { color: var(--gold); border-bottom-color: var(--gold); } -.config-block { position: relative; background: rgba(0,0,0,0.35); border: 1px solid var(--border); border-top: none; border-radius: 0 0 var(--radius-sm) var(--radius-sm); padding: 1rem 1.25rem; font-family: var(--font-mono); font-size: 0.8rem; line-height: 1.65; color: var(--muted); overflow-x: auto; white-space: pre; } -.config-copy { position: absolute; top: 0.5rem; right: 0.5rem; font-family: var(--font-sans); font-size: 0.75rem; font-weight: 600; padding: 0.25rem 0.625rem; border-radius: 0.25rem; background: rgba(255,255,255,0.08); color: var(--subtle); border: 1px solid var(--border); cursor: pointer; transition: color 0.15s, background 0.15s; } -.config-copy:hover { color: var(--text); background: rgba(255,255,255,0.12); } - -/* ============================================================ - APP — Scrollbar (subtle, matching dark theme) - ============================================================ */ -::-webkit-scrollbar { width: 6px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: rgba(148,163,184,0.15); border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: rgba(148,163,184,0.25); } - -/* ============================================================ - SITE — Marketing / Landing Pages - ============================================================ */ -.site { min-height: 100vh; display: flex; flex-direction: column; } -.site-column { max-width: 64rem; margin: 0 auto; width: 100%; padding: 0 1rem; } - -/* Hero */ -.hero { text-align: center; margin-bottom: 5rem; } -.hero-title { font-size: clamp(2.5rem, 6vw, 4rem); font-weight: 800; line-height: 1.1; margin-bottom: 1.5rem; letter-spacing: -0.03em; } -.hero-sub { font-size: 1.125rem; color: var(--muted); max-width: 40rem; margin: 0 auto 2rem; line-height: 1.7; } -.hero-actions { display: flex; gap: 1rem; justify-content: center; } - -/* Features */ -.features { margin-bottom: 5rem; } -.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1.25rem; } -.feature-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 1.5rem; } -.feature-card h3 { font-size: 1rem; font-weight: 700; margin: 0.5rem 0; } -.feature-card p { font-size: 0.875rem; color: var(--muted); line-height: 1.6; margin: 0; } -.feature-label { font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--subtle); } -.feature-label.accent { color: var(--accent); } - -/* How it works */ -.how-it-works { margin-bottom: 5rem; } -.how-it-works h2, -.l2-demo h2 { font-size: 1.5rem; font-weight: 700; margin-bottom: 1.5rem; } -.code-examples { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1.25rem; } -.code-block { background: rgba(0,0,0,0.3); border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; } -.code-label { font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600; padding: 0.625rem 1rem; background: rgba(255,255,255,0.03); border-bottom: 1px solid var(--border); color: var(--subtle); text-transform: uppercase; letter-spacing: 0.05em; } -.code-block pre { margin: 0; padding: 1rem; overflow-x: auto; } -.code-block code { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--muted); line-height: 1.6; } - -/* L2 demo */ -.l2-demo { margin-bottom: 5rem; } - -/* Footer */ -.site-footer { margin-top: auto; padding: 2rem 1rem; border-top: 1px solid var(--border); } -.site-footer .site-column { display: flex; align-items: center; gap: 1.5rem; } -.footer-text { font-size: 0.8125rem; color: var(--subtle); font-family: var(--font-mono); } - -/* Small button variant */ -.btn-sm { padding: 0.375rem 0.875rem; font-size: 0.8125rem; } diff --git a/clavis/clavis-vault/_old/cmd/clavitor/web/clavitor.css b/clavis/clavis-vault/_old/cmd/clavitor/web/clavitor.css deleted file mode 100644 index b1ad2c6..0000000 --- a/clavis/clavis-vault/_old/cmd/clavitor/web/clavitor.css +++ /dev/null @@ -1,199 +0,0 @@ -/* ============================================================ - clavitor — global stylesheet - ONE rule per class. No exceptions. No inline styles. - All layout, spacing, color and type lives here. - ============================================================ */ - -/* === TOKENS === */ -:root { - --width: 1280px; - --pad: 2rem; - --radius: 1rem; - --radius-sm: 0.5rem; - --gap: 1.25rem; - - --bg: #0d1520; - --surface: #142542; - --surface-alt: #0e2414; - --surface-gold: #2a1f00; - - --border: rgba(255,255,255,0.09); - --border-gold: rgba(212,175,55,0.3); - - --text: #f1f5f9; - --muted: #94a3b8; - --subtle: #64748b; - - --accent: #4ade80; - --gold: #D4AF37; - --red: #EF4444; - - --font-sans: Inter, sans-serif; - --font-mono: 'JetBrains Mono', monospace; -} - -/* === RESET === */ -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -body { background: var(--bg); color: var(--text); font-family: var(--font-sans); line-height: 1.6; } -a { color: inherit; text-decoration: none; } -img, svg { display: block; max-width: 100%; } - -/* === LAYOUT === */ -.container { max-width: var(--width); margin: 0 auto; padding: 0 var(--pad); } -.section { padding-top: 5rem; padding-bottom: 5rem; } -hr.divider { border: none; border-top: 1px solid var(--border); } - -/* === TYPOGRAPHY === */ -h1 { font-size: clamp(2rem, 4vw, 3.5rem); font-weight: 800; line-height: 1.1; color: var(--text); } -h2 { font-size: clamp(1.5rem, 3vw, 2.25rem); font-weight: 700; line-height: 1.2; color: var(--text); } -h3 { font-size: 1.2rem; font-weight: 700; line-height: 1.3; color: var(--text); } -p { color: var(--muted); font-size: 1rem; line-height: 1.75; } -p.lead { font-size: 1.125rem; } - -/* === LABELS === */ -.label { font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--subtle); } -.label.accent { color: var(--accent); } -.label.gold { color: var(--gold); } -.label.red { color: var(--red); } - -/* === VAULTNAME === */ -.vaultname { font-family: var(--font-mono); font-weight: 700; color: var(--text); } -.vaultname .n { color: var(--accent); } - -/* === CARDS === */ -.card { background: rgba(100,140,200,0.18); border: 1px solid rgba(148,163,184,0.22); border-radius: var(--radius); padding: 1.75rem; } -.card.alt { background: rgba(34,197,94,0.15); border-color: rgba(34,197,94,0.3); } -.card.gold { background: rgba(212,175,55,0.15); border-color: rgba(212,175,55,0.35); } -.card.red { background: rgba(239,68,68,0.15); border-color: rgba(239,68,68,0.3); } -.card-hover { transition: transform 0.2s, box-shadow 0.2s; border-color: rgba(255,255,255,0.12); } -.card-hover:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.3); } - -/* === GRID === */ -.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); } -.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--gap); } - -/* === BUTTONS === */ -.btn { display: inline-block; font-family: var(--font-sans); font-size: 0.875rem; font-weight: 600; padding: 0.625rem 1.25rem; border-radius: var(--radius-sm); border: 1px solid transparent; cursor: pointer; transition: opacity 0.15s; text-align: center; } -.btn:hover { opacity: 1; filter: brightness(1.15); } -.btn-primary { background: var(--accent); color: var(--bg); border-color: var(--accent); } -.btn-ghost { background: transparent; color: var(--text); border-color: rgba(74,222,128,0.4); } -.btn-accent { background: rgba(34,197,94,0.15); color: var(--accent); border-color: rgba(34,197,94,0.3); } -.btn-gold { background: rgba(212,175,55,0.15); color: var(--gold); border-color: rgba(212,175,55,0.3); } -.btn-red { background: rgba(239,68,68,0.15); color: var(--red); border-color: rgba(239,68,68,0.3); } -.btn-block { display: block; width: 100%; } -.btn-row { display: flex; flex-wrap: wrap; gap: 1rem; } - -/* === HERO === */ -.hero { padding-top: 100px; padding-bottom: 4rem; text-align: center; } -.hero h1 { margin-bottom: 1rem; } -.hero p.lead { max-width: 600px; margin-left: auto; margin-right: auto; } -.hero-split { padding-top: 100px; padding-bottom: 4rem; display: grid; grid-template-columns: 1fr 1fr; gap: 4rem; align-items: center; } - -/* === MAP === */ -.map-wrap { border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); } -.map-wrap svg { display: block; width: 100%; background: var(--bg); } -.map-gap { height: 1rem; } - -/* === DC GRID (3 action cards below map) === */ -#dc-grid { display: flex; gap: var(--gap); } -#dc-grid .dc-card { flex: 1; min-width: 0; border-radius: var(--radius); padding: 1rem; text-align: center; background: var(--surface); border: 1px solid var(--border); } -#dc-grid .dc-card.gold { background: var(--surface-gold); border-color: var(--border-gold); } -#dc-grid .dc-card.red { background: #1a0505; border-color: rgba(239,68,68,0.3); } -#dc-grid .dc-icon { font-size: 1.5rem; margin-bottom: 0.375rem; } -#dc-grid .dc-name { font-size: 0.875rem; font-weight: 600; color: var(--text); margin-bottom: 0.25rem; } -#dc-grid .dc-sub { font-size: 0.75rem; color: var(--subtle); margin-bottom: 0.625rem; } -#dc-grid .dc-status { display: flex; align-items: center; justify-content: center; gap: 0.375rem; font-size: 0.75rem; color: var(--subtle); margin-bottom: 0.75rem; } -#dc-grid .dc-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; } - -/* === SPACING === */ -.mt-2 { margin-top: 0.5rem; } .mb-2 { margin-bottom: 0.5rem; } -.mt-3 { margin-top: 0.75rem; } .mb-3 { margin-bottom: 0.75rem; } -.mt-4 { margin-top: 1rem; } .mb-4 { margin-bottom: 1rem; } -.mt-6 { margin-top: 1.5rem; } .mb-6 { margin-bottom: 1.5rem; } -.mt-8 { margin-top: 2rem; } .mb-8 { margin-bottom: 2rem; } -.mt-12 { margin-top: 3rem; } .mb-12 { margin-bottom: 3rem; } - -/* === ANIMATIONS === */ -@keyframes hostedPulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:0.3; transform:scale(1.8); } } - -/* === NAV === */ -.nav { position: fixed; top: 0; width: 100%; z-index: 50; background: rgba(10,22,40,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); } -.nav-inner { max-width: var(--width); margin: 0 auto; padding: 0 var(--pad); height: 64px; display: flex; align-items: center; justify-content: space-between; } -.nav-logo { font-family: var(--font-mono); font-weight: 700; font-size: 2rem; color: var(--text); line-height: 1; letter-spacing: -0.02em; } -.nav-logo .n { color: var(--accent); } -.nav-links { display: flex; align-items: center; gap: 1.5rem; font-size: 0.875rem; } -.nav-link { color: var(--muted); transition: color 0.15s; } -.nav-link:hover { color: var(--text); } -.nav-link.active { color: var(--gold); font-weight: 600; display: flex; align-items: center; gap: 0.375rem; } -.nav-link.active::before { content:''; display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--gold); animation: hostedPulse 2s ease-in-out infinite; } - -/* --- Nav dropdown --- */ -.nav-dropdown { position: relative; } -.nav-dropdown > a { cursor: pointer; } -.nav-dropdown-menu { display: none; position: absolute; top: 100%; right: 0; padding-top: 0.75rem; min-width: 10rem; z-index: 100; } -.nav-dropdown-menu-inner { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.5rem 0; box-shadow: 0 8px 24px rgba(0,0,0,0.4); } -.nav-dropdown-menu a { display: block; padding: 0.5rem 1rem; color: var(--muted); font-size: 0.8125rem; transition: color 0.15s, background 0.15s; } -.nav-dropdown-menu a:hover { color: var(--text); background: rgba(255,255,255,0.05); } -.nav-dropdown:hover .nav-dropdown-menu { display: block; } - -/* === GRADIENT TEXT === */ -.gradient-text { background: linear-gradient(135deg, #22C55E 0%, #4ade80 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } - -/* === CODE BLOCKS === */ -.code-block { background: rgba(0,0,0,0.6); border: 1px solid rgba(148,163,184,0.2); border-radius: var(--radius); padding: 1.5rem; font-family: var(--font-mono); font-size: 0.875rem; overflow-x: auto; line-height: 1.7; } -.code-block .prompt { color: var(--accent); } -.code-block .comment { color: var(--subtle); } -.code-label { font-size: 0.75rem; color: var(--subtle); margin-bottom: 0.75rem; font-family: var(--font-sans); } -.code-block pre { margin: 0; color: var(--muted); } -pre, code { font-family: var(--font-mono); } - -/* === FEATURE ICON === */ -.feature-icon { width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; background: rgba(34,197,94,0.1); display: flex; align-items: center; justify-content: center; margin-bottom: 1.25rem; flex-shrink: 0; } -.feature-icon svg { width: 1.25rem; height: 1.25rem; color: var(--accent); stroke: var(--accent); } -.feature-icon.red { background: rgba(239,68,68,0.1); } -.feature-icon.red svg { color: var(--red); stroke: var(--red); } - -/* === CHECK LIST === */ -.checklist { list-style: none; } -.checklist li { display: flex; align-items: flex-start; gap: 0.75rem; font-size: 0.875rem; color: var(--muted); margin-bottom: 0.75rem; } -.checklist li::before { content: ''; width: 1rem; height: 1rem; flex-shrink: 0; background: var(--accent); border-radius: 50%; margin-top: 0.125rem; clip-path: polygon(20% 50%, 40% 70%, 80% 25%, 85% 30%, 40% 80%, 15% 55%); } -.checklist.red li::before { background: var(--red); } - -/* === FOOTER === */ -.footer { border-top: 1px solid var(--border); padding: 3rem 0; } -.footer-inner { display: flex; flex-direction: row; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1.5rem; } -.footer-links { display: flex; align-items: center; gap: 1rem; font-size: 0.875rem; color: var(--subtle); } -.footer-links a { color: var(--subtle); transition: color 0.15s; } -.footer-links a:hover { color: var(--muted); } -.footer-copy { text-align: center; font-size: 0.75rem; color: var(--subtle); margin-top: 2rem; } - -/* === PROSE (legal pages) === */ -.prose h2 { font-size: 1.375rem; font-weight: 700; color: var(--text); margin: 2.5rem 0 1rem; } -.prose h3 { font-size: 1.1rem; font-weight: 600; color: var(--text); margin: 1.75rem 0 0.75rem; } -.prose p { color: var(--muted); line-height: 1.8; margin-bottom: 1rem; } -.prose ul { color: var(--muted); padding-left: 1.5rem; margin-bottom: 1rem; line-height: 1.8; } -.prose a { color: var(--accent); } -.prose a:hover { text-decoration: underline; } - -/* === BADGE === */ -.badge { display: inline-block; font-family: var(--font-mono); font-size: 0.7rem; font-weight: 600; padding: 0.25rem 0.625rem; border-radius: 9999px; } -.badge.accent { background: rgba(34,197,94,0.15); color: var(--accent); border: 1px solid rgba(34,197,94,0.3); } -.badge.gold { background: rgba(212,175,55,0.15); color: var(--gold); border: 1px solid rgba(212,175,55,0.3); } -.badge.recommended { background: var(--accent); color: var(--bg); } -.badge.red { background: rgba(239,68,68,0.15); color: var(--red); border: 1px solid rgba(239,68,68,0.3); } - -/* === PRICING CARDS === */ -.price-card { border-radius: var(--radius); border: 1px solid var(--border); padding: 2.5rem; background: rgba(100,140,200,0.08); } -.price-card.featured { border-color: rgba(34,197,94,0.4); background: rgba(34,197,94,0.06); } -.price-amount { font-size: 3rem; font-weight: 800; color: var(--text); line-height: 1; } -.price-period { font-size: 1rem; color: var(--muted); font-weight: 400; } - -/* === SCROLL === */ -html { scroll-behavior: smooth; } - -/* === ANIMATIONS (pulse for map/decorative) === */ -@keyframes pulseDot { 0%,100% { transform:scale(1); } 50% { transform:scale(1.15); } } -@keyframes pulseRing { 0% { transform:scale(0.8); opacity:1; } 100% { transform:scale(2.5); opacity:0; } } -.pulse-dot { animation: pulseDot 2s ease-in-out infinite; } -.pulse-ring { animation: pulseRing 2s ease-out infinite; } -.pulse-ring-2 { animation: pulseRing 2s ease-out infinite 0.5s; } diff --git a/clavis/clavis-vault/_old/cmd/clavitor/web/crypto.js b/clavis/clavis-vault/_old/cmd/clavitor/web/crypto.js deleted file mode 100644 index 8125a3d..0000000 --- a/clavis/clavis-vault/_old/cmd/clavitor/web/crypto.js +++ /dev/null @@ -1,219 +0,0 @@ -/* - * clavitor — shared crypto module - * Runs in both QuickJS (CLI) and browser (extension). - * - * In CLI (QuickJS): native_* functions provided by jsbridge.c via BearSSL. - * All calls are synchronous. - * In browser: Web Crypto API used directly (async). - * - * This file is the single source of truth for L2/L3 field crypto. - */ - -/* Detect environment */ -const IS_BROWSER = typeof globalThis.crypto !== 'undefined' - && typeof globalThis.crypto.subtle !== 'undefined'; - -/* --- base64 helpers --- */ - -function uint8_to_base64(bytes) { - if (IS_BROWSER) { - return btoa(String.fromCharCode.apply(null, bytes)); - } else { - return native_base64_encode(bytes); - } -} - -function base64_to_uint8(str) { - if (IS_BROWSER) { - const bin = atob(str); - const bytes = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); - return bytes; - } else { - return native_base64_decode(str); - } -} - -/* --- AES-GCM --- */ - -/** - * Encrypt plaintext with AES-GCM. - * @param {Uint8Array} key - 16 bytes (AES-128) or 32 bytes (AES-256) - * @param {Uint8Array} plaintext - * @returns {Uint8Array|Promise} nonce(12) || ciphertext || tag(16) - */ -function aes_gcm_encrypt(key, plaintext) { - if (IS_BROWSER) { - const iv = crypto.getRandomValues(new Uint8Array(12)); - return crypto.subtle.importKey( - 'raw', key, { name: 'AES-GCM' }, false, ['encrypt'] - ).then(function(cryptoKey) { - return crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, cryptoKey, plaintext); - }).then(function(ct) { - const result = new Uint8Array(12 + ct.byteLength); - result.set(iv, 0); - result.set(new Uint8Array(ct), 12); - return result; - }); - } else { - /* QuickJS: synchronous BearSSL binding */ - return native_aes_gcm_encrypt(key, plaintext); - } -} - -/** - * Decrypt AES-GCM ciphertext. - * @param {Uint8Array} key - 16 or 32 bytes - * @param {Uint8Array} data - nonce(12) || ciphertext || tag(16) - * @returns {Uint8Array|Promise} plaintext - */ -function aes_gcm_decrypt(key, data) { - if (data.length < 28) throw new Error('ciphertext too short'); - - /* Use subarray for typed array compatibility (QuickJS) */ - var iv, ct; - if (typeof data.subarray === 'function') { - iv = new Uint8Array(data.subarray(0, 12)); - ct = new Uint8Array(data.subarray(12)); - } else { - iv = data.slice(0, 12); - ct = data.slice(12); - } - - if (IS_BROWSER) { - return crypto.subtle.importKey( - 'raw', key, { name: 'AES-GCM' }, false, ['decrypt'] - ).then(function(cryptoKey) { - return crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, cryptoKey, ct); - }).then(function(pt) { - return new Uint8Array(pt); - }); - } else { - /* Pass full data blob to native — C splits nonce/ct internally */ - return native_aes_gcm_decrypt_blob(key, data); - } -} - -/* --- HKDF-SHA256 --- */ - -/** - * HKDF-SHA256 extract + expand. - * @param {Uint8Array} ikm - input key material - * @param {Uint8Array|null} salt - optional salt - * @param {Uint8Array} info - context info - * @param {number} length - output length in bytes - * @returns {Uint8Array|Promise} - */ -function hkdf_sha256(ikm, salt, info, length) { - if (IS_BROWSER) { - return crypto.subtle.importKey( - 'raw', ikm, 'HKDF', false, ['deriveBits'] - ).then(function(cryptoKey) { - return crypto.subtle.deriveBits( - { name: 'HKDF', hash: 'SHA-256', salt: salt || new Uint8Array(0), info: info }, - cryptoKey, length * 8 - ); - }).then(function(bits) { - return new Uint8Array(bits); - }); - } else { - return native_hkdf_sha256(ikm, salt, info, length); - } -} - -/* --- Field encryption/decryption --- */ - -/** - * Encrypt a field value. - * Key length determines tier: 16 bytes = L2 (AES-128), 32 bytes = L3 (AES-256). - * @param {Uint8Array} key - 16 or 32 bytes - * @param {string} field_label - field label (for per-field key derivation) - * @param {string} plaintext - field value to encrypt - * @returns {string|Promise} base64-encoded ciphertext - */ -/* - * Normalize key for AES: 8-byte keys are doubled to 16 bytes. - * AES requires 16, 24, or 32 byte keys. - * HKDF output length matches the (normalized) key length. - */ -function normalize_key(key) { - if (key.length === 8) { - var doubled = new Uint8Array(16); - doubled.set(key, 0); - doubled.set(key, 8); - return doubled; - } - return key; -} - -function encrypt_field(key, field_label, plaintext) { - var info_str = 'clavitor-field-' + field_label; - var nkey = normalize_key(key); - var aes_len = nkey.length; /* 16 or 32 */ - - if (IS_BROWSER) { - var enc = new TextEncoder(); - var info = enc.encode(info_str); - return hkdf_sha256(nkey, null, info, aes_len).then(function(field_key) { - return aes_gcm_encrypt(field_key, enc.encode(plaintext)); - }).then(function(ct) { - return uint8_to_base64(ct); - }); - } else { - var info = native_encode_utf8(info_str); - var field_key = native_hkdf_sha256(nkey, null, info, aes_len); - var pt_bytes = native_encode_utf8(plaintext); - var ct = native_aes_gcm_encrypt(field_key, pt_bytes); - return native_base64_encode(ct); - } -} - -/** - * Decrypt a field value. - * Key length determines tier: 16 bytes = L2, 32 bytes = L3. - * @param {Uint8Array} key - 16 or 32 bytes - * @param {string} field_label - field label - * @param {string} ciphertext_b64 - base64-encoded ciphertext - * @returns {string|Promise} plaintext - */ -function decrypt_field(key, field_label, ciphertext_b64) { - var info_str = 'clavitor-field-' + field_label; - var nkey = normalize_key(key); - var aes_len = nkey.length; - - if (IS_BROWSER) { - var enc = new TextEncoder(); - var dec = new TextDecoder(); - var info = enc.encode(info_str); - return hkdf_sha256(nkey, null, info, aes_len).then(function(field_key) { - var ct = base64_to_uint8(ciphertext_b64); - return aes_gcm_decrypt(field_key, ct); - }).then(function(pt) { - return dec.decode(pt); - }); - } else { - var info = native_encode_utf8(info_str); - var field_key = native_hkdf_sha256(nkey, null, info, aes_len); - var ct = native_base64_decode(ciphertext_b64); - var pt = native_aes_gcm_decrypt_blob(field_key, ct); - return native_decode_utf8(pt); - } -} - -/* Backward compat aliases */ -function l2_encrypt_field(key, entry_id, label, pt) { return encrypt_field(key, label, pt); } -function l2_decrypt_field(key, entry_id, label, ct) { return decrypt_field(key, label, ct); } - -/* Export for both environments */ -if (typeof globalThis.clavitor === 'undefined') globalThis.clavitor = {}; -globalThis.clavitor.crypto = { - aes_gcm_encrypt: aes_gcm_encrypt, - aes_gcm_decrypt: aes_gcm_decrypt, - hkdf_sha256: hkdf_sha256, - encrypt_field: encrypt_field, - decrypt_field: decrypt_field, - l2_encrypt_field: l2_encrypt_field, - l2_decrypt_field: l2_decrypt_field, - uint8_to_base64: uint8_to_base64, - base64_to_uint8: base64_to_uint8 -}; diff --git a/clavis/clavis-vault/_old/cmd/clavitor/web/jsqr.min.js b/clavis/clavis-vault/_old/cmd/clavitor/web/jsqr.min.js deleted file mode 100644 index 0fba6cd..0000000 --- a/clavis/clavis-vault/_old/cmd/clavitor/web/jsqr.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Minified by jsDelivr using Terser v5.37.0. - * Original file: /npm/jsqr@1.4.0/dist/jsQR.js - * - * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files - */ -!function(o,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.jsQR=e():o.jsQR=e()}("undefined"!=typeof self?self:this,(function(){return function(o){var e={};function r(t){if(e[t])return e[t].exports;var c=e[t]={i:t,l:!1,exports:{}};return o[t].call(c.exports,c,c.exports,r),c.l=!0,c.exports}return r.m=o,r.c=e,r.d=function(o,e,t){r.o(o,e)||Object.defineProperty(o,e,{configurable:!1,enumerable:!0,get:t})},r.n=function(o){var e=o&&o.__esModule?function(){return o.default}:function(){return o};return r.d(e,"a",e),e},r.o=function(o,e){return Object.prototype.hasOwnProperty.call(o,e)},r.p="",r(r.s=3)}([function(o,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=function(){function o(o,e){this.width=e,this.height=o.length/e,this.data=o}return o.createEmpty=function(e,r){return new o(new Uint8ClampedArray(e*r),e)},o.prototype.get=function(o,e){return!(o<0||o>=this.width||e<0||e>=this.height)&&!!this.data[e*this.width+o]},o.prototype.set=function(o,e,r){this.data[e*this.width+o]=r?1:0},o.prototype.setRegion=function(o,e,r,t,c){for(var s=e;s=this.size&&(c=(c^this.primitive)&this.size-1);for(s=0;s1&&0===e[0]){for(var t=1;ts.length&&(c=(r=[s,c])[0],s=r[1]);for(var a=new Uint8ClampedArray(s.length),n=s.length-c.length,d=0;dr?r:o}var s=function(){function o(o,e){this.width=o,this.data=new Uint8ClampedArray(o*e)}return o.prototype.get=function(o,e){return this.data[e*this.width+o]},o.prototype.set=function(o,e,r){this.data[e*this.width+o]=r},o}();e.binarize=function(o,e,r,a){if(o.length!==e*r*4)throw new Error("Malformed data passed to binarizer.");for(var n=new s(e,r),d=0;d0&&w>0)){var b=(m.get(w,f-1)+2*m.get(w-1,f)+m.get(w-1,f-1))/4;v6&&(r.setRegion(e-11,0,3,6,!0),r.setRegion(0,e-11,6,3,!0)),r}(e),n=[],l=0,B=0,k=!0,u=s-1;u>0;u-=2){6===u&&u--;for(var C=0;C=0;c--)for(var s=e-9;s>=e-11;s--)t=d(o.get(s,c),t);var l=0;for(s=5;s>=0;s--)for(c=e-9;c>=e-11;c--)l=d(o.get(s,c),l);for(var i,B=1/0,k=0,u=a.VERSIONS;k=0;t--)6!==t&&(e=d(o.get(8,t),e));var c=o.height,s=0;for(t=c-1;t>=c-7;t--)s=d(o.get(8,t),s);for(r=c-8;r1){var i=t.ecBlocks[0].numBlocks,B=t.ecBlocks[1].numBlocks;for(n=0;n0;)for(var k=0,u=c;k=3;){if((l=o.readBits(10))>=1e3)throw new Error("Invalid numeric value above 999");var a=Math.floor(l/100),n=Math.floor(l/10)%10,d=l%10;r.push(48+a,48+n,48+d),t+=a.toString()+n.toString()+d.toString(),s-=3}if(2===s){if((l=o.readBits(7))>=100)throw new Error("Invalid numeric value above 99");a=Math.floor(l/10),n=l%10;r.push(48+a,48+n),t+=a.toString()+n.toString()}else if(1===s){var l;if((l=o.readBits(4))>=10)throw new Error("Invalid numeric value above 9");r.push(48+l),t+=l.toString()}return{bytes:r,text:t}}!function(o){o.Numeric="numeric",o.Alphanumeric="alphanumeric",o.Byte="byte",o.Kanji="kanji",o.ECI="eci"}(t=e.Mode||(e.Mode={})),function(o){o[o.Terminator=0]="Terminator",o[o.Numeric=1]="Numeric",o[o.Alphanumeric=2]="Alphanumeric",o[o.Byte=4]="Byte",o[o.Kanji=8]="Kanji",o[o.ECI=7]="ECI"}(c||(c={}));var d=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function l(o,e){for(var r=[],t="",c=[9,11,13][e],s=o.readBits(c);s>=2;){var a=o.readBits(11),n=Math.floor(a/45),l=a%45;r.push(d[n].charCodeAt(0),d[l].charCodeAt(0)),t+=d[n]+d[l],s-=2}if(1===s){n=o.readBits(6);r.push(d[n].charCodeAt(0)),t+=d[n]}return{bytes:r,text:t}}function i(o,e){for(var r=[],t="",c=[8,16,16][e],s=o.readBits(c),a=0;a>8,255&l),t+=String.fromCharCode(a.shiftJISTable[l])}return{bytes:r,text:t}}e.decode=function(o,e){for(var r,a,d,k,u=new s.BitStream(o),C=e<=9?0:e<=26?1:2,m={text:"",bytes:[],chunks:[],version:e};u.available()>=4;){var f=u.readBits(4);if(f===c.Terminator)return m;if(f===c.ECI)0===u.readBits(1)?m.chunks.push({type:t.ECI,assignmentNumber:u.readBits(7)}):0===u.readBits(1)?m.chunks.push({type:t.ECI,assignmentNumber:u.readBits(14)}):0===u.readBits(1)?m.chunks.push({type:t.ECI,assignmentNumber:u.readBits(21)}):m.chunks.push({type:t.ECI,assignmentNumber:-1});else if(f===c.Numeric){var w=n(u,C);m.text+=w.text,(r=m.bytes).push.apply(r,w.bytes),m.chunks.push({type:t.Numeric,text:w.text})}else if(f===c.Alphanumeric){var P=l(u,C);m.text+=P.text,(a=m.bytes).push.apply(a,P.bytes),m.chunks.push({type:t.Alphanumeric,text:P.text})}else if(f===c.Byte){var v=i(u,C);m.text+=v.text,(d=m.bytes).push.apply(d,v.bytes),m.chunks.push({type:t.Byte,bytes:v.bytes,text:v.text})}else if(f===c.Kanji){var h=B(u,C);m.text+=h.text,(k=m.bytes).push.apply(k,h.bytes),m.chunks.push({type:t.Kanji,bytes:h.bytes,text:h.text})}}if(0===u.available()||0===u.readBits(u.available()))return m}},function(o,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=function(){function o(o){this.byteOffset=0,this.bitOffset=0,this.bytes=o}return o.prototype.readBits=function(o){if(o<1||o>32||o>this.available())throw new Error("Cannot read "+o.toString()+" bits");var e=0;if(this.bitOffset>0){var r=8-this.bitOffset,t=o>8-t<<(s=r-t);e=(this.bytes[this.byteOffset]&c)>>s,o-=t,this.bitOffset+=t,8===this.bitOffset&&(this.bitOffset=0,this.byteOffset++)}if(o>0){for(;o>=8;)e=e<<8|255&this.bytes[this.byteOffset],this.byteOffset++,o-=8;if(o>0){var s;c=255>>(s=8-o)<>s,this.bitOffset+=o}}return e},o.prototype.available=function(){return 8*(this.bytes.length-this.byteOffset)-this.bitOffset},o}();e.BitStream=t},function(o,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.shiftJISTable={32:32,33:33,34:34,35:35,36:36,37:37,38:38,39:39,40:40,41:41,42:42,43:43,44:44,45:45,46:46,47:47,48:48,49:49,50:50,51:51,52:52,53:53,54:54,55:55,56:56,57:57,58:58,59:59,60:60,61:61,62:62,63:63,64:64,65:65,66:66,67:67,68:68,69:69,70:70,71:71,72:72,73:73,74:74,75:75,76:76,77:77,78:78,79:79,80:80,81:81,82:82,83:83,84:84,85:85,86:86,87:87,88:88,89:89,90:90,91:91,92:165,93:93,94:94,95:95,96:96,97:97,98:98,99:99,100:100,101:101,102:102,103:103,104:104,105:105,106:106,107:107,108:108,109:109,110:110,111:111,112:112,113:113,114:114,115:115,116:116,117:117,118:118,119:119,120:120,121:121,122:122,123:123,124:124,125:125,126:8254,33088:12288,33089:12289,33090:12290,33091:65292,33092:65294,33093:12539,33094:65306,33095:65307,33096:65311,33097:65281,33098:12443,33099:12444,33100:180,33101:65344,33102:168,33103:65342,33104:65507,33105:65343,33106:12541,33107:12542,33108:12445,33109:12446,33110:12291,33111:20189,33112:12293,33113:12294,33114:12295,33115:12540,33116:8213,33117:8208,33118:65295,33119:92,33120:12316,33121:8214,33122:65372,33123:8230,33124:8229,33125:8216,33126:8217,33127:8220,33128:8221,33129:65288,33130:65289,33131:12308,33132:12309,33133:65339,33134:65341,33135:65371,33136:65373,33137:12296,33138:12297,33139:12298,33140:12299,33141:12300,33142:12301,33143:12302,33144:12303,33145:12304,33146:12305,33147:65291,33148:8722,33149:177,33150:215,33152:247,33153:65309,33154:8800,33155:65308,33156:65310,33157:8806,33158:8807,33159:8734,33160:8756,33161:9794,33162:9792,33163:176,33164:8242,33165:8243,33166:8451,33167:65509,33168:65284,33169:162,33170:163,33171:65285,33172:65283,33173:65286,33174:65290,33175:65312,33176:167,33177:9734,33178:9733,33179:9675,33180:9679,33181:9678,33182:9671,33183:9670,33184:9633,33185:9632,33186:9651,33187:9650,33188:9661,33189:9660,33190:8251,33191:12306,33192:8594,33193:8592,33194:8593,33195:8595,33196:12307,33208:8712,33209:8715,33210:8838,33211:8839,33212:8834,33213:8835,33214:8746,33215:8745,33224:8743,33225:8744,33226:172,33227:8658,33228:8660,33229:8704,33230:8707,33242:8736,33243:8869,33244:8978,33245:8706,33246:8711,33247:8801,33248:8786,33249:8810,33250:8811,33251:8730,33252:8765,33253:8733,33254:8757,33255:8747,33256:8748,33264:8491,33265:8240,33266:9839,33267:9837,33268:9834,33269:8224,33270:8225,33271:182,33276:9711,33359:65296,33360:65297,33361:65298,33362:65299,33363:65300,33364:65301,33365:65302,33366:65303,33367:65304,33368:65305,33376:65313,33377:65314,33378:65315,33379:65316,33380:65317,33381:65318,33382:65319,33383:65320,33384:65321,33385:65322,33386:65323,33387:65324,33388:65325,33389:65326,33390:65327,33391:65328,33392:65329,33393:65330,33394:65331,33395:65332,33396:65333,33397:65334,33398:65335,33399:65336,33400:65337,33401:65338,33409:65345,33410:65346,33411:65347,33412:65348,33413:65349,33414:65350,33415:65351,33416:65352,33417:65353,33418:65354,33419:65355,33420:65356,33421:65357,33422:65358,33423:65359,33424:65360,33425:65361,33426:65362,33427:65363,33428:65364,33429:65365,33430:65366,33431:65367,33432:65368,33433:65369,33434:65370,33439:12353,33440:12354,33441:12355,33442:12356,33443:12357,33444:12358,33445:12359,33446:12360,33447:12361,33448:12362,33449:12363,33450:12364,33451:12365,33452:12366,33453:12367,33454:12368,33455:12369,33456:12370,33457:12371,33458:12372,33459:12373,33460:12374,33461:12375,33462:12376,33463:12377,33464:12378,33465:12379,33466:12380,33467:12381,33468:12382,33469:12383,33470:12384,33471:12385,33472:12386,33473:12387,33474:12388,33475:12389,33476:12390,33477:12391,33478:12392,33479:12393,33480:12394,33481:12395,33482:12396,33483:12397,33484:12398,33485:12399,33486:12400,33487:12401,33488:12402,33489:12403,33490:12404,33491:12405,33492:12406,33493:12407,33494:12408,33495:12409,33496:12410,33497:12411,33498:12412,33499:12413,33500:12414,33501:12415,33502:12416,33503:12417,33504:12418,33505:12419,33506:12420,33507:12421,33508:12422,33509:12423,33510:12424,33511:12425,33512:12426,33513:12427,33514:12428,33515:12429,33516:12430,33517:12431,33518:12432,33519:12433,33520:12434,33521:12435,33600:12449,33601:12450,33602:12451,33603:12452,33604:12453,33605:12454,33606:12455,33607:12456,33608:12457,33609:12458,33610:12459,33611:12460,33612:12461,33613:12462,33614:12463,33615:12464,33616:12465,33617:12466,33618:12467,33619:12468,33620:12469,33621:12470,33622:12471,33623:12472,33624:12473,33625:12474,33626:12475,33627:12476,33628:12477,33629:12478,33630:12479,33631:12480,33632:12481,33633:12482,33634:12483,33635:12484,33636:12485,33637:12486,33638:12487,33639:12488,33640:12489,33641:12490,33642:12491,33643:12492,33644:12493,33645:12494,33646:12495,33647:12496,33648:12497,33649:12498,33650:12499,33651:12500,33652:12501,33653:12502,33654:12503,33655:12504,33656:12505,33657:12506,33658:12507,33659:12508,33660:12509,33661:12510,33662:12511,33664:12512,33665:12513,33666:12514,33667:12515,33668:12516,33669:12517,33670:12518,33671:12519,33672:12520,33673:12521,33674:12522,33675:12523,33676:12524,33677:12525,33678:12526,33679:12527,33680:12528,33681:12529,33682:12530,33683:12531,33684:12532,33685:12533,33686:12534,33695:913,33696:914,33697:915,33698:916,33699:917,33700:918,33701:919,33702:920,33703:921,33704:922,33705:923,33706:924,33707:925,33708:926,33709:927,33710:928,33711:929,33712:931,33713:932,33714:933,33715:934,33716:935,33717:936,33718:937,33727:945,33728:946,33729:947,33730:948,33731:949,33732:950,33733:951,33734:952,33735:953,33736:954,33737:955,33738:956,33739:957,33740:958,33741:959,33742:960,33743:961,33744:963,33745:964,33746:965,33747:966,33748:967,33749:968,33750:969,33856:1040,33857:1041,33858:1042,33859:1043,33860:1044,33861:1045,33862:1025,33863:1046,33864:1047,33865:1048,33866:1049,33867:1050,33868:1051,33869:1052,33870:1053,33871:1054,33872:1055,33873:1056,33874:1057,33875:1058,33876:1059,33877:1060,33878:1061,33879:1062,33880:1063,33881:1064,33882:1065,33883:1066,33884:1067,33885:1068,33886:1069,33887:1070,33888:1071,33904:1072,33905:1073,33906:1074,33907:1075,33908:1076,33909:1077,33910:1105,33911:1078,33912:1079,33913:1080,33914:1081,33915:1082,33916:1083,33917:1084,33918:1085,33920:1086,33921:1087,33922:1088,33923:1089,33924:1090,33925:1091,33926:1092,33927:1093,33928:1094,33929:1095,33930:1096,33931:1097,33932:1098,33933:1099,33934:1100,33935:1101,33936:1102,33937:1103,33951:9472,33952:9474,33953:9484,33954:9488,33955:9496,33956:9492,33957:9500,33958:9516,33959:9508,33960:9524,33961:9532,33962:9473,33963:9475,33964:9487,33965:9491,33966:9499,33967:9495,33968:9507,33969:9523,33970:9515,33971:9531,33972:9547,33973:9504,33974:9519,33975:9512,33976:9527,33977:9535,33978:9501,33979:9520,33980:9509,33981:9528,33982:9538,34975:20124,34976:21782,34977:23043,34978:38463,34979:21696,34980:24859,34981:25384,34982:23030,34983:36898,34984:33909,34985:33564,34986:31312,34987:24746,34988:25569,34989:28197,34990:26093,34991:33894,34992:33446,34993:39925,34994:26771,34995:22311,34996:26017,34997:25201,34998:23451,34999:22992,35e3:34427,35001:39156,35002:32098,35003:32190,35004:39822,35005:25110,35006:31903,35007:34999,35008:23433,35009:24245,35010:25353,35011:26263,35012:26696,35013:38343,35014:38797,35015:26447,35016:20197,35017:20234,35018:20301,35019:20381,35020:20553,35021:22258,35022:22839,35023:22996,35024:23041,35025:23561,35026:24799,35027:24847,35028:24944,35029:26131,35030:26885,35031:28858,35032:30031,35033:30064,35034:31227,35035:32173,35036:32239,35037:32963,35038:33806,35039:34915,35040:35586,35041:36949,35042:36986,35043:21307,35044:20117,35045:20133,35046:22495,35047:32946,35048:37057,35049:30959,35050:19968,35051:22769,35052:28322,35053:36920,35054:31282,35055:33576,35056:33419,35057:39983,35058:20801,35059:21360,35060:21693,35061:21729,35062:22240,35063:23035,35064:24341,35065:39154,35066:28139,35067:32996,35068:34093,35136:38498,35137:38512,35138:38560,35139:38907,35140:21515,35141:21491,35142:23431,35143:28879,35144:32701,35145:36802,35146:38632,35147:21359,35148:40284,35149:31418,35150:19985,35151:30867,35152:33276,35153:28198,35154:22040,35155:21764,35156:27421,35157:34074,35158:39995,35159:23013,35160:21417,35161:28006,35162:29916,35163:38287,35164:22082,35165:20113,35166:36939,35167:38642,35168:33615,35169:39180,35170:21473,35171:21942,35172:23344,35173:24433,35174:26144,35175:26355,35176:26628,35177:27704,35178:27891,35179:27945,35180:29787,35181:30408,35182:31310,35183:38964,35184:33521,35185:34907,35186:35424,35187:37613,35188:28082,35189:30123,35190:30410,35191:39365,35192:24742,35193:35585,35194:36234,35195:38322,35196:27022,35197:21421,35198:20870,35200:22290,35201:22576,35202:22852,35203:23476,35204:24310,35205:24616,35206:25513,35207:25588,35208:27839,35209:28436,35210:28814,35211:28948,35212:29017,35213:29141,35214:29503,35215:32257,35216:33398,35217:33489,35218:34199,35219:36960,35220:37467,35221:40219,35222:22633,35223:26044,35224:27738,35225:29989,35226:20985,35227:22830,35228:22885,35229:24448,35230:24540,35231:25276,35232:26106,35233:27178,35234:27431,35235:27572,35236:29579,35237:32705,35238:35158,35239:40236,35240:40206,35241:40644,35242:23713,35243:27798,35244:33659,35245:20740,35246:23627,35247:25014,35248:33222,35249:26742,35250:29281,35251:20057,35252:20474,35253:21368,35254:24681,35255:28201,35256:31311,35257:38899,35258:19979,35259:21270,35260:20206,35261:20309,35262:20285,35263:20385,35264:20339,35265:21152,35266:21487,35267:22025,35268:22799,35269:23233,35270:23478,35271:23521,35272:31185,35273:26247,35274:26524,35275:26550,35276:27468,35277:27827,35278:28779,35279:29634,35280:31117,35281:31166,35282:31292,35283:31623,35284:33457,35285:33499,35286:33540,35287:33655,35288:33775,35289:33747,35290:34662,35291:35506,35292:22057,35293:36008,35294:36838,35295:36942,35296:38686,35297:34442,35298:20420,35299:23784,35300:25105,35301:29273,35302:30011,35303:33253,35304:33469,35305:34558,35306:36032,35307:38597,35308:39187,35309:39381,35310:20171,35311:20250,35312:35299,35313:22238,35314:22602,35315:22730,35316:24315,35317:24555,35318:24618,35319:24724,35320:24674,35321:25040,35322:25106,35323:25296,35324:25913,35392:39745,35393:26214,35394:26800,35395:28023,35396:28784,35397:30028,35398:30342,35399:32117,35400:33445,35401:34809,35402:38283,35403:38542,35404:35997,35405:20977,35406:21182,35407:22806,35408:21683,35409:23475,35410:23830,35411:24936,35412:27010,35413:28079,35414:30861,35415:33995,35416:34903,35417:35442,35418:37799,35419:39608,35420:28012,35421:39336,35422:34521,35423:22435,35424:26623,35425:34510,35426:37390,35427:21123,35428:22151,35429:21508,35430:24275,35431:25313,35432:25785,35433:26684,35434:26680,35435:27579,35436:29554,35437:30906,35438:31339,35439:35226,35440:35282,35441:36203,35442:36611,35443:37101,35444:38307,35445:38548,35446:38761,35447:23398,35448:23731,35449:27005,35450:38989,35451:38990,35452:25499,35453:31520,35454:27179,35456:27263,35457:26806,35458:39949,35459:28511,35460:21106,35461:21917,35462:24688,35463:25324,35464:27963,35465:28167,35466:28369,35467:33883,35468:35088,35469:36676,35470:19988,35471:39993,35472:21494,35473:26907,35474:27194,35475:38788,35476:26666,35477:20828,35478:31427,35479:33970,35480:37340,35481:37772,35482:22107,35483:40232,35484:26658,35485:33541,35486:33841,35487:31909,35488:21e3,35489:33477,35490:29926,35491:20094,35492:20355,35493:20896,35494:23506,35495:21002,35496:21208,35497:21223,35498:24059,35499:21914,35500:22570,35501:23014,35502:23436,35503:23448,35504:23515,35505:24178,35506:24185,35507:24739,35508:24863,35509:24931,35510:25022,35511:25563,35512:25954,35513:26577,35514:26707,35515:26874,35516:27454,35517:27475,35518:27735,35519:28450,35520:28567,35521:28485,35522:29872,35523:29976,35524:30435,35525:30475,35526:31487,35527:31649,35528:31777,35529:32233,35530:32566,35531:32752,35532:32925,35533:33382,35534:33694,35535:35251,35536:35532,35537:36011,35538:36996,35539:37969,35540:38291,35541:38289,35542:38306,35543:38501,35544:38867,35545:39208,35546:33304,35547:20024,35548:21547,35549:23736,35550:24012,35551:29609,35552:30284,35553:30524,35554:23721,35555:32747,35556:36107,35557:38593,35558:38929,35559:38996,35560:39e3,35561:20225,35562:20238,35563:21361,35564:21916,35565:22120,35566:22522,35567:22855,35568:23305,35569:23492,35570:23696,35571:24076,35572:24190,35573:24524,35574:25582,35575:26426,35576:26071,35577:26082,35578:26399,35579:26827,35580:26820,35648:27231,35649:24112,35650:27589,35651:27671,35652:27773,35653:30079,35654:31048,35655:23395,35656:31232,35657:32e3,35658:24509,35659:35215,35660:35352,35661:36020,35662:36215,35663:36556,35664:36637,35665:39138,35666:39438,35667:39740,35668:20096,35669:20605,35670:20736,35671:22931,35672:23452,35673:25135,35674:25216,35675:25836,35676:27450,35677:29344,35678:30097,35679:31047,35680:32681,35681:34811,35682:35516,35683:35696,35684:25516,35685:33738,35686:38816,35687:21513,35688:21507,35689:21931,35690:26708,35691:27224,35692:35440,35693:30759,35694:26485,35695:40653,35696:21364,35697:23458,35698:33050,35699:34384,35700:36870,35701:19992,35702:20037,35703:20167,35704:20241,35705:21450,35706:21560,35707:23470,35708:24339,35709:24613,35710:25937,35712:26429,35713:27714,35714:27762,35715:27875,35716:28792,35717:29699,35718:31350,35719:31406,35720:31496,35721:32026,35722:31998,35723:32102,35724:26087,35725:29275,35726:21435,35727:23621,35728:24040,35729:25298,35730:25312,35731:25369,35732:28192,35733:34394,35734:35377,35735:36317,35736:37624,35737:28417,35738:31142,35739:39770,35740:20136,35741:20139,35742:20140,35743:20379,35744:20384,35745:20689,35746:20807,35747:31478,35748:20849,35749:20982,35750:21332,35751:21281,35752:21375,35753:21483,35754:21932,35755:22659,35756:23777,35757:24375,35758:24394,35759:24623,35760:24656,35761:24685,35762:25375,35763:25945,35764:27211,35765:27841,35766:29378,35767:29421,35768:30703,35769:33016,35770:33029,35771:33288,35772:34126,35773:37111,35774:37857,35775:38911,35776:39255,35777:39514,35778:20208,35779:20957,35780:23597,35781:26241,35782:26989,35783:23616,35784:26354,35785:26997,35786:29577,35787:26704,35788:31873,35789:20677,35790:21220,35791:22343,35792:24062,35793:37670,35794:26020,35795:27427,35796:27453,35797:29748,35798:31105,35799:31165,35800:31563,35801:32202,35802:33465,35803:33740,35804:34943,35805:35167,35806:35641,35807:36817,35808:37329,35809:21535,35810:37504,35811:20061,35812:20534,35813:21477,35814:21306,35815:29399,35816:29590,35817:30697,35818:33510,35819:36527,35820:39366,35821:39368,35822:39378,35823:20855,35824:24858,35825:34398,35826:21936,35827:31354,35828:20598,35829:23507,35830:36935,35831:38533,35832:20018,35833:27355,35834:37351,35835:23633,35836:23624,35904:25496,35905:31391,35906:27795,35907:38772,35908:36705,35909:31402,35910:29066,35911:38536,35912:31874,35913:26647,35914:32368,35915:26705,35916:37740,35917:21234,35918:21531,35919:34219,35920:35347,35921:32676,35922:36557,35923:37089,35924:21350,35925:34952,35926:31041,35927:20418,35928:20670,35929:21009,35930:20804,35931:21843,35932:22317,35933:29674,35934:22411,35935:22865,35936:24418,35937:24452,35938:24693,35939:24950,35940:24935,35941:25001,35942:25522,35943:25658,35944:25964,35945:26223,35946:26690,35947:28179,35948:30054,35949:31293,35950:31995,35951:32076,35952:32153,35953:32331,35954:32619,35955:33550,35956:33610,35957:34509,35958:35336,35959:35427,35960:35686,35961:36605,35962:38938,35963:40335,35964:33464,35965:36814,35966:39912,35968:21127,35969:25119,35970:25731,35971:28608,35972:38553,35973:26689,35974:20625,35975:27424,35976:27770,35977:28500,35978:31348,35979:32080,35980:34880,35981:35363,35982:26376,35983:20214,35984:20537,35985:20518,35986:20581,35987:20860,35988:21048,35989:21091,35990:21927,35991:22287,35992:22533,35993:23244,35994:24314,35995:25010,35996:25080,35997:25331,35998:25458,35999:26908,36e3:27177,36001:29309,36002:29356,36003:29486,36004:30740,36005:30831,36006:32121,36007:30476,36008:32937,36009:35211,36010:35609,36011:36066,36012:36562,36013:36963,36014:37749,36015:38522,36016:38997,36017:39443,36018:40568,36019:20803,36020:21407,36021:21427,36022:24187,36023:24358,36024:28187,36025:28304,36026:29572,36027:29694,36028:32067,36029:33335,36030:35328,36031:35578,36032:38480,36033:20046,36034:20491,36035:21476,36036:21628,36037:22266,36038:22993,36039:23396,36040:24049,36041:24235,36042:24359,36043:25144,36044:25925,36045:26543,36046:28246,36047:29392,36048:31946,36049:34996,36050:32929,36051:32993,36052:33776,36053:34382,36054:35463,36055:36328,36056:37431,36057:38599,36058:39015,36059:40723,36060:20116,36061:20114,36062:20237,36063:21320,36064:21577,36065:21566,36066:23087,36067:24460,36068:24481,36069:24735,36070:26791,36071:27278,36072:29786,36073:30849,36074:35486,36075:35492,36076:35703,36077:37264,36078:20062,36079:39881,36080:20132,36081:20348,36082:20399,36083:20505,36084:20502,36085:20809,36086:20844,36087:21151,36088:21177,36089:21246,36090:21402,36091:21475,36092:21521,36160:21518,36161:21897,36162:22353,36163:22434,36164:22909,36165:23380,36166:23389,36167:23439,36168:24037,36169:24039,36170:24055,36171:24184,36172:24195,36173:24218,36174:24247,36175:24344,36176:24658,36177:24908,36178:25239,36179:25304,36180:25511,36181:25915,36182:26114,36183:26179,36184:26356,36185:26477,36186:26657,36187:26775,36188:27083,36189:27743,36190:27946,36191:28009,36192:28207,36193:28317,36194:30002,36195:30343,36196:30828,36197:31295,36198:31968,36199:32005,36200:32024,36201:32094,36202:32177,36203:32789,36204:32771,36205:32943,36206:32945,36207:33108,36208:33167,36209:33322,36210:33618,36211:34892,36212:34913,36213:35611,36214:36002,36215:36092,36216:37066,36217:37237,36218:37489,36219:30783,36220:37628,36221:38308,36222:38477,36224:38917,36225:39321,36226:39640,36227:40251,36228:21083,36229:21163,36230:21495,36231:21512,36232:22741,36233:25335,36234:28640,36235:35946,36236:36703,36237:40633,36238:20811,36239:21051,36240:21578,36241:22269,36242:31296,36243:37239,36244:40288,36245:40658,36246:29508,36247:28425,36248:33136,36249:29969,36250:24573,36251:24794,36252:39592,36253:29403,36254:36796,36255:27492,36256:38915,36257:20170,36258:22256,36259:22372,36260:22718,36261:23130,36262:24680,36263:25031,36264:26127,36265:26118,36266:26681,36267:26801,36268:28151,36269:30165,36270:32058,36271:33390,36272:39746,36273:20123,36274:20304,36275:21449,36276:21766,36277:23919,36278:24038,36279:24046,36280:26619,36281:27801,36282:29811,36283:30722,36284:35408,36285:37782,36286:35039,36287:22352,36288:24231,36289:25387,36290:20661,36291:20652,36292:20877,36293:26368,36294:21705,36295:22622,36296:22971,36297:23472,36298:24425,36299:25165,36300:25505,36301:26685,36302:27507,36303:28168,36304:28797,36305:37319,36306:29312,36307:30741,36308:30758,36309:31085,36310:25998,36311:32048,36312:33756,36313:35009,36314:36617,36315:38555,36316:21092,36317:22312,36318:26448,36319:32618,36320:36001,36321:20916,36322:22338,36323:38442,36324:22586,36325:27018,36326:32948,36327:21682,36328:23822,36329:22524,36330:30869,36331:40442,36332:20316,36333:21066,36334:21643,36335:25662,36336:26152,36337:26388,36338:26613,36339:31364,36340:31574,36341:32034,36342:37679,36343:26716,36344:39853,36345:31545,36346:21273,36347:20874,36348:21047,36416:23519,36417:25334,36418:25774,36419:25830,36420:26413,36421:27578,36422:34217,36423:38609,36424:30352,36425:39894,36426:25420,36427:37638,36428:39851,36429:30399,36430:26194,36431:19977,36432:20632,36433:21442,36434:23665,36435:24808,36436:25746,36437:25955,36438:26719,36439:29158,36440:29642,36441:29987,36442:31639,36443:32386,36444:34453,36445:35715,36446:36059,36447:37240,36448:39184,36449:26028,36450:26283,36451:27531,36452:20181,36453:20180,36454:20282,36455:20351,36456:21050,36457:21496,36458:21490,36459:21987,36460:22235,36461:22763,36462:22987,36463:22985,36464:23039,36465:23376,36466:23629,36467:24066,36468:24107,36469:24535,36470:24605,36471:25351,36472:25903,36473:23388,36474:26031,36475:26045,36476:26088,36477:26525,36478:27490,36480:27515,36481:27663,36482:29509,36483:31049,36484:31169,36485:31992,36486:32025,36487:32043,36488:32930,36489:33026,36490:33267,36491:35222,36492:35422,36493:35433,36494:35430,36495:35468,36496:35566,36497:36039,36498:36060,36499:38604,36500:39164,36501:27503,36502:20107,36503:20284,36504:20365,36505:20816,36506:23383,36507:23546,36508:24904,36509:25345,36510:26178,36511:27425,36512:28363,36513:27835,36514:29246,36515:29885,36516:30164,36517:30913,36518:31034,36519:32780,36520:32819,36521:33258,36522:33940,36523:36766,36524:27728,36525:40575,36526:24335,36527:35672,36528:40235,36529:31482,36530:36600,36531:23437,36532:38635,36533:19971,36534:21489,36535:22519,36536:22833,36537:23241,36538:23460,36539:24713,36540:28287,36541:28422,36542:30142,36543:36074,36544:23455,36545:34048,36546:31712,36547:20594,36548:26612,36549:33437,36550:23649,36551:34122,36552:32286,36553:33294,36554:20889,36555:23556,36556:25448,36557:36198,36558:26012,36559:29038,36560:31038,36561:32023,36562:32773,36563:35613,36564:36554,36565:36974,36566:34503,36567:37034,36568:20511,36569:21242,36570:23610,36571:26451,36572:28796,36573:29237,36574:37196,36575:37320,36576:37675,36577:33509,36578:23490,36579:24369,36580:24825,36581:20027,36582:21462,36583:23432,36584:25163,36585:26417,36586:27530,36587:29417,36588:29664,36589:31278,36590:33131,36591:36259,36592:37202,36593:39318,36594:20754,36595:21463,36596:21610,36597:23551,36598:25480,36599:27193,36600:32172,36601:38656,36602:22234,36603:21454,36604:21608,36672:23447,36673:23601,36674:24030,36675:20462,36676:24833,36677:25342,36678:27954,36679:31168,36680:31179,36681:32066,36682:32333,36683:32722,36684:33261,36685:33311,36686:33936,36687:34886,36688:35186,36689:35728,36690:36468,36691:36655,36692:36913,36693:37195,36694:37228,36695:38598,36696:37276,36697:20160,36698:20303,36699:20805,36700:21313,36701:24467,36702:25102,36703:26580,36704:27713,36705:28171,36706:29539,36707:32294,36708:37325,36709:37507,36710:21460,36711:22809,36712:23487,36713:28113,36714:31069,36715:32302,36716:31899,36717:22654,36718:29087,36719:20986,36720:34899,36721:36848,36722:20426,36723:23803,36724:26149,36725:30636,36726:31459,36727:33308,36728:39423,36729:20934,36730:24490,36731:26092,36732:26991,36733:27529,36734:28147,36736:28310,36737:28516,36738:30462,36739:32020,36740:24033,36741:36981,36742:37255,36743:38918,36744:20966,36745:21021,36746:25152,36747:26257,36748:26329,36749:28186,36750:24246,36751:32210,36752:32626,36753:26360,36754:34223,36755:34295,36756:35576,36757:21161,36758:21465,36759:22899,36760:24207,36761:24464,36762:24661,36763:37604,36764:38500,36765:20663,36766:20767,36767:21213,36768:21280,36769:21319,36770:21484,36771:21736,36772:21830,36773:21809,36774:22039,36775:22888,36776:22974,36777:23100,36778:23477,36779:23558,36780:23567,36781:23569,36782:23578,36783:24196,36784:24202,36785:24288,36786:24432,36787:25215,36788:25220,36789:25307,36790:25484,36791:25463,36792:26119,36793:26124,36794:26157,36795:26230,36796:26494,36797:26786,36798:27167,36799:27189,36800:27836,36801:28040,36802:28169,36803:28248,36804:28988,36805:28966,36806:29031,36807:30151,36808:30465,36809:30813,36810:30977,36811:31077,36812:31216,36813:31456,36814:31505,36815:31911,36816:32057,36817:32918,36818:33750,36819:33931,36820:34121,36821:34909,36822:35059,36823:35359,36824:35388,36825:35412,36826:35443,36827:35937,36828:36062,36829:37284,36830:37478,36831:37758,36832:37912,36833:38556,36834:38808,36835:19978,36836:19976,36837:19998,36838:20055,36839:20887,36840:21104,36841:22478,36842:22580,36843:22732,36844:23330,36845:24120,36846:24773,36847:25854,36848:26465,36849:26454,36850:27972,36851:29366,36852:30067,36853:31331,36854:33976,36855:35698,36856:37304,36857:37664,36858:22065,36859:22516,36860:39166,36928:25325,36929:26893,36930:27542,36931:29165,36932:32340,36933:32887,36934:33394,36935:35302,36936:39135,36937:34645,36938:36785,36939:23611,36940:20280,36941:20449,36942:20405,36943:21767,36944:23072,36945:23517,36946:23529,36947:24515,36948:24910,36949:25391,36950:26032,36951:26187,36952:26862,36953:27035,36954:28024,36955:28145,36956:30003,36957:30137,36958:30495,36959:31070,36960:31206,36961:32051,36962:33251,36963:33455,36964:34218,36965:35242,36966:35386,36967:36523,36968:36763,36969:36914,36970:37341,36971:38663,36972:20154,36973:20161,36974:20995,36975:22645,36976:22764,36977:23563,36978:29978,36979:23613,36980:33102,36981:35338,36982:36805,36983:38499,36984:38765,36985:31525,36986:35535,36987:38920,36988:37218,36989:22259,36990:21416,36992:36887,36993:21561,36994:22402,36995:24101,36996:25512,36997:27700,36998:28810,36999:30561,37e3:31883,37001:32736,37002:34928,37003:36930,37004:37204,37005:37648,37006:37656,37007:38543,37008:29790,37009:39620,37010:23815,37011:23913,37012:25968,37013:26530,37014:36264,37015:38619,37016:25454,37017:26441,37018:26905,37019:33733,37020:38935,37021:38592,37022:35070,37023:28548,37024:25722,37025:23544,37026:19990,37027:28716,37028:30045,37029:26159,37030:20932,37031:21046,37032:21218,37033:22995,37034:24449,37035:24615,37036:25104,37037:25919,37038:25972,37039:26143,37040:26228,37041:26866,37042:26646,37043:27491,37044:28165,37045:29298,37046:29983,37047:30427,37048:31934,37049:32854,37050:22768,37051:35069,37052:35199,37053:35488,37054:35475,37055:35531,37056:36893,37057:37266,37058:38738,37059:38745,37060:25993,37061:31246,37062:33030,37063:38587,37064:24109,37065:24796,37066:25114,37067:26021,37068:26132,37069:26512,37070:30707,37071:31309,37072:31821,37073:32318,37074:33034,37075:36012,37076:36196,37077:36321,37078:36447,37079:30889,37080:20999,37081:25305,37082:25509,37083:25666,37084:25240,37085:35373,37086:31363,37087:31680,37088:35500,37089:38634,37090:32118,37091:33292,37092:34633,37093:20185,37094:20808,37095:21315,37096:21344,37097:23459,37098:23554,37099:23574,37100:24029,37101:25126,37102:25159,37103:25776,37104:26643,37105:26676,37106:27849,37107:27973,37108:27927,37109:26579,37110:28508,37111:29006,37112:29053,37113:26059,37114:31359,37115:31661,37116:32218,37184:32330,37185:32680,37186:33146,37187:33307,37188:33337,37189:34214,37190:35438,37191:36046,37192:36341,37193:36984,37194:36983,37195:37549,37196:37521,37197:38275,37198:39854,37199:21069,37200:21892,37201:28472,37202:28982,37203:20840,37204:31109,37205:32341,37206:33203,37207:31950,37208:22092,37209:22609,37210:23720,37211:25514,37212:26366,37213:26365,37214:26970,37215:29401,37216:30095,37217:30094,37218:30990,37219:31062,37220:31199,37221:31895,37222:32032,37223:32068,37224:34311,37225:35380,37226:38459,37227:36961,37228:40736,37229:20711,37230:21109,37231:21452,37232:21474,37233:20489,37234:21930,37235:22766,37236:22863,37237:29245,37238:23435,37239:23652,37240:21277,37241:24803,37242:24819,37243:25436,37244:25475,37245:25407,37246:25531,37248:25805,37249:26089,37250:26361,37251:24035,37252:27085,37253:27133,37254:28437,37255:29157,37256:20105,37257:30185,37258:30456,37259:31379,37260:31967,37261:32207,37262:32156,37263:32865,37264:33609,37265:33624,37266:33900,37267:33980,37268:34299,37269:35013,37270:36208,37271:36865,37272:36973,37273:37783,37274:38684,37275:39442,37276:20687,37277:22679,37278:24974,37279:33235,37280:34101,37281:36104,37282:36896,37283:20419,37284:20596,37285:21063,37286:21363,37287:24687,37288:25417,37289:26463,37290:28204,37291:36275,37292:36895,37293:20439,37294:23646,37295:36042,37296:26063,37297:32154,37298:21330,37299:34966,37300:20854,37301:25539,37302:23384,37303:23403,37304:23562,37305:25613,37306:26449,37307:36956,37308:20182,37309:22810,37310:22826,37311:27760,37312:35409,37313:21822,37314:22549,37315:22949,37316:24816,37317:25171,37318:26561,37319:33333,37320:26965,37321:38464,37322:39364,37323:39464,37324:20307,37325:22534,37326:23550,37327:32784,37328:23729,37329:24111,37330:24453,37331:24608,37332:24907,37333:25140,37334:26367,37335:27888,37336:28382,37337:32974,37338:33151,37339:33492,37340:34955,37341:36024,37342:36864,37343:36910,37344:38538,37345:40667,37346:39899,37347:20195,37348:21488,37349:22823,37350:31532,37351:37261,37352:38988,37353:40441,37354:28381,37355:28711,37356:21331,37357:21828,37358:23429,37359:25176,37360:25246,37361:25299,37362:27810,37363:28655,37364:29730,37365:35351,37366:37944,37367:28609,37368:35582,37369:33592,37370:20967,37371:34552,37372:21482,37440:21481,37441:20294,37442:36948,37443:36784,37444:22890,37445:33073,37446:24061,37447:31466,37448:36799,37449:26842,37450:35895,37451:29432,37452:40008,37453:27197,37454:35504,37455:20025,37456:21336,37457:22022,37458:22374,37459:25285,37460:25506,37461:26086,37462:27470,37463:28129,37464:28251,37465:28845,37466:30701,37467:31471,37468:31658,37469:32187,37470:32829,37471:32966,37472:34507,37473:35477,37474:37723,37475:22243,37476:22727,37477:24382,37478:26029,37479:26262,37480:27264,37481:27573,37482:30007,37483:35527,37484:20516,37485:30693,37486:22320,37487:24347,37488:24677,37489:26234,37490:27744,37491:30196,37492:31258,37493:32622,37494:33268,37495:34584,37496:36933,37497:39347,37498:31689,37499:30044,37500:31481,37501:31569,37502:33988,37504:36880,37505:31209,37506:31378,37507:33590,37508:23265,37509:30528,37510:20013,37511:20210,37512:23449,37513:24544,37514:25277,37515:26172,37516:26609,37517:27880,37518:34411,37519:34935,37520:35387,37521:37198,37522:37619,37523:39376,37524:27159,37525:28710,37526:29482,37527:33511,37528:33879,37529:36015,37530:19969,37531:20806,37532:20939,37533:21899,37534:23541,37535:24086,37536:24115,37537:24193,37538:24340,37539:24373,37540:24427,37541:24500,37542:25074,37543:25361,37544:26274,37545:26397,37546:28526,37547:29266,37548:30010,37549:30522,37550:32884,37551:33081,37552:33144,37553:34678,37554:35519,37555:35548,37556:36229,37557:36339,37558:37530,37559:38263,37560:38914,37561:40165,37562:21189,37563:25431,37564:30452,37565:26389,37566:27784,37567:29645,37568:36035,37569:37806,37570:38515,37571:27941,37572:22684,37573:26894,37574:27084,37575:36861,37576:37786,37577:30171,37578:36890,37579:22618,37580:26626,37581:25524,37582:27131,37583:20291,37584:28460,37585:26584,37586:36795,37587:34086,37588:32180,37589:37716,37590:26943,37591:28528,37592:22378,37593:22775,37594:23340,37595:32044,37596:29226,37597:21514,37598:37347,37599:40372,37600:20141,37601:20302,37602:20572,37603:20597,37604:21059,37605:35998,37606:21576,37607:22564,37608:23450,37609:24093,37610:24213,37611:24237,37612:24311,37613:24351,37614:24716,37615:25269,37616:25402,37617:25552,37618:26799,37619:27712,37620:30855,37621:31118,37622:31243,37623:32224,37624:33351,37625:35330,37626:35558,37627:36420,37628:36883,37696:37048,37697:37165,37698:37336,37699:40718,37700:27877,37701:25688,37702:25826,37703:25973,37704:28404,37705:30340,37706:31515,37707:36969,37708:37841,37709:28346,37710:21746,37711:24505,37712:25764,37713:36685,37714:36845,37715:37444,37716:20856,37717:22635,37718:22825,37719:23637,37720:24215,37721:28155,37722:32399,37723:29980,37724:36028,37725:36578,37726:39003,37727:28857,37728:20253,37729:27583,37730:28593,37731:3e4,37732:38651,37733:20814,37734:21520,37735:22581,37736:22615,37737:22956,37738:23648,37739:24466,37740:26007,37741:26460,37742:28193,37743:30331,37744:33759,37745:36077,37746:36884,37747:37117,37748:37709,37749:30757,37750:30778,37751:21162,37752:24230,37753:22303,37754:22900,37755:24594,37756:20498,37757:20826,37758:20908,37760:20941,37761:20992,37762:21776,37763:22612,37764:22616,37765:22871,37766:23445,37767:23798,37768:23947,37769:24764,37770:25237,37771:25645,37772:26481,37773:26691,37774:26812,37775:26847,37776:30423,37777:28120,37778:28271,37779:28059,37780:28783,37781:29128,37782:24403,37783:30168,37784:31095,37785:31561,37786:31572,37787:31570,37788:31958,37789:32113,37790:21040,37791:33891,37792:34153,37793:34276,37794:35342,37795:35588,37796:35910,37797:36367,37798:36867,37799:36879,37800:37913,37801:38518,37802:38957,37803:39472,37804:38360,37805:20685,37806:21205,37807:21516,37808:22530,37809:23566,37810:24999,37811:25758,37812:27934,37813:30643,37814:31461,37815:33012,37816:33796,37817:36947,37818:37509,37819:23776,37820:40199,37821:21311,37822:24471,37823:24499,37824:28060,37825:29305,37826:30563,37827:31167,37828:31716,37829:27602,37830:29420,37831:35501,37832:26627,37833:27233,37834:20984,37835:31361,37836:26932,37837:23626,37838:40182,37839:33515,37840:23493,37841:37193,37842:28702,37843:22136,37844:23663,37845:24775,37846:25958,37847:27788,37848:35930,37849:36929,37850:38931,37851:21585,37852:26311,37853:37389,37854:22856,37855:37027,37856:20869,37857:20045,37858:20970,37859:34201,37860:35598,37861:28760,37862:25466,37863:37707,37864:26978,37865:39348,37866:32260,37867:30071,37868:21335,37869:26976,37870:36575,37871:38627,37872:27741,37873:20108,37874:23612,37875:24336,37876:36841,37877:21250,37878:36049,37879:32905,37880:34425,37881:24319,37882:26085,37883:20083,37884:20837,37952:22914,37953:23615,37954:38894,37955:20219,37956:22922,37957:24525,37958:35469,37959:28641,37960:31152,37961:31074,37962:23527,37963:33905,37964:29483,37965:29105,37966:24180,37967:24565,37968:25467,37969:25754,37970:29123,37971:31896,37972:20035,37973:24316,37974:20043,37975:22492,37976:22178,37977:24745,37978:28611,37979:32013,37980:33021,37981:33075,37982:33215,37983:36786,37984:35223,37985:34468,37986:24052,37987:25226,37988:25773,37989:35207,37990:26487,37991:27874,37992:27966,37993:29750,37994:30772,37995:23110,37996:32629,37997:33453,37998:39340,37999:20467,38e3:24259,38001:25309,38002:25490,38003:25943,38004:26479,38005:30403,38006:29260,38007:32972,38008:32954,38009:36649,38010:37197,38011:20493,38012:22521,38013:23186,38014:26757,38016:26995,38017:29028,38018:29437,38019:36023,38020:22770,38021:36064,38022:38506,38023:36889,38024:34687,38025:31204,38026:30695,38027:33833,38028:20271,38029:21093,38030:21338,38031:25293,38032:26575,38033:27850,38034:30333,38035:31636,38036:31893,38037:33334,38038:34180,38039:36843,38040:26333,38041:28448,38042:29190,38043:32283,38044:33707,38045:39361,38046:40614,38047:20989,38048:31665,38049:30834,38050:31672,38051:32903,38052:31560,38053:27368,38054:24161,38055:32908,38056:30033,38057:30048,38058:20843,38059:37474,38060:28300,38061:30330,38062:37271,38063:39658,38064:20240,38065:32624,38066:25244,38067:31567,38068:38309,38069:40169,38070:22138,38071:22617,38072:34532,38073:38588,38074:20276,38075:21028,38076:21322,38077:21453,38078:21467,38079:24070,38080:25644,38081:26001,38082:26495,38083:27710,38084:27726,38085:29256,38086:29359,38087:29677,38088:30036,38089:32321,38090:33324,38091:34281,38092:36009,38093:31684,38094:37318,38095:29033,38096:38930,38097:39151,38098:25405,38099:26217,38100:30058,38101:30436,38102:30928,38103:34115,38104:34542,38105:21290,38106:21329,38107:21542,38108:22915,38109:24199,38110:24444,38111:24754,38112:25161,38113:25209,38114:25259,38115:26e3,38116:27604,38117:27852,38118:30130,38119:30382,38120:30865,38121:31192,38122:32203,38123:32631,38124:32933,38125:34987,38126:35513,38127:36027,38128:36991,38129:38750,38130:39131,38131:27147,38132:31800,38133:20633,38134:23614,38135:24494,38136:26503,38137:27608,38138:29749,38139:30473,38140:32654,38208:40763,38209:26570,38210:31255,38211:21305,38212:30091,38213:39661,38214:24422,38215:33181,38216:33777,38217:32920,38218:24380,38219:24517,38220:30050,38221:31558,38222:36924,38223:26727,38224:23019,38225:23195,38226:32016,38227:30334,38228:35628,38229:20469,38230:24426,38231:27161,38232:27703,38233:28418,38234:29922,38235:31080,38236:34920,38237:35413,38238:35961,38239:24287,38240:25551,38241:30149,38242:31186,38243:33495,38244:37672,38245:37618,38246:33948,38247:34541,38248:39981,38249:21697,38250:24428,38251:25996,38252:27996,38253:28693,38254:36007,38255:36051,38256:38971,38257:25935,38258:29942,38259:19981,38260:20184,38261:22496,38262:22827,38263:23142,38264:23500,38265:20904,38266:24067,38267:24220,38268:24598,38269:25206,38270:25975,38272:26023,38273:26222,38274:28014,38275:29238,38276:31526,38277:33104,38278:33178,38279:33433,38280:35676,38281:36e3,38282:36070,38283:36212,38284:38428,38285:38468,38286:20398,38287:25771,38288:27494,38289:33310,38290:33889,38291:34154,38292:37096,38293:23553,38294:26963,38295:39080,38296:33914,38297:34135,38298:20239,38299:21103,38300:24489,38301:24133,38302:26381,38303:31119,38304:33145,38305:35079,38306:35206,38307:28149,38308:24343,38309:25173,38310:27832,38311:20175,38312:29289,38313:39826,38314:20998,38315:21563,38316:22132,38317:22707,38318:24996,38319:25198,38320:28954,38321:22894,38322:31881,38323:31966,38324:32027,38325:38640,38326:25991,38327:32862,38328:19993,38329:20341,38330:20853,38331:22592,38332:24163,38333:24179,38334:24330,38335:26564,38336:20006,38337:34109,38338:38281,38339:38491,38340:31859,38341:38913,38342:20731,38343:22721,38344:30294,38345:30887,38346:21029,38347:30629,38348:34065,38349:31622,38350:20559,38351:22793,38352:29255,38353:31687,38354:32232,38355:36794,38356:36820,38357:36941,38358:20415,38359:21193,38360:23081,38361:24321,38362:38829,38363:20445,38364:33303,38365:37610,38366:22275,38367:25429,38368:27497,38369:29995,38370:35036,38371:36628,38372:31298,38373:21215,38374:22675,38375:24917,38376:25098,38377:26286,38378:27597,38379:31807,38380:33769,38381:20515,38382:20472,38383:21253,38384:21574,38385:22577,38386:22857,38387:23453,38388:23792,38389:23791,38390:23849,38391:24214,38392:25265,38393:25447,38394:25918,38395:26041,38396:26379,38464:27861,38465:27873,38466:28921,38467:30770,38468:32299,38469:32990,38470:33459,38471:33804,38472:34028,38473:34562,38474:35090,38475:35370,38476:35914,38477:37030,38478:37586,38479:39165,38480:40179,38481:40300,38482:20047,38483:20129,38484:20621,38485:21078,38486:22346,38487:22952,38488:24125,38489:24536,38490:24537,38491:25151,38492:26292,38493:26395,38494:26576,38495:26834,38496:20882,38497:32033,38498:32938,38499:33192,38500:35584,38501:35980,38502:36031,38503:37502,38504:38450,38505:21536,38506:38956,38507:21271,38508:20693,38509:21340,38510:22696,38511:25778,38512:26420,38513:29287,38514:30566,38515:31302,38516:37350,38517:21187,38518:27809,38519:27526,38520:22528,38521:24140,38522:22868,38523:26412,38524:32763,38525:20961,38526:30406,38528:25705,38529:30952,38530:39764,38531:40635,38532:22475,38533:22969,38534:26151,38535:26522,38536:27598,38537:21737,38538:27097,38539:24149,38540:33180,38541:26517,38542:39850,38543:26622,38544:40018,38545:26717,38546:20134,38547:20451,38548:21448,38549:25273,38550:26411,38551:27819,38552:36804,38553:20397,38554:32365,38555:40639,38556:19975,38557:24930,38558:28288,38559:28459,38560:34067,38561:21619,38562:26410,38563:39749,38564:24051,38565:31637,38566:23724,38567:23494,38568:34588,38569:28234,38570:34001,38571:31252,38572:33032,38573:22937,38574:31885,38575:27665,38576:30496,38577:21209,38578:22818,38579:28961,38580:29279,38581:30683,38582:38695,38583:40289,38584:26891,38585:23167,38586:23064,38587:20901,38588:21517,38589:21629,38590:26126,38591:30431,38592:36855,38593:37528,38594:40180,38595:23018,38596:29277,38597:28357,38598:20813,38599:26825,38600:32191,38601:32236,38602:38754,38603:40634,38604:25720,38605:27169,38606:33538,38607:22916,38608:23391,38609:27611,38610:29467,38611:30450,38612:32178,38613:32791,38614:33945,38615:20786,38616:26408,38617:40665,38618:30446,38619:26466,38620:21247,38621:39173,38622:23588,38623:25147,38624:31870,38625:36016,38626:21839,38627:24758,38628:32011,38629:38272,38630:21249,38631:20063,38632:20918,38633:22812,38634:29242,38635:32822,38636:37326,38637:24357,38638:30690,38639:21380,38640:24441,38641:32004,38642:34220,38643:35379,38644:36493,38645:38742,38646:26611,38647:34222,38648:37971,38649:24841,38650:24840,38651:27833,38652:30290,38720:35565,38721:36664,38722:21807,38723:20305,38724:20778,38725:21191,38726:21451,38727:23461,38728:24189,38729:24736,38730:24962,38731:25558,38732:26377,38733:26586,38734:28263,38735:28044,38736:29494,38737:29495,38738:30001,38739:31056,38740:35029,38741:35480,38742:36938,38743:37009,38744:37109,38745:38596,38746:34701,38747:22805,38748:20104,38749:20313,38750:19982,38751:35465,38752:36671,38753:38928,38754:20653,38755:24188,38756:22934,38757:23481,38758:24248,38759:25562,38760:25594,38761:25793,38762:26332,38763:26954,38764:27096,38765:27915,38766:28342,38767:29076,38768:29992,38769:31407,38770:32650,38771:32768,38772:33865,38773:33993,38774:35201,38775:35617,38776:36362,38777:36965,38778:38525,38779:39178,38780:24958,38781:25233,38782:27442,38784:27779,38785:28020,38786:32716,38787:32764,38788:28096,38789:32645,38790:34746,38791:35064,38792:26469,38793:33713,38794:38972,38795:38647,38796:27931,38797:32097,38798:33853,38799:37226,38800:20081,38801:21365,38802:23888,38803:27396,38804:28651,38805:34253,38806:34349,38807:35239,38808:21033,38809:21519,38810:23653,38811:26446,38812:26792,38813:29702,38814:29827,38815:30178,38816:35023,38817:35041,38818:37324,38819:38626,38820:38520,38821:24459,38822:29575,38823:31435,38824:33870,38825:25504,38826:30053,38827:21129,38828:27969,38829:28316,38830:29705,38831:30041,38832:30827,38833:31890,38834:38534,38835:31452,38836:40845,38837:20406,38838:24942,38839:26053,38840:34396,38841:20102,38842:20142,38843:20698,38844:20001,38845:20940,38846:23534,38847:26009,38848:26753,38849:28092,38850:29471,38851:30274,38852:30637,38853:31260,38854:31975,38855:33391,38856:35538,38857:36988,38858:37327,38859:38517,38860:38936,38861:21147,38862:32209,38863:20523,38864:21400,38865:26519,38866:28107,38867:29136,38868:29747,38869:33256,38870:36650,38871:38563,38872:40023,38873:40607,38874:29792,38875:22593,38876:28057,38877:32047,38878:39006,38879:20196,38880:20278,38881:20363,38882:20919,38883:21169,38884:23994,38885:24604,38886:29618,38887:31036,38888:33491,38889:37428,38890:38583,38891:38646,38892:38666,38893:40599,38894:40802,38895:26278,38896:27508,38897:21015,38898:21155,38899:28872,38900:35010,38901:24265,38902:24651,38903:24976,38904:28451,38905:29001,38906:31806,38907:32244,38908:32879,38976:34030,38977:36899,38978:37676,38979:21570,38980:39791,38981:27347,38982:28809,38983:36034,38984:36335,38985:38706,38986:21172,38987:23105,38988:24266,38989:24324,38990:26391,38991:27004,38992:27028,38993:28010,38994:28431,38995:29282,38996:29436,38997:31725,38998:32769,38999:32894,39e3:34635,39001:37070,39002:20845,39003:40595,39004:31108,39005:32907,39006:37682,39007:35542,39008:20525,39009:21644,39010:35441,39011:27498,39012:36036,39013:33031,39014:24785,39015:26528,39016:40434,39017:20121,39018:20120,39019:39952,39020:35435,39021:34241,39022:34152,39023:26880,39024:28286,39025:30871,39026:33109,39071:24332,39072:19984,39073:19989,39074:20010,39075:20017,39076:20022,39077:20028,39078:20031,39079:20034,39080:20054,39081:20056,39082:20098,39083:20101,39084:35947,39085:20106,39086:33298,39087:24333,39088:20110,39089:20126,39090:20127,39091:20128,39092:20130,39093:20144,39094:20147,39095:20150,39096:20174,39097:20173,39098:20164,39099:20166,39100:20162,39101:20183,39102:20190,39103:20205,39104:20191,39105:20215,39106:20233,39107:20314,39108:20272,39109:20315,39110:20317,39111:20311,39112:20295,39113:20342,39114:20360,39115:20367,39116:20376,39117:20347,39118:20329,39119:20336,39120:20369,39121:20335,39122:20358,39123:20374,39124:20760,39125:20436,39126:20447,39127:20430,39128:20440,39129:20443,39130:20433,39131:20442,39132:20432,39133:20452,39134:20453,39135:20506,39136:20520,39137:20500,39138:20522,39139:20517,39140:20485,39141:20252,39142:20470,39143:20513,39144:20521,39145:20524,39146:20478,39147:20463,39148:20497,39149:20486,39150:20547,39151:20551,39152:26371,39153:20565,39154:20560,39155:20552,39156:20570,39157:20566,39158:20588,39159:20600,39160:20608,39161:20634,39162:20613,39163:20660,39164:20658,39232:20681,39233:20682,39234:20659,39235:20674,39236:20694,39237:20702,39238:20709,39239:20717,39240:20707,39241:20718,39242:20729,39243:20725,39244:20745,39245:20737,39246:20738,39247:20758,39248:20757,39249:20756,39250:20762,39251:20769,39252:20794,39253:20791,39254:20796,39255:20795,39256:20799,39257:20800,39258:20818,39259:20812,39260:20820,39261:20834,39262:31480,39263:20841,39264:20842,39265:20846,39266:20864,39267:20866,39268:22232,39269:20876,39270:20873,39271:20879,39272:20881,39273:20883,39274:20885,39275:20886,39276:20900,39277:20902,39278:20898,39279:20905,39280:20906,39281:20907,39282:20915,39283:20913,39284:20914,39285:20912,39286:20917,39287:20925,39288:20933,39289:20937,39290:20955,39291:20960,39292:34389,39293:20969,39294:20973,39296:20976,39297:20981,39298:20990,39299:20996,39300:21003,39301:21012,39302:21006,39303:21031,39304:21034,39305:21038,39306:21043,39307:21049,39308:21071,39309:21060,39310:21067,39311:21068,39312:21086,39313:21076,39314:21098,39315:21108,39316:21097,39317:21107,39318:21119,39319:21117,39320:21133,39321:21140,39322:21138,39323:21105,39324:21128,39325:21137,39326:36776,39327:36775,39328:21164,39329:21165,39330:21180,39331:21173,39332:21185,39333:21197,39334:21207,39335:21214,39336:21219,39337:21222,39338:39149,39339:21216,39340:21235,39341:21237,39342:21240,39343:21241,39344:21254,39345:21256,39346:30008,39347:21261,39348:21264,39349:21263,39350:21269,39351:21274,39352:21283,39353:21295,39354:21297,39355:21299,39356:21304,39357:21312,39358:21318,39359:21317,39360:19991,39361:21321,39362:21325,39363:20950,39364:21342,39365:21353,39366:21358,39367:22808,39368:21371,39369:21367,39370:21378,39371:21398,39372:21408,39373:21414,39374:21413,39375:21422,39376:21424,39377:21430,39378:21443,39379:31762,39380:38617,39381:21471,39382:26364,39383:29166,39384:21486,39385:21480,39386:21485,39387:21498,39388:21505,39389:21565,39390:21568,39391:21548,39392:21549,39393:21564,39394:21550,39395:21558,39396:21545,39397:21533,39398:21582,39399:21647,39400:21621,39401:21646,39402:21599,39403:21617,39404:21623,39405:21616,39406:21650,39407:21627,39408:21632,39409:21622,39410:21636,39411:21648,39412:21638,39413:21703,39414:21666,39415:21688,39416:21669,39417:21676,39418:21700,39419:21704,39420:21672,39488:21675,39489:21698,39490:21668,39491:21694,39492:21692,39493:21720,39494:21733,39495:21734,39496:21775,39497:21780,39498:21757,39499:21742,39500:21741,39501:21754,39502:21730,39503:21817,39504:21824,39505:21859,39506:21836,39507:21806,39508:21852,39509:21829,39510:21846,39511:21847,39512:21816,39513:21811,39514:21853,39515:21913,39516:21888,39517:21679,39518:21898,39519:21919,39520:21883,39521:21886,39522:21912,39523:21918,39524:21934,39525:21884,39526:21891,39527:21929,39528:21895,39529:21928,39530:21978,39531:21957,39532:21983,39533:21956,39534:21980,39535:21988,39536:21972,39537:22036,39538:22007,39539:22038,39540:22014,39541:22013,39542:22043,39543:22009,39544:22094,39545:22096,39546:29151,39547:22068,39548:22070,39549:22066,39550:22072,39552:22123,39553:22116,39554:22063,39555:22124,39556:22122,39557:22150,39558:22144,39559:22154,39560:22176,39561:22164,39562:22159,39563:22181,39564:22190,39565:22198,39566:22196,39567:22210,39568:22204,39569:22209,39570:22211,39571:22208,39572:22216,39573:22222,39574:22225,39575:22227,39576:22231,39577:22254,39578:22265,39579:22272,39580:22271,39581:22276,39582:22281,39583:22280,39584:22283,39585:22285,39586:22291,39587:22296,39588:22294,39589:21959,39590:22300,39591:22310,39592:22327,39593:22328,39594:22350,39595:22331,39596:22336,39597:22351,39598:22377,39599:22464,39600:22408,39601:22369,39602:22399,39603:22409,39604:22419,39605:22432,39606:22451,39607:22436,39608:22442,39609:22448,39610:22467,39611:22470,39612:22484,39613:22482,39614:22483,39615:22538,39616:22486,39617:22499,39618:22539,39619:22553,39620:22557,39621:22642,39622:22561,39623:22626,39624:22603,39625:22640,39626:27584,39627:22610,39628:22589,39629:22649,39630:22661,39631:22713,39632:22687,39633:22699,39634:22714,39635:22750,39636:22715,39637:22712,39638:22702,39639:22725,39640:22739,39641:22737,39642:22743,39643:22745,39644:22744,39645:22757,39646:22748,39647:22756,39648:22751,39649:22767,39650:22778,39651:22777,39652:22779,39653:22780,39654:22781,39655:22786,39656:22794,39657:22800,39658:22811,39659:26790,39660:22821,39661:22828,39662:22829,39663:22834,39664:22840,39665:22846,39666:31442,39667:22869,39668:22864,39669:22862,39670:22874,39671:22872,39672:22882,39673:22880,39674:22887,39675:22892,39676:22889,39744:22904,39745:22913,39746:22941,39747:20318,39748:20395,39749:22947,39750:22962,39751:22982,39752:23016,39753:23004,39754:22925,39755:23001,39756:23002,39757:23077,39758:23071,39759:23057,39760:23068,39761:23049,39762:23066,39763:23104,39764:23148,39765:23113,39766:23093,39767:23094,39768:23138,39769:23146,39770:23194,39771:23228,39772:23230,39773:23243,39774:23234,39775:23229,39776:23267,39777:23255,39778:23270,39779:23273,39780:23254,39781:23290,39782:23291,39783:23308,39784:23307,39785:23318,39786:23346,39787:23248,39788:23338,39789:23350,39790:23358,39791:23363,39792:23365,39793:23360,39794:23377,39795:23381,39796:23386,39797:23387,39798:23397,39799:23401,39800:23408,39801:23411,39802:23413,39803:23416,39804:25992,39805:23418,39806:23424,39808:23427,39809:23462,39810:23480,39811:23491,39812:23495,39813:23497,39814:23508,39815:23504,39816:23524,39817:23526,39818:23522,39819:23518,39820:23525,39821:23531,39822:23536,39823:23542,39824:23539,39825:23557,39826:23559,39827:23560,39828:23565,39829:23571,39830:23584,39831:23586,39832:23592,39833:23608,39834:23609,39835:23617,39836:23622,39837:23630,39838:23635,39839:23632,39840:23631,39841:23409,39842:23660,39843:23662,39844:20066,39845:23670,39846:23673,39847:23692,39848:23697,39849:23700,39850:22939,39851:23723,39852:23739,39853:23734,39854:23740,39855:23735,39856:23749,39857:23742,39858:23751,39859:23769,39860:23785,39861:23805,39862:23802,39863:23789,39864:23948,39865:23786,39866:23819,39867:23829,39868:23831,39869:23900,39870:23839,39871:23835,39872:23825,39873:23828,39874:23842,39875:23834,39876:23833,39877:23832,39878:23884,39879:23890,39880:23886,39881:23883,39882:23916,39883:23923,39884:23926,39885:23943,39886:23940,39887:23938,39888:23970,39889:23965,39890:23980,39891:23982,39892:23997,39893:23952,39894:23991,39895:23996,39896:24009,39897:24013,39898:24019,39899:24018,39900:24022,39901:24027,39902:24043,39903:24050,39904:24053,39905:24075,39906:24090,39907:24089,39908:24081,39909:24091,39910:24118,39911:24119,39912:24132,39913:24131,39914:24128,39915:24142,39916:24151,39917:24148,39918:24159,39919:24162,39920:24164,39921:24135,39922:24181,39923:24182,39924:24186,39925:40636,39926:24191,39927:24224,39928:24257,39929:24258,39930:24264,39931:24272,39932:24271,4e4:24278,40001:24291,40002:24285,40003:24282,40004:24283,40005:24290,40006:24289,40007:24296,40008:24297,40009:24300,40010:24305,40011:24307,40012:24304,40013:24308,40014:24312,40015:24318,40016:24323,40017:24329,40018:24413,40019:24412,40020:24331,40021:24337,40022:24342,40023:24361,40024:24365,40025:24376,40026:24385,40027:24392,40028:24396,40029:24398,40030:24367,40031:24401,40032:24406,40033:24407,40034:24409,40035:24417,40036:24429,40037:24435,40038:24439,40039:24451,40040:24450,40041:24447,40042:24458,40043:24456,40044:24465,40045:24455,40046:24478,40047:24473,40048:24472,40049:24480,40050:24488,40051:24493,40052:24508,40053:24534,40054:24571,40055:24548,40056:24568,40057:24561,40058:24541,40059:24755,40060:24575,40061:24609,40062:24672,40064:24601,40065:24592,40066:24617,40067:24590,40068:24625,40069:24603,40070:24597,40071:24619,40072:24614,40073:24591,40074:24634,40075:24666,40076:24641,40077:24682,40078:24695,40079:24671,40080:24650,40081:24646,40082:24653,40083:24675,40084:24643,40085:24676,40086:24642,40087:24684,40088:24683,40089:24665,40090:24705,40091:24717,40092:24807,40093:24707,40094:24730,40095:24708,40096:24731,40097:24726,40098:24727,40099:24722,40100:24743,40101:24715,40102:24801,40103:24760,40104:24800,40105:24787,40106:24756,40107:24560,40108:24765,40109:24774,40110:24757,40111:24792,40112:24909,40113:24853,40114:24838,40115:24822,40116:24823,40117:24832,40118:24820,40119:24826,40120:24835,40121:24865,40122:24827,40123:24817,40124:24845,40125:24846,40126:24903,40127:24894,40128:24872,40129:24871,40130:24906,40131:24895,40132:24892,40133:24876,40134:24884,40135:24893,40136:24898,40137:24900,40138:24947,40139:24951,40140:24920,40141:24921,40142:24922,40143:24939,40144:24948,40145:24943,40146:24933,40147:24945,40148:24927,40149:24925,40150:24915,40151:24949,40152:24985,40153:24982,40154:24967,40155:25004,40156:24980,40157:24986,40158:24970,40159:24977,40160:25003,40161:25006,40162:25036,40163:25034,40164:25033,40165:25079,40166:25032,40167:25027,40168:25030,40169:25018,40170:25035,40171:32633,40172:25037,40173:25062,40174:25059,40175:25078,40176:25082,40177:25076,40178:25087,40179:25085,40180:25084,40181:25086,40182:25088,40183:25096,40184:25097,40185:25101,40186:25100,40187:25108,40188:25115,40256:25118,40257:25121,40258:25130,40259:25134,40260:25136,40261:25138,40262:25139,40263:25153,40264:25166,40265:25182,40266:25187,40267:25179,40268:25184,40269:25192,40270:25212,40271:25218,40272:25225,40273:25214,40274:25234,40275:25235,40276:25238,40277:25300,40278:25219,40279:25236,40280:25303,40281:25297,40282:25275,40283:25295,40284:25343,40285:25286,40286:25812,40287:25288,40288:25308,40289:25292,40290:25290,40291:25282,40292:25287,40293:25243,40294:25289,40295:25356,40296:25326,40297:25329,40298:25383,40299:25346,40300:25352,40301:25327,40302:25333,40303:25424,40304:25406,40305:25421,40306:25628,40307:25423,40308:25494,40309:25486,40310:25472,40311:25515,40312:25462,40313:25507,40314:25487,40315:25481,40316:25503,40317:25525,40318:25451,40320:25449,40321:25534,40322:25577,40323:25536,40324:25542,40325:25571,40326:25545,40327:25554,40328:25590,40329:25540,40330:25622,40331:25652,40332:25606,40333:25619,40334:25638,40335:25654,40336:25885,40337:25623,40338:25640,40339:25615,40340:25703,40341:25711,40342:25718,40343:25678,40344:25898,40345:25749,40346:25747,40347:25765,40348:25769,40349:25736,40350:25788,40351:25818,40352:25810,40353:25797,40354:25799,40355:25787,40356:25816,40357:25794,40358:25841,40359:25831,40360:33289,40361:25824,40362:25825,40363:25260,40364:25827,40365:25839,40366:25900,40367:25846,40368:25844,40369:25842,40370:25850,40371:25856,40372:25853,40373:25880,40374:25884,40375:25861,40376:25892,40377:25891,40378:25899,40379:25908,40380:25909,40381:25911,40382:25910,40383:25912,40384:30027,40385:25928,40386:25942,40387:25941,40388:25933,40389:25944,40390:25950,40391:25949,40392:25970,40393:25976,40394:25986,40395:25987,40396:35722,40397:26011,40398:26015,40399:26027,40400:26039,40401:26051,40402:26054,40403:26049,40404:26052,40405:26060,40406:26066,40407:26075,40408:26073,40409:26080,40410:26081,40411:26097,40412:26482,40413:26122,40414:26115,40415:26107,40416:26483,40417:26165,40418:26166,40419:26164,40420:26140,40421:26191,40422:26180,40423:26185,40424:26177,40425:26206,40426:26205,40427:26212,40428:26215,40429:26216,40430:26207,40431:26210,40432:26224,40433:26243,40434:26248,40435:26254,40436:26249,40437:26244,40438:26264,40439:26269,40440:26305,40441:26297,40442:26313,40443:26302,40444:26300,40512:26308,40513:26296,40514:26326,40515:26330,40516:26336,40517:26175,40518:26342,40519:26345,40520:26352,40521:26357,40522:26359,40523:26383,40524:26390,40525:26398,40526:26406,40527:26407,40528:38712,40529:26414,40530:26431,40531:26422,40532:26433,40533:26424,40534:26423,40535:26438,40536:26462,40537:26464,40538:26457,40539:26467,40540:26468,40541:26505,40542:26480,40543:26537,40544:26492,40545:26474,40546:26508,40547:26507,40548:26534,40549:26529,40550:26501,40551:26551,40552:26607,40553:26548,40554:26604,40555:26547,40556:26601,40557:26552,40558:26596,40559:26590,40560:26589,40561:26594,40562:26606,40563:26553,40564:26574,40565:26566,40566:26599,40567:27292,40568:26654,40569:26694,40570:26665,40571:26688,40572:26701,40573:26674,40574:26702,40576:26803,40577:26667,40578:26713,40579:26723,40580:26743,40581:26751,40582:26783,40583:26767,40584:26797,40585:26772,40586:26781,40587:26779,40588:26755,40589:27310,40590:26809,40591:26740,40592:26805,40593:26784,40594:26810,40595:26895,40596:26765,40597:26750,40598:26881,40599:26826,40600:26888,40601:26840,40602:26914,40603:26918,40604:26849,40605:26892,40606:26829,40607:26836,40608:26855,40609:26837,40610:26934,40611:26898,40612:26884,40613:26839,40614:26851,40615:26917,40616:26873,40617:26848,40618:26863,40619:26920,40620:26922,40621:26906,40622:26915,40623:26913,40624:26822,40625:27001,40626:26999,40627:26972,40628:27e3,40629:26987,40630:26964,40631:27006,40632:26990,40633:26937,40634:26996,40635:26941,40636:26969,40637:26928,40638:26977,40639:26974,40640:26973,40641:27009,40642:26986,40643:27058,40644:27054,40645:27088,40646:27071,40647:27073,40648:27091,40649:27070,40650:27086,40651:23528,40652:27082,40653:27101,40654:27067,40655:27075,40656:27047,40657:27182,40658:27025,40659:27040,40660:27036,40661:27029,40662:27060,40663:27102,40664:27112,40665:27138,40666:27163,40667:27135,40668:27402,40669:27129,40670:27122,40671:27111,40672:27141,40673:27057,40674:27166,40675:27117,40676:27156,40677:27115,40678:27146,40679:27154,40680:27329,40681:27171,40682:27155,40683:27204,40684:27148,40685:27250,40686:27190,40687:27256,40688:27207,40689:27234,40690:27225,40691:27238,40692:27208,40693:27192,40694:27170,40695:27280,40696:27277,40697:27296,40698:27268,40699:27298,40700:27299,40768:27287,40769:34327,40770:27323,40771:27331,40772:27330,40773:27320,40774:27315,40775:27308,40776:27358,40777:27345,40778:27359,40779:27306,40780:27354,40781:27370,40782:27387,40783:27397,40784:34326,40785:27386,40786:27410,40787:27414,40788:39729,40789:27423,40790:27448,40791:27447,40792:30428,40793:27449,40794:39150,40795:27463,40796:27459,40797:27465,40798:27472,40799:27481,40800:27476,40801:27483,40802:27487,40803:27489,40804:27512,40805:27513,40806:27519,40807:27520,40808:27524,40809:27523,40810:27533,40811:27544,40812:27541,40813:27550,40814:27556,40815:27562,40816:27563,40817:27567,40818:27570,40819:27569,40820:27571,40821:27575,40822:27580,40823:27590,40824:27595,40825:27603,40826:27615,40827:27628,40828:27627,40829:27635,40830:27631,40832:40638,40833:27656,40834:27667,40835:27668,40836:27675,40837:27684,40838:27683,40839:27742,40840:27733,40841:27746,40842:27754,40843:27778,40844:27789,40845:27802,40846:27777,40847:27803,40848:27774,40849:27752,40850:27763,40851:27794,40852:27792,40853:27844,40854:27889,40855:27859,40856:27837,40857:27863,40858:27845,40859:27869,40860:27822,40861:27825,40862:27838,40863:27834,40864:27867,40865:27887,40866:27865,40867:27882,40868:27935,40869:34893,40870:27958,40871:27947,40872:27965,40873:27960,40874:27929,40875:27957,40876:27955,40877:27922,40878:27916,40879:28003,40880:28051,40881:28004,40882:27994,40883:28025,40884:27993,40885:28046,40886:28053,40887:28644,40888:28037,40889:28153,40890:28181,40891:28170,40892:28085,40893:28103,40894:28134,40895:28088,40896:28102,40897:28140,40898:28126,40899:28108,40900:28136,40901:28114,40902:28101,40903:28154,40904:28121,40905:28132,40906:28117,40907:28138,40908:28142,40909:28205,40910:28270,40911:28206,40912:28185,40913:28274,40914:28255,40915:28222,40916:28195,40917:28267,40918:28203,40919:28278,40920:28237,40921:28191,40922:28227,40923:28218,40924:28238,40925:28196,40926:28415,40927:28189,40928:28216,40929:28290,40930:28330,40931:28312,40932:28361,40933:28343,40934:28371,40935:28349,40936:28335,40937:28356,40938:28338,40939:28372,40940:28373,40941:28303,40942:28325,40943:28354,40944:28319,40945:28481,40946:28433,40947:28748,40948:28396,40949:28408,40950:28414,40951:28479,40952:28402,40953:28465,40954:28399,40955:28466,40956:28364,161:65377,162:65378,163:65379,164:65380,165:65381,166:65382,167:65383,168:65384,169:65385,170:65386,171:65387,172:65388,173:65389,174:65390,175:65391,176:65392,177:65393,178:65394,179:65395,180:65396,181:65397,182:65398,183:65399,184:65400,185:65401,186:65402,187:65403,188:65404,189:65405,190:65406,191:65407,192:65408,193:65409,194:65410,195:65411,196:65412,197:65413,198:65414,199:65415,200:65416,201:65417,202:65418,203:65419,204:65420,205:65421,206:65422,207:65423,208:65424,209:65425,210:65426,211:65427,212:65428,213:65429,214:65430,215:65431,216:65432,217:65433,218:65434,219:65435,220:65436,221:65437,222:65438,223:65439,57408:28478,57409:28435,57410:28407,57411:28550,57412:28538,57413:28536,57414:28545,57415:28544,57416:28527,57417:28507,57418:28659,57419:28525,57420:28546,57421:28540,57422:28504,57423:28558,57424:28561,57425:28610,57426:28518,57427:28595,57428:28579,57429:28577,57430:28580,57431:28601,57432:28614,57433:28586,57434:28639,57435:28629,57436:28652,57437:28628,57438:28632,57439:28657,57440:28654,57441:28635,57442:28681,57443:28683,57444:28666,57445:28689,57446:28673,57447:28687,57448:28670,57449:28699,57450:28698,57451:28532,57452:28701,57453:28696,57454:28703,57455:28720,57456:28734,57457:28722,57458:28753,57459:28771,57460:28825,57461:28818,57462:28847,57463:28913,57464:28844,57465:28856,57466:28851,57467:28846,57468:28895,57469:28875,57470:28893,57472:28889,57473:28937,57474:28925,57475:28956,57476:28953,57477:29029,57478:29013,57479:29064,57480:29030,57481:29026,57482:29004,57483:29014,57484:29036,57485:29071,57486:29179,57487:29060,57488:29077,57489:29096,57490:29100,57491:29143,57492:29113,57493:29118,57494:29138,57495:29129,57496:29140,57497:29134,57498:29152,57499:29164,57500:29159,57501:29173,57502:29180,57503:29177,57504:29183,57505:29197,57506:29200,57507:29211,57508:29224,57509:29229,57510:29228,57511:29232,57512:29234,57513:29243,57514:29244,57515:29247,57516:29248,57517:29254,57518:29259,57519:29272,57520:29300,57521:29310,57522:29314,57523:29313,57524:29319,57525:29330,57526:29334,57527:29346,57528:29351,57529:29369,57530:29362,57531:29379,57532:29382,57533:29380,57534:29390,57535:29394,57536:29410,57537:29408,57538:29409,57539:29433,57540:29431,57541:20495,57542:29463,57543:29450,57544:29468,57545:29462,57546:29469,57547:29492,57548:29487,57549:29481,57550:29477,57551:29502,57552:29518,57553:29519,57554:40664,57555:29527,57556:29546,57557:29544,57558:29552,57559:29560,57560:29557,57561:29563,57562:29562,57563:29640,57564:29619,57565:29646,57566:29627,57567:29632,57568:29669,57569:29678,57570:29662,57571:29858,57572:29701,57573:29807,57574:29733,57575:29688,57576:29746,57577:29754,57578:29781,57579:29759,57580:29791,57581:29785,57582:29761,57583:29788,57584:29801,57585:29808,57586:29795,57587:29802,57588:29814,57589:29822,57590:29835,57591:29854,57592:29863,57593:29898,57594:29903,57595:29908,57596:29681,57664:29920,57665:29923,57666:29927,57667:29929,57668:29934,57669:29938,57670:29936,57671:29937,57672:29944,57673:29943,57674:29956,57675:29955,57676:29957,57677:29964,57678:29966,57679:29965,57680:29973,57681:29971,57682:29982,57683:29990,57684:29996,57685:30012,57686:30020,57687:30029,57688:30026,57689:30025,57690:30043,57691:30022,57692:30042,57693:30057,57694:30052,57695:30055,57696:30059,57697:30061,57698:30072,57699:30070,57700:30086,57701:30087,57702:30068,57703:30090,57704:30089,57705:30082,57706:30100,57707:30106,57708:30109,57709:30117,57710:30115,57711:30146,57712:30131,57713:30147,57714:30133,57715:30141,57716:30136,57717:30140,57718:30129,57719:30157,57720:30154,57721:30162,57722:30169,57723:30179,57724:30174,57725:30206,57726:30207,57728:30204,57729:30209,57730:30192,57731:30202,57732:30194,57733:30195,57734:30219,57735:30221,57736:30217,57737:30239,57738:30247,57739:30240,57740:30241,57741:30242,57742:30244,57743:30260,57744:30256,57745:30267,57746:30279,57747:30280,57748:30278,57749:30300,57750:30296,57751:30305,57752:30306,57753:30312,57754:30313,57755:30314,57756:30311,57757:30316,57758:30320,57759:30322,57760:30326,57761:30328,57762:30332,57763:30336,57764:30339,57765:30344,57766:30347,57767:30350,57768:30358,57769:30355,57770:30361,57771:30362,57772:30384,57773:30388,57774:30392,57775:30393,57776:30394,57777:30402,57778:30413,57779:30422,57780:30418,57781:30430,57782:30433,57783:30437,57784:30439,57785:30442,57786:34351,57787:30459,57788:30472,57789:30471,57790:30468,57791:30505,57792:30500,57793:30494,57794:30501,57795:30502,57796:30491,57797:30519,57798:30520,57799:30535,57800:30554,57801:30568,57802:30571,57803:30555,57804:30565,57805:30591,57806:30590,57807:30585,57808:30606,57809:30603,57810:30609,57811:30624,57812:30622,57813:30640,57814:30646,57815:30649,57816:30655,57817:30652,57818:30653,57819:30651,57820:30663,57821:30669,57822:30679,57823:30682,57824:30684,57825:30691,57826:30702,57827:30716,57828:30732,57829:30738,57830:31014,57831:30752,57832:31018,57833:30789,57834:30862,57835:30836,57836:30854,57837:30844,57838:30874,57839:30860,57840:30883,57841:30901,57842:30890,57843:30895,57844:30929,57845:30918,57846:30923,57847:30932,57848:30910,57849:30908,57850:30917,57851:30922,57852:30956,57920:30951,57921:30938,57922:30973,57923:30964,57924:30983,57925:30994,57926:30993,57927:31001,57928:31020,57929:31019,57930:31040,57931:31072,57932:31063,57933:31071,57934:31066,57935:31061,57936:31059,57937:31098,57938:31103,57939:31114,57940:31133,57941:31143,57942:40779,57943:31146,57944:31150,57945:31155,57946:31161,57947:31162,57948:31177,57949:31189,57950:31207,57951:31212,57952:31201,57953:31203,57954:31240,57955:31245,57956:31256,57957:31257,57958:31264,57959:31263,57960:31104,57961:31281,57962:31291,57963:31294,57964:31287,57965:31299,57966:31319,57967:31305,57968:31329,57969:31330,57970:31337,57971:40861,57972:31344,57973:31353,57974:31357,57975:31368,57976:31383,57977:31381,57978:31384,57979:31382,57980:31401,57981:31432,57982:31408,57984:31414,57985:31429,57986:31428,57987:31423,57988:36995,57989:31431,57990:31434,57991:31437,57992:31439,57993:31445,57994:31443,57995:31449,57996:31450,57997:31453,57998:31457,57999:31458,58e3:31462,58001:31469,58002:31472,58003:31490,58004:31503,58005:31498,58006:31494,58007:31539,58008:31512,58009:31513,58010:31518,58011:31541,58012:31528,58013:31542,58014:31568,58015:31610,58016:31492,58017:31565,58018:31499,58019:31564,58020:31557,58021:31605,58022:31589,58023:31604,58024:31591,58025:31600,58026:31601,58027:31596,58028:31598,58029:31645,58030:31640,58031:31647,58032:31629,58033:31644,58034:31642,58035:31627,58036:31634,58037:31631,58038:31581,58039:31641,58040:31691,58041:31681,58042:31692,58043:31695,58044:31668,58045:31686,58046:31709,58047:31721,58048:31761,58049:31764,58050:31718,58051:31717,58052:31840,58053:31744,58054:31751,58055:31763,58056:31731,58057:31735,58058:31767,58059:31757,58060:31734,58061:31779,58062:31783,58063:31786,58064:31775,58065:31799,58066:31787,58067:31805,58068:31820,58069:31811,58070:31828,58071:31823,58072:31808,58073:31824,58074:31832,58075:31839,58076:31844,58077:31830,58078:31845,58079:31852,58080:31861,58081:31875,58082:31888,58083:31908,58084:31917,58085:31906,58086:31915,58087:31905,58088:31912,58089:31923,58090:31922,58091:31921,58092:31918,58093:31929,58094:31933,58095:31936,58096:31941,58097:31938,58098:31960,58099:31954,58100:31964,58101:31970,58102:39739,58103:31983,58104:31986,58105:31988,58106:31990,58107:31994,58108:32006,58176:32002,58177:32028,58178:32021,58179:32010,58180:32069,58181:32075,58182:32046,58183:32050,58184:32063,58185:32053,58186:32070,58187:32115,58188:32086,58189:32078,58190:32114,58191:32104,58192:32110,58193:32079,58194:32099,58195:32147,58196:32137,58197:32091,58198:32143,58199:32125,58200:32155,58201:32186,58202:32174,58203:32163,58204:32181,58205:32199,58206:32189,58207:32171,58208:32317,58209:32162,58210:32175,58211:32220,58212:32184,58213:32159,58214:32176,58215:32216,58216:32221,58217:32228,58218:32222,58219:32251,58220:32242,58221:32225,58222:32261,58223:32266,58224:32291,58225:32289,58226:32274,58227:32305,58228:32287,58229:32265,58230:32267,58231:32290,58232:32326,58233:32358,58234:32315,58235:32309,58236:32313,58237:32323,58238:32311,58240:32306,58241:32314,58242:32359,58243:32349,58244:32342,58245:32350,58246:32345,58247:32346,58248:32377,58249:32362,58250:32361,58251:32380,58252:32379,58253:32387,58254:32213,58255:32381,58256:36782,58257:32383,58258:32392,58259:32393,58260:32396,58261:32402,58262:32400,58263:32403,58264:32404,58265:32406,58266:32398,58267:32411,58268:32412,58269:32568,58270:32570,58271:32581,58272:32588,58273:32589,58274:32590,58275:32592,58276:32593,58277:32597,58278:32596,58279:32600,58280:32607,58281:32608,58282:32616,58283:32617,58284:32615,58285:32632,58286:32642,58287:32646,58288:32643,58289:32648,58290:32647,58291:32652,58292:32660,58293:32670,58294:32669,58295:32666,58296:32675,58297:32687,58298:32690,58299:32697,58300:32686,58301:32694,58302:32696,58303:35697,58304:32709,58305:32710,58306:32714,58307:32725,58308:32724,58309:32737,58310:32742,58311:32745,58312:32755,58313:32761,58314:39132,58315:32774,58316:32772,58317:32779,58318:32786,58319:32792,58320:32793,58321:32796,58322:32801,58323:32808,58324:32831,58325:32827,58326:32842,58327:32838,58328:32850,58329:32856,58330:32858,58331:32863,58332:32866,58333:32872,58334:32883,58335:32882,58336:32880,58337:32886,58338:32889,58339:32893,58340:32895,58341:32900,58342:32902,58343:32901,58344:32923,58345:32915,58346:32922,58347:32941,58348:20880,58349:32940,58350:32987,58351:32997,58352:32985,58353:32989,58354:32964,58355:32986,58356:32982,58357:33033,58358:33007,58359:33009,58360:33051,58361:33065,58362:33059,58363:33071,58364:33099,58432:38539,58433:33094,58434:33086,58435:33107,58436:33105,58437:33020,58438:33137,58439:33134,58440:33125,58441:33126,58442:33140,58443:33155,58444:33160,58445:33162,58446:33152,58447:33154,58448:33184,58449:33173,58450:33188,58451:33187,58452:33119,58453:33171,58454:33193,58455:33200,58456:33205,58457:33214,58458:33208,58459:33213,58460:33216,58461:33218,58462:33210,58463:33225,58464:33229,58465:33233,58466:33241,58467:33240,58468:33224,58469:33242,58470:33247,58471:33248,58472:33255,58473:33274,58474:33275,58475:33278,58476:33281,58477:33282,58478:33285,58479:33287,58480:33290,58481:33293,58482:33296,58483:33302,58484:33321,58485:33323,58486:33336,58487:33331,58488:33344,58489:33369,58490:33368,58491:33373,58492:33370,58493:33375,58494:33380,58496:33378,58497:33384,58498:33386,58499:33387,58500:33326,58501:33393,58502:33399,58503:33400,58504:33406,58505:33421,58506:33426,58507:33451,58508:33439,58509:33467,58510:33452,58511:33505,58512:33507,58513:33503,58514:33490,58515:33524,58516:33523,58517:33530,58518:33683,58519:33539,58520:33531,58521:33529,58522:33502,58523:33542,58524:33500,58525:33545,58526:33497,58527:33589,58528:33588,58529:33558,58530:33586,58531:33585,58532:33600,58533:33593,58534:33616,58535:33605,58536:33583,58537:33579,58538:33559,58539:33560,58540:33669,58541:33690,58542:33706,58543:33695,58544:33698,58545:33686,58546:33571,58547:33678,58548:33671,58549:33674,58550:33660,58551:33717,58552:33651,58553:33653,58554:33696,58555:33673,58556:33704,58557:33780,58558:33811,58559:33771,58560:33742,58561:33789,58562:33795,58563:33752,58564:33803,58565:33729,58566:33783,58567:33799,58568:33760,58569:33778,58570:33805,58571:33826,58572:33824,58573:33725,58574:33848,58575:34054,58576:33787,58577:33901,58578:33834,58579:33852,58580:34138,58581:33924,58582:33911,58583:33899,58584:33965,58585:33902,58586:33922,58587:33897,58588:33862,58589:33836,58590:33903,58591:33913,58592:33845,58593:33994,58594:33890,58595:33977,58596:33983,58597:33951,58598:34009,58599:33997,58600:33979,58601:34010,58602:34e3,58603:33985,58604:33990,58605:34006,58606:33953,58607:34081,58608:34047,58609:34036,58610:34071,58611:34072,58612:34092,58613:34079,58614:34069,58615:34068,58616:34044,58617:34112,58618:34147,58619:34136,58620:34120,58688:34113,58689:34306,58690:34123,58691:34133,58692:34176,58693:34212,58694:34184,58695:34193,58696:34186,58697:34216,58698:34157,58699:34196,58700:34203,58701:34282,58702:34183,58703:34204,58704:34167,58705:34174,58706:34192,58707:34249,58708:34234,58709:34255,58710:34233,58711:34256,58712:34261,58713:34269,58714:34277,58715:34268,58716:34297,58717:34314,58718:34323,58719:34315,58720:34302,58721:34298,58722:34310,58723:34338,58724:34330,58725:34352,58726:34367,58727:34381,58728:20053,58729:34388,58730:34399,58731:34407,58732:34417,58733:34451,58734:34467,58735:34473,58736:34474,58737:34443,58738:34444,58739:34486,58740:34479,58741:34500,58742:34502,58743:34480,58744:34505,58745:34851,58746:34475,58747:34516,58748:34526,58749:34537,58750:34540,58752:34527,58753:34523,58754:34543,58755:34578,58756:34566,58757:34568,58758:34560,58759:34563,58760:34555,58761:34577,58762:34569,58763:34573,58764:34553,58765:34570,58766:34612,58767:34623,58768:34615,58769:34619,58770:34597,58771:34601,58772:34586,58773:34656,58774:34655,58775:34680,58776:34636,58777:34638,58778:34676,58779:34647,58780:34664,58781:34670,58782:34649,58783:34643,58784:34659,58785:34666,58786:34821,58787:34722,58788:34719,58789:34690,58790:34735,58791:34763,58792:34749,58793:34752,58794:34768,58795:38614,58796:34731,58797:34756,58798:34739,58799:34759,58800:34758,58801:34747,58802:34799,58803:34802,58804:34784,58805:34831,58806:34829,58807:34814,58808:34806,58809:34807,58810:34830,58811:34770,58812:34833,58813:34838,58814:34837,58815:34850,58816:34849,58817:34865,58818:34870,58819:34873,58820:34855,58821:34875,58822:34884,58823:34882,58824:34898,58825:34905,58826:34910,58827:34914,58828:34923,58829:34945,58830:34942,58831:34974,58832:34933,58833:34941,58834:34997,58835:34930,58836:34946,58837:34967,58838:34962,58839:34990,58840:34969,58841:34978,58842:34957,58843:34980,58844:34992,58845:35007,58846:34993,58847:35011,58848:35012,58849:35028,58850:35032,58851:35033,58852:35037,58853:35065,58854:35074,58855:35068,58856:35060,58857:35048,58858:35058,58859:35076,58860:35084,58861:35082,58862:35091,58863:35139,58864:35102,58865:35109,58866:35114,58867:35115,58868:35137,58869:35140,58870:35131,58871:35126,58872:35128,58873:35148,58874:35101,58875:35168,58876:35166,58944:35174,58945:35172,58946:35181,58947:35178,58948:35183,58949:35188,58950:35191,58951:35198,58952:35203,58953:35208,58954:35210,58955:35219,58956:35224,58957:35233,58958:35241,58959:35238,58960:35244,58961:35247,58962:35250,58963:35258,58964:35261,58965:35263,58966:35264,58967:35290,58968:35292,58969:35293,58970:35303,58971:35316,58972:35320,58973:35331,58974:35350,58975:35344,58976:35340,58977:35355,58978:35357,58979:35365,58980:35382,58981:35393,58982:35419,58983:35410,58984:35398,58985:35400,58986:35452,58987:35437,58988:35436,58989:35426,58990:35461,58991:35458,58992:35460,58993:35496,58994:35489,58995:35473,58996:35493,58997:35494,58998:35482,58999:35491,59e3:35524,59001:35533,59002:35522,59003:35546,59004:35563,59005:35571,59006:35559,59008:35556,59009:35569,59010:35604,59011:35552,59012:35554,59013:35575,59014:35550,59015:35547,59016:35596,59017:35591,59018:35610,59019:35553,59020:35606,59021:35600,59022:35607,59023:35616,59024:35635,59025:38827,59026:35622,59027:35627,59028:35646,59029:35624,59030:35649,59031:35660,59032:35663,59033:35662,59034:35657,59035:35670,59036:35675,59037:35674,59038:35691,59039:35679,59040:35692,59041:35695,59042:35700,59043:35709,59044:35712,59045:35724,59046:35726,59047:35730,59048:35731,59049:35734,59050:35737,59051:35738,59052:35898,59053:35905,59054:35903,59055:35912,59056:35916,59057:35918,59058:35920,59059:35925,59060:35938,59061:35948,59062:35960,59063:35962,59064:35970,59065:35977,59066:35973,59067:35978,59068:35981,59069:35982,59070:35988,59071:35964,59072:35992,59073:25117,59074:36013,59075:36010,59076:36029,59077:36018,59078:36019,59079:36014,59080:36022,59081:36040,59082:36033,59083:36068,59084:36067,59085:36058,59086:36093,59087:36090,59088:36091,59089:36100,59090:36101,59091:36106,59092:36103,59093:36111,59094:36109,59095:36112,59096:40782,59097:36115,59098:36045,59099:36116,59100:36118,59101:36199,59102:36205,59103:36209,59104:36211,59105:36225,59106:36249,59107:36290,59108:36286,59109:36282,59110:36303,59111:36314,59112:36310,59113:36300,59114:36315,59115:36299,59116:36330,59117:36331,59118:36319,59119:36323,59120:36348,59121:36360,59122:36361,59123:36351,59124:36381,59125:36382,59126:36368,59127:36383,59128:36418,59129:36405,59130:36400,59131:36404,59132:36426,59200:36423,59201:36425,59202:36428,59203:36432,59204:36424,59205:36441,59206:36452,59207:36448,59208:36394,59209:36451,59210:36437,59211:36470,59212:36466,59213:36476,59214:36481,59215:36487,59216:36485,59217:36484,59218:36491,59219:36490,59220:36499,59221:36497,59222:36500,59223:36505,59224:36522,59225:36513,59226:36524,59227:36528,59228:36550,59229:36529,59230:36542,59231:36549,59232:36552,59233:36555,59234:36571,59235:36579,59236:36604,59237:36603,59238:36587,59239:36606,59240:36618,59241:36613,59242:36629,59243:36626,59244:36633,59245:36627,59246:36636,59247:36639,59248:36635,59249:36620,59250:36646,59251:36659,59252:36667,59253:36665,59254:36677,59255:36674,59256:36670,59257:36684,59258:36681,59259:36678,59260:36686,59261:36695,59262:36700,59264:36706,59265:36707,59266:36708,59267:36764,59268:36767,59269:36771,59270:36781,59271:36783,59272:36791,59273:36826,59274:36837,59275:36834,59276:36842,59277:36847,59278:36999,59279:36852,59280:36869,59281:36857,59282:36858,59283:36881,59284:36885,59285:36897,59286:36877,59287:36894,59288:36886,59289:36875,59290:36903,59291:36918,59292:36917,59293:36921,59294:36856,59295:36943,59296:36944,59297:36945,59298:36946,59299:36878,59300:36937,59301:36926,59302:36950,59303:36952,59304:36958,59305:36968,59306:36975,59307:36982,59308:38568,59309:36978,59310:36994,59311:36989,59312:36993,59313:36992,59314:37002,59315:37001,59316:37007,59317:37032,59318:37039,59319:37041,59320:37045,59321:37090,59322:37092,59323:25160,59324:37083,59325:37122,59326:37138,59327:37145,59328:37170,59329:37168,59330:37194,59331:37206,59332:37208,59333:37219,59334:37221,59335:37225,59336:37235,59337:37234,59338:37259,59339:37257,59340:37250,59341:37282,59342:37291,59343:37295,59344:37290,59345:37301,59346:37300,59347:37306,59348:37312,59349:37313,59350:37321,59351:37323,59352:37328,59353:37334,59354:37343,59355:37345,59356:37339,59357:37372,59358:37365,59359:37366,59360:37406,59361:37375,59362:37396,59363:37420,59364:37397,59365:37393,59366:37470,59367:37463,59368:37445,59369:37449,59370:37476,59371:37448,59372:37525,59373:37439,59374:37451,59375:37456,59376:37532,59377:37526,59378:37523,59379:37531,59380:37466,59381:37583,59382:37561,59383:37559,59384:37609,59385:37647,59386:37626,59387:37700,59388:37678,59456:37657,59457:37666,59458:37658,59459:37667,59460:37690,59461:37685,59462:37691,59463:37724,59464:37728,59465:37756,59466:37742,59467:37718,59468:37808,59469:37804,59470:37805,59471:37780,59472:37817,59473:37846,59474:37847,59475:37864,59476:37861,59477:37848,59478:37827,59479:37853,59480:37840,59481:37832,59482:37860,59483:37914,59484:37908,59485:37907,59486:37891,59487:37895,59488:37904,59489:37942,59490:37931,59491:37941,59492:37921,59493:37946,59494:37953,59495:37970,59496:37956,59497:37979,59498:37984,59499:37986,59500:37982,59501:37994,59502:37417,59503:38e3,59504:38005,59505:38007,59506:38013,59507:37978,59508:38012,59509:38014,59510:38017,59511:38015,59512:38274,59513:38279,59514:38282,59515:38292,59516:38294,59517:38296,59518:38297,59520:38304,59521:38312,59522:38311,59523:38317,59524:38332,59525:38331,59526:38329,59527:38334,59528:38346,59529:28662,59530:38339,59531:38349,59532:38348,59533:38357,59534:38356,59535:38358,59536:38364,59537:38369,59538:38373,59539:38370,59540:38433,59541:38440,59542:38446,59543:38447,59544:38466,59545:38476,59546:38479,59547:38475,59548:38519,59549:38492,59550:38494,59551:38493,59552:38495,59553:38502,59554:38514,59555:38508,59556:38541,59557:38552,59558:38549,59559:38551,59560:38570,59561:38567,59562:38577,59563:38578,59564:38576,59565:38580,59566:38582,59567:38584,59568:38585,59569:38606,59570:38603,59571:38601,59572:38605,59573:35149,59574:38620,59575:38669,59576:38613,59577:38649,59578:38660,59579:38662,59580:38664,59581:38675,59582:38670,59583:38673,59584:38671,59585:38678,59586:38681,59587:38692,59588:38698,59589:38704,59590:38713,59591:38717,59592:38718,59593:38724,59594:38726,59595:38728,59596:38722,59597:38729,59598:38748,59599:38752,59600:38756,59601:38758,59602:38760,59603:21202,59604:38763,59605:38769,59606:38777,59607:38789,59608:38780,59609:38785,59610:38778,59611:38790,59612:38795,59613:38799,59614:38800,59615:38812,59616:38824,59617:38822,59618:38819,59619:38835,59620:38836,59621:38851,59622:38854,59623:38856,59624:38859,59625:38876,59626:38893,59627:40783,59628:38898,59629:31455,59630:38902,59631:38901,59632:38927,59633:38924,59634:38968,59635:38948,59636:38945,59637:38967,59638:38973,59639:38982,59640:38991,59641:38987,59642:39019,59643:39023,59644:39024,59712:39025,59713:39028,59714:39027,59715:39082,59716:39087,59717:39089,59718:39094,59719:39108,59720:39107,59721:39110,59722:39145,59723:39147,59724:39171,59725:39177,59726:39186,59727:39188,59728:39192,59729:39201,59730:39197,59731:39198,59732:39204,59733:39200,59734:39212,59735:39214,59736:39229,59737:39230,59738:39234,59739:39241,59740:39237,59741:39248,59742:39243,59743:39249,59744:39250,59745:39244,59746:39253,59747:39319,59748:39320,59749:39333,59750:39341,59751:39342,59752:39356,59753:39391,59754:39387,59755:39389,59756:39384,59757:39377,59758:39405,59759:39406,59760:39409,59761:39410,59762:39419,59763:39416,59764:39425,59765:39439,59766:39429,59767:39394,59768:39449,59769:39467,59770:39479,59771:39493,59772:39490,59773:39488,59774:39491,59776:39486,59777:39509,59778:39501,59779:39515,59780:39511,59781:39519,59782:39522,59783:39525,59784:39524,59785:39529,59786:39531,59787:39530,59788:39597,59789:39600,59790:39612,59791:39616,59792:39631,59793:39633,59794:39635,59795:39636,59796:39646,59797:39647,59798:39650,59799:39651,59800:39654,59801:39663,59802:39659,59803:39662,59804:39668,59805:39665,59806:39671,59807:39675,59808:39686,59809:39704,59810:39706,59811:39711,59812:39714,59813:39715,59814:39717,59815:39719,59816:39720,59817:39721,59818:39722,59819:39726,59820:39727,59821:39730,59822:39748,59823:39747,59824:39759,59825:39757,59826:39758,59827:39761,59828:39768,59829:39796,59830:39827,59831:39811,59832:39825,59833:39830,59834:39831,59835:39839,59836:39840,59837:39848,59838:39860,59839:39872,59840:39882,59841:39865,59842:39878,59843:39887,59844:39889,59845:39890,59846:39907,59847:39906,59848:39908,59849:39892,59850:39905,59851:39994,59852:39922,59853:39921,59854:39920,59855:39957,59856:39956,59857:39945,59858:39955,59859:39948,59860:39942,59861:39944,59862:39954,59863:39946,59864:39940,59865:39982,59866:39963,59867:39973,59868:39972,59869:39969,59870:39984,59871:40007,59872:39986,59873:40006,59874:39998,59875:40026,59876:40032,59877:40039,59878:40054,59879:40056,59880:40167,59881:40172,59882:40176,59883:40201,59884:40200,59885:40171,59886:40195,59887:40198,59888:40234,59889:40230,59890:40367,59891:40227,59892:40223,59893:40260,59894:40213,59895:40210,59896:40257,59897:40255,59898:40254,59899:40262,59900:40264,59968:40285,59969:40286,59970:40292,59971:40273,59972:40272,59973:40281,59974:40306,59975:40329,59976:40327,59977:40363,59978:40303,59979:40314,59980:40346,59981:40356,59982:40361,59983:40370,59984:40388,59985:40385,59986:40379,59987:40376,59988:40378,59989:40390,59990:40399,59991:40386,59992:40409,59993:40403,59994:40440,59995:40422,59996:40429,59997:40431,59998:40445,59999:40474,6e4:40475,60001:40478,60002:40565,60003:40569,60004:40573,60005:40577,60006:40584,60007:40587,60008:40588,60009:40594,60010:40597,60011:40593,60012:40605,60013:40613,60014:40617,60015:40632,60016:40618,60017:40621,60018:38753,60019:40652,60020:40654,60021:40655,60022:40656,60023:40660,60024:40668,60025:40670,60026:40669,60027:40672,60028:40677,60029:40680,60030:40687,60032:40692,60033:40694,60034:40695,60035:40697,60036:40699,60037:40700,60038:40701,60039:40711,60040:40712,60041:30391,60042:40725,60043:40737,60044:40748,60045:40766,60046:40778,60047:40786,60048:40788,60049:40803,60050:40799,60051:40800,60052:40801,60053:40806,60054:40807,60055:40812,60056:40810,60057:40823,60058:40818,60059:40822,60060:40853,60061:40860,60062:40864,60063:22575,60064:27079,60065:36953,60066:29796,60067:20956,60068:29081}},function(o,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=r(1),c=r(2);e.decode=function(o,e){var r=new Uint8ClampedArray(o.length);r.set(o);for(var s=new t.default(285,256,0),a=new c.default(s,r),n=new Uint8ClampedArray(e),d=!1,l=0;l=t/2;){var l=s,i=n;if(n=d,(s=a).isZero())return null;a=l;for(var B=o.zero,k=s.getCoefficient(s.degree()),u=o.inverse(k);a.degree()>=s.degree()&&!a.isZero();){var C=a.degree()-s.degree(),m=o.multiply(a.getCoefficient(a.degree()),u);B=B.addOrSubtract(o.buildMonomial(C,m)),a=a.addOrSubtract(s.multiplyByMonomial(C,m))}if(d=B.multiplyPoly(n).addOrSubtract(i),a.degree()>=s.degree())return null}var f=d.getCoefficient(0);if(0===f)return null;var w=o.inverse(f);return[d.multiply(w),a.multiply(w)]}(s,s.buildMonomial(e,1),B,e);if(null===k)return null;var u=function(o,e){var r=e.degree();if(1===r)return[e.getCoefficient(1)];for(var t=new Array(r),c=0,s=1;sMath.abs(e.x-o.x);i?(s=Math.floor(o.y),a=Math.floor(o.x),n=Math.floor(e.y),d=Math.floor(e.x)):(s=Math.floor(o.x),a=Math.floor(o.y),n=Math.floor(e.x),d=Math.floor(e.y));for(var B=Math.abs(n-s),k=Math.abs(d-a),u=Math.floor(-B/2),C=s0){if(P===d)break;P+=m,u-=B}}for(var y=[],p=0;p=15&&w.length?w[0]:f,dimension:i}}e.locate=function(o){for(var e=[],r=[],s=[],a=[],n=function(t){for(var n=0,d=!1,l=[0,0,0,0,0],i=function(e){var s=o.get(e,t);if(s===d)n++;else{l=[l[1],l[2],l[3],l[4],n],n=1,d=s;var i=c(l)/7,B=Math.abs(l[0]-i)=o.bottom.startX&&m<=o.bottom.endX||C>=o.bottom.startX&&m<=o.bottom.endX||m<=o.bottom.startX&&C>=o.bottom.endX&&l[2]/(o.bottom.endX-o.bottom.startX)<1.5&&l[2]/(o.bottom.endX-o.bottom.startX)>.5}))).length>0?w[0].bottom=f:r.push({top:f,bottom:f})}if(u){var w,P=e-l[4],v=P-l[3];f={startX:v,y:t,endX:P};(w=a.filter((function(o){return v>=o.bottom.startX&&v<=o.bottom.endX||P>=o.bottom.startX&&v<=o.bottom.endX||v<=o.bottom.startX&&P>=o.bottom.endX&&l[2]/(o.bottom.endX-o.bottom.startX)<1.5&&l[2]/(o.bottom.endX-o.bottom.startX)>.5}))).length>0?w[0].bottom=f:a.push({top:f,bottom:f})}}},B=-1;B<=o.width;B++)i(B);e.push.apply(e,r.filter((function(o){return o.bottom.y!==t&&o.bottom.y-o.top.y>=2}))),r=r.filter((function(o){return o.bottom.y===t})),s.push.apply(s,a.filter((function(o){return o.bottom.y!==t}))),a=a.filter((function(o){return o.bottom.y===t}))},B=0;B<=o.height;B++)n(B);e.push.apply(e,r.filter((function(o){return o.bottom.y-o.top.y>=2}))),s.push.apply(s,a);var k=e.filter((function(o){return o.bottom.y-o.top.y>=2})).map((function(e){var r=(e.top.startX+e.top.endX+e.bottom.startX+e.bottom.endX)/4,t=(e.top.y+e.bottom.y+1)/2;if(o.get(Math.round(r),Math.round(t))){var s=[e.top.endX-e.top.startX,e.bottom.endX-e.bottom.startX,e.bottom.y-e.top.y+1],a=c(s)/s.length;return{score:d({x:Math.round(r),y:Math.round(t)},[1,1,3,1,1],o),x:r,y:t,size:a}}})).filter((function(o){return!!o})).sort((function(o,e){return o.score-e.score})).map((function(o,e,r){if(e>4)return null;var t=r.filter((function(o,r){return e!==r})).map((function(e){return{x:e.x,y:e.y,score:e.score+Math.pow(e.size-o.size,2)/o.size,size:e.size}})).sort((function(o,e){return o.score-e.score}));if(t.length<2)return null;var c=o.score+t[0].score+t[1].score;return{points:[o].concat(t.slice(0,2)),score:c}})).filter((function(o){return!!o})).sort((function(o,e){return o.score-e.score}));if(0===k.length)return null;var u=function(o,e,r){var c,s,a,n,d,l,i,B=t(o,e),k=t(e,r),u=t(o,r);return k>=B&&k>=u?(d=(c=[e,o,r])[0],l=c[1],i=c[2]):u>=k&&u>=B?(d=(s=[o,e,r])[0],l=s[1],i=s[2]):(d=(a=[o,r,e])[0],l=a[1],i=a[2]),(i.x-l.x)*(d.y-l.y)-(i.y-l.y)*(d.x-l.x)<0&&(d=(n=[i,d])[0],i=n[1]),{bottomLeft:d,topLeft:l,topRight:i}}(k[0].points[0],k[0].points[1],k[0].points[2]),C=u.topRight,m=u.topLeft,f=u.bottomLeft,w=i(o,s,C,m,f),P=[];w&&P.push({alignmentPattern:{x:w.alignmentPattern.x,y:w.alignmentPattern.y},bottomLeft:{x:f.x,y:f.y},dimension:w.dimension,topLeft:{x:m.x,y:m.y},topRight:{x:C.x,y:C.y}});var v=l(o,C),h=l(o,m),y=l(o,f),p=i(o,s,v,h,y);return p&&P.push({alignmentPattern:{x:p.alignmentPattern.x,y:p.alignmentPattern.y},bottomLeft:{x:y.x,y:y.y},topLeft:{x:h.x,y:h.y},topRight:{x:v.x,y:v.y},dimension:p.dimension}),0===P.length?null:P}}]).default})); -//# sourceMappingURL=/sm/261261d91f249d4079ae119cfa50f739467d90fc365078a671172e0f499e862a.map \ No newline at end of file diff --git a/clavis/clavis-vault/_old/cmd/clavitor/web/test_crypto.js b/clavis/clavis-vault/_old/cmd/clavitor/web/test_crypto.js deleted file mode 100644 index c1e6c7f..0000000 --- a/clavis/clavis-vault/_old/cmd/clavitor/web/test_crypto.js +++ /dev/null @@ -1,308 +0,0 @@ -/* - * clavitor — crypto test suite - * Runs in both QuickJS (CLI) and browser. - * - * CLI: clavitor-cli test-roundtrip - * Web: open browser console, paste: fetch('/app/test_crypto.js').then(r=>r.text()).then(eval) - * or load as