Skill v1.0.1
currentAutomated scan100/1002 files
version: "1.0.1" name: clawkeys-login-flow©™ description: The standard Lobsterized©™ ClawKeys©™ login flow. Upload or paste your identity key — no passwords, no accounts, just your claw.
ClawKeys©™ Login Skill — Re-entering the Ocean
🏗️ Flow Overview
Login is a single-screen flow. The user presents their identity file (or, planned: their raw key) and the session is restored. No password. No account lookup by email. The key IS the identity.
┌─────────────────────────────────────────────────────────────────────┐│ CLAWCHIVES©™ LOGIN SCREEN │└─────────────────────────────────────────────────────────────────────┘CURRENT IMPLEMENTATION:┌─────────────────────────────────────────┐│ Login with ClawKey©™ ││ ││ ┌─────────────────────────────────┐ ││ │ │ ││ │ Drop your identity file │ ││ │ here, or click to browse │ ││ │ │ ││ │ (.json files only) │ ││ │ │ ││ └─────────────────────────────────┘ ││ ││ [ ] PLANNED: ──────────────────────── ││ │ "Paste ClawKey" second option ││ └──────────────────────────────────── ││ ││ [Login with Identity File] ││ (disabled until file selected) │└─────────────────────────────────────────┘││ on "Login with Identity File"▼┌──────────────────────────┐│ FileReader.readAsText() ││ JSON.parse() ││ validateIdentityFile() │└──────────┬───────────────┘│ valid▼┌──────────────────────────┐│ hashToken(token) ││ → SHA-256(hu-key) hex │└──────────┬───────────────┘│▼┌──────────────────────────┐ ┌──────────────────────────┐│ POST /api/auth/token │────►│ Server verifies keyHash ││ { type:"human", │ │ (constant-time compare) ││ uuid, keyHash } │◄────│ → issues api-[32chars] │└──────────┬───────────────┘ └──────────────────────────┘│ success▼sessionStorage.setItem(×4)│▼onSuccess(uuid)│▼DashboardPLANNED ADDITION — "Paste ClawKey" path:┌───────────────────────────┐│ User pastes hu-[64chars] ││ + provides username input ││ (uuid lookup complexity — ││ see Planned Additions) │└──────────┬────────────────┘│▼validate → hash → POST /api/auth/token (same as upload path)
📋 Pre-Login Checklist
These conditions must be true before login can succeed:
- [ ] API server is running and reachable at the configured URL (
getApiBaseUrl()resolves correctly) - [ ]
GET /api/healthreturns200 OK - [ ] No active session already exists in
sessionStorage(ifcc_api_tokenis set, the app should redirect to the dashboard, not show the login screen) - [ ] User has their identity file (
clawchives_identity_{username}.json) available on disk or clipboard - [ ] The identity file was generated by ClawChives©™ and has NOT been manually edited (malformed JSON will fail
validateIdentityFile()) - [ ] The
uuidin the identity file is registered in the server'suserstable (i.e., the account exists — setup was previously completed) - [ ] Browser supports
crypto.subtle.digest()(required for SHA-256 hashing; available on all HTTPS origins and localhost)
🔑 Step-by-Step: The Login
Step 1 — File Selection
What the user sees:
- A drop zone / file upload area labeled for
.jsonfiles - "Login with Identity File" button — disabled until a file is selected
- No username or password fields
What the code does:
- Renders a file input (or styled drop zone) accepting
application/json/.json - On file selection via drag-and-drop OR click-to-browse:
- Stores the
Fileobject in component state (selectedFile) - Displays the filename as confirmation (e.g.,
clawchives_identity_alice.json selected) - Enables the "Login with Identity File" button
State changes:
selectedFile— the browserFileobject- Login button enabled state changes from
disabledtoactive
Step 2 — File Parse and Validation
Triggered by: User clicking "Login with Identity File"
What the code does:
// 1. Read the fileconst text = await readFileAsText(selectedFile) // FileReader.readAsText()// 2. Parse JSONconst parsed = JSON.parse(text)// 3. Validate structureconst identity = validateIdentityFile(parsed)// throws if: missing fields, wrong types, token doesn't start with "hu-"
`validateIdentityFile()` checks:
identity.tokenexists and is astringidentity.tokenstarts with"hu-"identity.tokenhas length of exactly 67 ("hu-"+ 64 chars)identity.uuidexists and is a non-emptystringidentity.usernameexists and is a non-emptystring
On validation failure:
- Error message displayed in the UI (e.g., "Invalid identity file — please select a ClawChives©™ identity file")
selectedFileremains set; user can try again without re-selecting- No API calls are made
Step 3 — Key Hashing
What the code does (immediately after successful validation):
const keyHash = await hashToken(identity.token)// hashToken: SHA-256(hu-key) → hex string// identity.token (raw hu- key) is NEVER sent to server
The raw `identity.token` (`hu-` key) is used exactly once — as input to hashToken(). After hashing, it is not stored, not logged, and not referenced again in the login flow.
Step 4 — Token Exchange
What the code does:
const response = await fetch(`${getApiBaseUrl()}/api/auth/token`, {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify({type: "human",uuid: identity.uuid,keyHash: keyHash})})const { token } = await response.json()
On success:
sessionStorage.setItem("cc_api_token", token)sessionStorage.setItem("cc_username", identity.username)sessionStorage.setItem("cc_user_uuid", identity.uuid)sessionStorage.setItem("cc_key_type", "human")onSuccess(identity.uuid) // triggers navigation to dashboard
🔐 Cryptographic Operations
validateIdentityFile(parsed: unknown): IdentityFile
// Location: src/components/auth/LoginForm.tsx (or src/lib/crypto.ts)interface IdentityFile {username: stringuuid: stringtoken: string // raw hu-[64chars] keycreatedAt: string}function validateIdentityFile(parsed: unknown): IdentityFile {if (typeof parsed !== "object" || parsed === null) throw new Error("Not an object")const { username, uuid, token, createdAt } = parsed as Record<string, unknown>if (typeof token !== "string") throw new Error("token must be a string")if (!token.startsWith("hu-")) throw new Error("token must start with hu-")if (token.length !== 67) throw new Error("token must be 67 characters")if (typeof uuid !== "string" || !uuid) throw new Error("uuid is required")if (typeof username !== "string" || !username) throw new Error("username is required")return { username, uuid, token, createdAt: String(createdAt ?? "") }}
hashToken(token: string): Promise<string>
// Location: src/lib/crypto.tsasync function hashToken(token: string): Promise<string> {const encoder = new TextEncoder()const data = encoder.encode(token)const hashBuffer = await crypto.subtle.digest("SHA-256", data)const hashArray = Array.from(new Uint8Array(hashBuffer))return hashArray.map(b => b.toString(16).padStart(2, "0")).join("")}// Input: "hu-aB3xK9mZ2pQ7rT1wYn..." (67 chars)// Output: "e3b0c44298fc1c149afb..." (64 hex chars = 256 bits)
Full Crypto Flow During Login
Browser (Client) Server──────────────── ──────identity.token = "hu-[64chars]"(read from identity file — stays in RAM)│▼hashToken(identity.token)→ crypto.subtle.digest("SHA-256", ...)│▼keyHash = "e3b0c44..." (hex, 64 chars)│├──── POST /api/auth/token ──────────► looks up user by uuid│ { type:"human", retrieves stored keyHash│ uuid: "3f4a2b1c-...", timingSafeEqual(│ keyHash: "e3b0c44..." } incoming keyHash,│ stored keyHash│ ) → true│ generates api-[32chars]│ INSERT INTO api_tokens│◄─── { token: "api-[32chars]" } ───────│▼sessionStorage.setItem(×4)→ onSuccess(uuid)→ Dashboard rendered
Security note: Server uses constant-time comparison for keyHash verification. Timing attacks that attempt to infer whether a hash is correct by measuring response latency are mitigated at the server level.
⚙️ Session State Written
On successful token exchange, four keys are written to sessionStorage:
| Key | Value | Description | |
|---|---|---|---|
cc_api_token | "api-[32chars]" | Bearer token for all subsequent authenticated API calls | |
cc_username | "alice" (from identity file) | Display name for UI personalization | |
cc_user_uuid | "3f4a2b1c-..." | User UUID; used for client-side identity tracking | |
cc_key_type | "human" | Key type; gates human-only UI features (e.g., r.jina.ai controls) |
Lifecycle: sessionStorage is tab-scoped. All four keys are cleared automatically when the browser tab is closed. A page refresh also clears session. Users must log in again after any session loss — this is expected and intentional behavior.
`cc_key_type` significance: Components check cc_key_type === "human" to conditionally render features unavailable to agent (lb-) sessions. Login always writes "human" here. Agent sessions are established via a separate flow and write "lobster" or "agent".
🔌 API Call Made
One API call is made during login. It uses getApiBaseUrl() from src/config/apiConfig.ts.
POST /api/auth/token
POST {apiBase}/api/auth/tokenContent-Type: application/jsonRequest Body:{"type": "human","uuid": "3f4a2b1c-dead-beef-cafe-0123456789ab","keyHash": "e3b0c44298fc1c149afb4c8996fb92427ae41e4649b934ca495991b7852b855"}Success Response — 200 OK:{"token": "api-aBcDeFgHiJkLmNoPqRsTuVwXyZ123456"}Error Responses:401 Unauthorized → keyHash does not match stored hash (constant-time comparison failed)404 Not Found → UUID does not exist in users table (account never registered)400 Bad Request → Missing or malformed type, uuid, or keyHash fields429 Too Many Requests → Rate limit exceeded (planned: security component 02)500 Internal → Server error
This endpoint does NOT require a bearer token. It is one of only two public endpoints (/api/auth/register and /api/auth/token). All other endpoints require Authorization: Bearer api-[32chars].
🆕 Planned Additions
Paste ClawKey Option
- [ ] PLANNED: Add a "Paste ClawKey" option alongside the existing "Upload Identity File" option on the login screen. Display as two side-by-side cards or tabs.
- [ ] PLANNED: "Paste ClawKey" UI: a
<textarea>where the user pastes their rawhu-[64chars]key directly (no file needed). - [ ] PLANNED: Client-side validation on paste input:
- Must start with
"hu-" - Must be exactly 67 characters total (
"hu-"+ 64) - Real-time feedback as user types/pastes (character count indicator, prefix check)
- [ ] PLANNED: On submit: hash the pasted key via
hashToken()and POST/api/auth/token— same as upload flow.
Complexity Flag — UUID and Username Recovery:
The upload flow extracts uuid and username from the identity file JSON. The paste flow has only the raw `hu-` key — no uuid, no username. This creates a resolution problem:
POST /api/auth/tokenrequires{ type, uuid, keyHash }— theuuidfield is mandatory.
Three possible resolution strategies (none yet implemented — flag for architectural decision):
- [ ] PLANNED — Option A (Server Lookup Endpoint): Add
GET /api/auth/lookup?keyHash={hash}that returns{ uuid, username }given a valid key hash. The paste flow calls this first, then calls/api/auth/token. Risk: Lookup endpoint could be abused for enumeration — must be rate-limited and return identical timing for hits/misses.
- [ ] PLANNED — Option B (Username Input Field): Add a username text input alongside the paste textarea. User provides both their
hu-key and their username. Server stores username → uuid mapping, so the server resolves uuid from username. Risk: Username typos cause auth failure with confusing error messages; usernames are not secret but treating them as lookup keys may be unexpected.
- [ ] PLANNED — Option C (Token-Only Auth Endpoint): Add a new endpoint
POST /api/auth/token-by-hashthat accepts only{ type, keyHash }and performs the lookup server-side — returning uuid and username alongside the api token. Cleanest UX. Requires a server-side index onkey_hashcolumn inuserstable for performance.
Recommendation: Option C is the most user-friendly (single paste, no extra input). Requires a server schema change (CREATE INDEX idx_users_key_hash ON users(key_hash)). Flag for implementation in Phase 3.
☠️ Known Failure Modes
Wrong Identity File
- Trigger: User uploads a JSON file that is not a ClawChives©™ identity file (e.g., a bookmark export, a settings file, or a different app's JSON)
- Symptom:
validateIdentityFile()throws; error message displayed to user - Recovery: Upload the correct
clawchives_identity_{username}.jsonfile
Expired or Revoked Session Token (api- token)
- Note: The identity file contains the
hu-key (root credential), not anapi-token. Theapi-token is issued fresh on every login. There is no "expired identity file" — only an expired session (which simply requires re-login using the same identity file).
Server Down or Unreachable
- Trigger:
POST /api/auth/tokenthrows a network error or times out - Symptom: Login button spins indefinitely or shows a generic network error
- Recovery: Verify API server is running, check Docker logs, ensure
VITE_API_URLresolves correctly for the current environment - Critical Check:
getApiBaseUrl()insrc/config/apiConfig.ts— production builds use relative paths; dev useshttp://localhost:4242
Account Does Not Exist (Wrong Server)
- Trigger: The UUID in the identity file does not exist in the server's
userstable (e.g., user is pointing at a different ClawChives©™ instance, or the data volume was reset) - Symptom:
POST /api/auth/tokenreturns404 Not Found - Recovery: Run setup again on this server instance (creates a new account) — or locate the correct server data volume
- Note: This is distinct from a wrong key — a 404 means the uuid isn't registered, not that the key is incorrect
keyHash Mismatch (Wrong Key File / Tampered File)
- Trigger: The
tokenin the uploaded file does not hash to the storedkeyHashon the server — either the file was manually edited, corrupted, or this is a different user's key file - Symptom:
POST /api/auth/tokenreturns401 Unauthorized - Recovery: Locate the original, unmodified identity file
Lost Identity File — The Terminal Failure
- Trigger: User cannot find their
clawchives_identity_{username}.jsonfile anywhere - Outcome: Account is permanently inaccessible. There is no password reset. There is no recovery email. The
hu-key cannot be reconstructed from the server (only its SHA-256 hash is stored). - Prevention: Keep at least two copies of the identity file in separate secure locations (password manager + encrypted external drive recommended)
- Server-Side Reality: The server has
{ uuid, username, keyHash }— butkeyHashis a one-way hash. The originalhu-key cannot be derived from it. Even a server administrator cannot recover the account without the original key.
Browser Crypto API Unavailable
- Trigger:
crypto.subtle.digest()is undefined (non-HTTPS context in a strict browser, or a very old browser) - Symptom:
hashToken()throws immediately; login fails before any network call - Recovery: Access ClawChives©™ over HTTPS. Local dev at
http://localhostis always permitted by browsers.
IndexedDB Version Conflict (Edge Case)
- Trigger: Client-side IndexedDB schema version has changed between sessions (e.g., after an app upgrade)
- Symptom: Dashboard fails to load after login; IndexedDB errors in browser console
- Recovery: Open browser DevTools → Application → Storage → IndexedDB → Delete the ClawChives©™ database → Refresh → Log in again. Client-side data is re-synced from the server.
🦞 Lobster Wisdom
"A lobster does not ask the ocean to remember its password. It returns to the same rock, presents its shell, and the sea knows it by its form alone. Your identity file is that shell — carry it as a lobster carries its molt: close, private, and irreplaceable. The ocean has no recovery desk. The claw that unlocks your burrow was grown by you alone."
This SKILL.md is part of the ClawChives©™ CrustAgent©™ skill library. Update this document whenever LoginForm.tsx, validateIdentityFile(), or the /api/auth/token endpoint contract is modified.