OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-identity commits / d2cccf9

Add comprehensive documentation (README, IDENTITY, AUTH, API)
- README.md: Quick start, all auth methods, permission model, federation - docs/IDENTITY.md: Data model, storage schema, auth flow, proof chains - docs/AUTH.md: Provider details, challenge system, rate limiting, security - docs/API.md: HTTP endpoint reference with implementation patterns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
d2cccf99bcd85449c40d69a2073da4d4c32014c5
by plankatron <fleeky@gmail.com> · 2026-03-10T21:16:30.000Z

Changed files (4) +1112 -0

README.md +449 -0
……@@ -0,0 +1,449 @@
1# hyper-identity
2
3Generic P2P identity management with Autobase, ANProto, and multi-method authentication.
4
5Extracted from [pear-git](https://github.com/holepunchto/pear-git)'s battle-tested identity backend, genericized with an injectable permission model for any application.
6
7## Features
8
9- **Multi-method authentication**: ANProto (Ed25519), BIP39 mnemonic, bearer tokens, SSH keys
10- **Autobase-backed registry**: Multi-writer identity consensus across federated peers
11- **Injectable permissions**: Generic `resources` model with custom `checkPermission` callback
12- **Device linking**: Blind-pairing over Hyperswarm for secure mnemonic exchange
13- **Encrypted storage**: Argon2id + XSalsa20-Poly1305 vault for sensitive data
14- **Challenge-response**: Replay-resistant authentication with server-side challenges
15- **Rate limiting**: Token bucket algorithm with persistent state
16
17## Quick Start
18
19```javascript
20import { AuthManager, IdentityRegistry } from 'hyper-identity'
21import Corestore from 'corestore'
22import Hyperbee from 'hyperbee'
23
24// Setup storage
25const store = new Corestore('./identity-storage')
26const core = store.get({ name: 'identity-db' })
27const db = new Hyperbee(core, { keyEncoding: 'utf-8', valueEncoding: 'json' })
28await db.ready()
29
30// Create identity registry (Autobase-backed)
31const registry = new IdentityRegistry(store, { db })
32await registry.ready()
33
34// Create auth manager
35const auth = new AuthManager({
36 identityRegistry: registry,
37 db
38})
39
40// Authenticate HTTP requests
41const result = await auth.authenticate(req)
42if (result) {
43 console.log(`User ${result.userId} authenticated via ${result.method}`)
44 console.log(`Permissions:`, result.permissions)
45}
46```
47
48## Authentication Methods
49
50### Web Identity (ANProto)
51
52Browser-generated Ed25519 keypairs with challenge-response verification.
53
54**Headers:**
55```
56X-Identity-PublicKey: <base64-encoded-public-key>
57X-Identity-Signature: <base64-encoded-signature>
58X-Identity-Challenge: <challenge-id>
59```
60
61**Flow:**
621. Client requests challenge: `POST /api/auth/challenge` → `{ challengeId, challenge }`
632. Client signs challenge with Ed25519 secret key
643. Client sends signed request with headers above
654. Server verifies signature and looks up/creates user
66
67```javascript
68import { ANProto } from 'hyper-identity'
69
70// Generate keypair (client-side)
71const keypair = ANProto.gen()
72const publicKey = ANProto.getPublicKey(keypair.publicKey)
73
74// Sign challenge
75const signature = ANProto.sign(challenge, keypair.secretKey)
76
77// Verify (server-side)
78const valid = ANProto.verify(challenge, signature, publicKey)
79```
80
81### Keet Identity (BIP39 Mnemonic)
82
83BIP39 mnemonic → Ed25519 keypair with device attestation proof chains.
84
85**Headers:**
86```
87X-Identity-Proof: <json-encoded-proof-chain>
88```
89
90**Requires:** `keet-identity-key` package (pass as `IdentityKey` option).
91
92```javascript
93import { AuthManager } from 'hyper-identity'
94import IdentityKey from 'keet-identity-key'
95
96const auth = new AuthManager({
97 identityRegistry: registry,
98 db,
99 IdentityKey // Enable keet identity support
100})
101```
102
103### Bearer Token
104
105API tokens with embedded permissions for CLI and service-to-service auth.
106
107**Header:**
108```
109Authorization: Bearer <token>
110```
111
112```javascript
113// Create token
114const { token } = await auth.createToken({
115 username: 'admin',
116 permissions: ['read', 'write', 'admin'],
117 resources: ['*']
118})
119
120// First start: auto-create admin token
121const adminToken = await auth.createDefaultAdmin()
122```
123
124### SSH Key
125
126Ed25519 and RSA SSH key signature verification.
127
128**Headers:**
129```
130X-SSH-PublicKey: <ssh-public-key-string>
131X-SSH-Signature: <base64-signature>
132X-SSH-Challenge: <challenge-id>
133```
134
135## Permission Model
136
137hyper-identity uses a generic `resources` model instead of application-specific terms. Each user has:
138
139```javascript
140{
141 userId: "hex-string",
142 username: "alice",
143 globalPermissions: ["read", "write"], // Global default
144 resources: [
145 { name: "project-a", permissions: ["read", "write", "admin"] },
146 { name: "*", permissions: ["read"] } // Wildcard
147 ]
148}
149```
150
151### Injectable Permission Check
152
153Override the default permission logic by passing a `checkPermission` callback:
154
155```javascript
156const auth = new AuthManager({
157 identityRegistry: registry,
158 db,
159 // Custom permission check for your application
160 checkPermission (auth, action, resourceName) {
161 // Example: notebook-based permissions for hypernote
162 if (auth.permissions.admin) return true
163 if (!auth.permissions[action]) return false
164 return auth.permissions.resources.includes('*') ||
165 auth.permissions.resources.includes(resourceName)
166 }
167})
168
169// Check access
170auth.hasResourceAccess(authResult, 'my-notebook', 'write') // true/false
171```
172
173### Authorization Middleware
174
175```javascript
176// One-liner for route protection
177const { authorized, auth, reason } = await authManager.authorizeRequest(req, 'write', 'my-resource')
178if (!authorized) {
179 res.statusCode = auth ? 403 : 401
180 res.end(JSON.stringify({ error: reason }))
181 return
182}
183```
184
185## Identity Registry (Autobase)
186
187The `IdentityRegistry` provides multi-writer consensus via Autobase. Multiple servers can accept identity operations independently and sync automatically.
188
189```javascript
190import { IdentityRegistry } from 'hyper-identity'
191
192const registry = new IdentityRegistry(store, { db })
193await registry.ready()
194
195// Create user
196const user = await registry.createUser({
197 username: 'alice',
198 name: 'Alice',
199 globalPermissions: ['read', 'write'],
200 resources: [{ name: '*', permissions: ['read'] }]
201})
202
203// Register identity
204await registry.registerIdentity({
205 identityId: publicKey,
206 method: 'web-identity',
207 publicKey
208})
209
210// Link identity to user
211await registry.linkIdentityToUser(publicKey, user.userId)
212
213// Look up user by identity
214const found = await registry.getUserByIdentity(publicKey)
215
216// Upgrade permissions
217await registry.upgradePermissions(user.userId, 'my-resource', ['read', 'write', 'admin'])
218
219// Merge duplicate accounts
220await registry.mergeAccounts(primaryUserId, duplicateUserId)
221
222// Add writer for federation
223await registry.addWriter(remoteWriterKey)
224```
225
226### Storage Schema
227
228```
229user:{userId} → User object with permissions, resources, identities
230identity:{identityId} → Identity object with method, publicKey, metadata
231pubkey:{publicKey} → { identityId } (reverse lookup)
232user-identity:{id} → { userId } (Autobase view index)
233token:{hash} → Bearer token data
234challenge:{id} → Challenge state
235vault:{name} → Encrypted storage entry
236```
237
238## Device Linking
239
240Secure mnemonic exchange between devices using blind-pairing over Hyperswarm.
241
242```javascript
243import { DeviceLinker, IdentityManager } from 'hyper-identity'
244import Hyperswarm from 'hyperswarm'
245
246const swarm = new Hyperswarm()
247const identityManager = new IdentityManager(db)
248await identityManager.ready()
249
250// Set mnemonic on source device
251await identityManager.setMnemonic('abandon ability able ...')
252
253const linker = new DeviceLinker(swarm, { identityManager })
254
255// Source device: create invite
256const { invite } = await linker.createInvite()
257console.log('Share this invite:', invite)
258
259// Target device: join with invite
260const result = await linker.joinWithInvite(invite)
261console.log('Received mnemonic:', result.mnemonic)
262```
263
264## Encrypted Storage
265
266Argon2id key derivation + XSalsa20-Poly1305 encryption for sensitive data.
267
268```javascript
269import { SecureStorage } from 'hyper-identity'
270
271const vault = new SecureStorage(db)
272
273// Store encrypted
274await vault.put('mnemonic', 'abandon ability able ...', 'my-password')
275
276// Retrieve and decrypt
277const mnemonic = await vault.get('mnemonic', 'my-password')
278
279// Check existence
280const exists = await vault.has('mnemonic')
281
282// Delete
283await vault.del('mnemonic')
284```
285
286## Challenge Manager
287
288Server-side challenge generation with replay prevention.
289
290```javascript
291import { ChallengeManager } from 'hyper-identity'
292
293const challenges = new ChallengeManager({
294 db,
295 ttl: 5 * 60 * 1000 // 5 minutes
296})
297
298// Generate challenge
299const { challengeId, challenge, expiresAt } = await challenges.generate()
300
301// Verify and consume (single-use)
302const result = await challenges.verify(challengeId, challenge)
303```
304
305## Rate Limiter
306
307Token bucket rate limiting with configurable operations.
308
309```javascript
310import { RateLimiter } from 'hyper-identity'
311
312const limiter = new RateLimiter({
313 limits: {
314 auth: { tokens: 1000, refillRate: 100, refillInterval: 60000 },
315 api: { tokens: 10000, refillRate: 1000, refillInterval: 60000 }
316 }
317})
318
319const { allowed, remaining, retryAfter } = await limiter.check('192.168.1.1', 'auth')
320```
321
322## Federation (Multi-Writer)
323
324Multiple servers can share an identity registry via Autobase:
325
326```javascript
327// Server A: create registry
328const registryA = new IdentityRegistry(storeA, { db: dbA })
329await registryA.ready()
330
331// Server B: join with A's bootstrap key
332const registryB = new IdentityRegistry(storeB, {
333 db: dbB,
334 bootstrapKey: registryA.base.key // Share this out-of-band
335})
336await registryB.ready()
337
338// Add each other as writers
339await registryA.addWriter(registryB.base.local.key)
340await registryB.addWriter(registryA.base.local.key)
341
342// Both servers now see all identity operations
343```
344
345## Custom Auth Providers
346
347Extend `BaseProvider` to add custom authentication methods:
348
349```javascript
350import { BaseProvider } from 'hyper-identity'
351
352class MyCustomProvider extends BaseProvider {
353 constructor (registry) {
354 super('my-provider')
355 this.registry = registry
356 }
357
358 async extractCredentials (req) {
359 const token = req.headers['x-my-token']
360 return token ? { token } : null
361 }
362
363 async authenticate (credentials) {
364 // Your verification logic
365 return {
366 identityId: 'my-identity-id',
367 method: 'my-provider',
368 credentials
369 }
370 }
371}
372
373// Register with AuthManager
374auth.addProvider(new MyCustomProvider(registry))
375```
376
377## API Reference
378
379### AuthManager
380
381| Method | Description |
382|--------|-------------|
383| `authenticate(req, opts)` | Authenticate HTTP request via all providers |
384| `authorizeRequest(req, action, resource)` | Authenticate + check permissions |
385| `hasResourceAccess(auth, resource, action)` | Check permission for action |
386| `addProvider(provider)` | Register custom auth provider |
387| `createToken(opts)` | Create bearer token |
388| `validateToken(token)` | Validate bearer token |
389| `revokeToken(hash)` | Revoke bearer token |
390| `createDefaultAdmin()` | Auto-create admin token on first start |
391| `generateChallenge(opts)` | Generate auth challenge |
392| `verifyChallenge(id, challenge)` | Verify and consume challenge |
393| `getUserWithIdentities(id)` | Get user + all linked identities |
394| `upgradeUserPermissions(userId, resource, perms)` | Upgrade user permissions |
395| `getClientIp(req)` | Extract client IP (proxy-aware) |
396
397### IdentityRegistry
398
399| Method | Description |
400|--------|-------------|
401| `ready()` | Initialize Autobase |
402| `createUser(opts)` | Create user with permissions |
403| `getUser(userId)` | Get user by ID |
404| `getUserByIdentity(identityId)` | Get user by identity |
405| `getUserByUsername(username)` | Get user by username |
406| `updateUser(userId, updates)` | Update user fields |
407| `deleteUser(userId)` | Delete user |
408| `listUsers()` | List all non-merged users |
409| `registerIdentity(identity)` | Register new identity |
410| `getIdentity(identityId)` | Get identity by ID |
411| `getIdentityByPublicKey(pk)` | Get identity by public key |
412| `linkIdentityToUser(identityId, userId)` | Link identity to user |
413| `upgradePermissions(userId, resource, perms)` | Upgrade permissions |
414| `mergeAccounts(primary, secondary)` | Merge duplicate accounts |
415| `revokeIdentity(identityId, opts)` | Revoke identity |
416| `restoreIdentity(identityId)` | Restore revoked identity |
417| `addWriter(key)` | Add Autobase writer for federation |
418
419### ANProto
420
421| Method | Description |
422|--------|-------------|
423| `gen()` | Generate Ed25519 keypair |
424| `loadKeypair(keys)` | Load keypair from hex strings |
425| `sign(challenge, secretKey)` | Sign challenge → base64 signature |
426| `verify(challenge, signature, publicKey)` | Verify signature |
427| `getPublicKey(publicKey)` | Get hex-encoded public key |
428
429## Dependencies
430
431- `autobase` — Multi-writer consensus
432- `hyperbee` — Persistent key-value storage
433- `hypercore` / `hypercore-crypto` — Core storage and Ed25519 cryptography
434- `hyperswarm` — P2P networking (for device linking)
435- `blind-pairing` — Secure device pairing protocol
436- `keet-identity-key` — BIP39 mnemonic identity (optional)
437- `sodium-native` — Argon2id + XSalsa20-Poly1305 encryption
438- `b4a` — Buffer utilities
439- `corestore` — Hypercore namespace management
440
441## Used By
442
443- **[pear-git](https://github.com/holepunchto/pear-git)** — P2P Git forge (original source)
444- **[hypernote](https://github.com/holepunchto/hypernote)** — P2P notebook app (migration planned)
445- **[hyperident-gateway](https://github.com/holepunchto/hyperident-gateway)** — P2P VPN (planned)
446
447## License
448
449MIT
docs/API.md +247 -0
……@@ -0,0 +1,247 @@
1# API Endpoints Reference
2
3hyper-identity does not register routes itself — your application creates routes that call AuthManager methods. This document shows the standard endpoints used by pear-git and hypernote.
4
5## Authentication Endpoints
6
7### Generate Challenge
8
9```http
10POST /api/auth/challenge
11Content-Type: application/json
12
13Response 200:
14{
15 "challengeId": "a1b2c3d4e5f6...",
16 "challenge": "random-hex-string",
17 "expiresAt": 1709900300000
18}
19```
20
21**Implementation:**
22```javascript
23router.post('/api/auth/challenge', async (req, res) => {
24 const { challengeId, challenge, expiresAt } = await authManager.generateChallenge()
25 json(res, { challengeId, challenge, expiresAt })
26})
27```
28
29### Who Am I
30
31```http
32GET /api/whoami
33Headers:
34 X-Identity-PublicKey: <base64-key>
35 X-Identity-Signature: <base64-sig>
36 X-Identity-Challenge: <challenge-id>
37
38Response 200:
39{
40 "userId": "a1b2c3d4...",
41 "username": "alice",
42 "method": "web-identity",
43 "permissions": {
44 "read": true,
45 "write": true,
46 "admin": false,
47 "resources": ["*"]
48 }
49}
50
51Response 401:
52{
53 "error": "Unauthorized"
54}
55```
56
57**Implementation:**
58```javascript
59router.get('/api/whoami', async (req, res) => {
60 const auth = await authManager.authenticate(req)
61 if (!auth) return sendAuthError(res)
62 json(res, {
63 userId: auth.userId,
64 username: auth.user?.username,
65 method: auth.method,
66 permissions: auth.permissions
67 })
68})
69```
70
71## Identity Management Endpoints
72
73### Generate Web Identity
74
75```http
76POST /api/identity/generate
77
78Response 200:
79{
80 "publicKey": "base64-public-key",
81 "secretKey": "base64-secret-key"
82}
83```
84
85**Implementation:**
86```javascript
87import { ANProto } from 'hyper-identity'
88
89router.post('/api/identity/generate', async (req, res) => {
90 const keypair = ANProto.gen()
91 json(res, {
92 publicKey: b4a.toString(keypair.publicKey, 'base64'),
93 secretKey: b4a.toString(keypair.secretKey, 'base64')
94 })
95})
96```
97
98### List User Identities
99
100```http
101GET /api/user/identities
102Headers: [auth headers]
103
104Response 200:
105{
106 "userId": "a1b2c3d4...",
107 "identities": [
108 {
109 "identityId": "GXsPilmS0Ek...",
110 "method": "web-identity",
111 "createdAt": 1709900000000
112 }
113 ]
114}
115```
116
117### Link Identity to User
118
119```http
120POST /api/admin/identity/link
121Headers: Authorization: Bearer <admin-token>
122Body:
123{
124 "identityId": "new-identity-id",
125 "userId": "existing-user-id"
126}
127```
128
129## Token Management Endpoints
130
131### Create Token
132
133```http
134POST /api/admin/tokens
135Headers: Authorization: Bearer <admin-token>
136Body:
137{
138 "username": "api-user",
139 "description": "CI/CD token",
140 "permissions": ["read", "write"],
141 "resources": ["my-project"]
142}
143
144Response 201:
145{
146 "token": "raw-token-value-only-shown-once",
147 "hash": "sha256-hash",
148 "username": "api-user",
149 "permissions": ["read", "write"],
150 "resources": ["my-project"]
151}
152```
153
154### List Tokens
155
156```http
157GET /api/admin/tokens
158Headers: Authorization: Bearer <admin-token>
159
160Response 200:
161{
162 "tokens": [
163 {
164 "hash": "sha256-hash",
165 "username": "admin",
166 "description": "Default admin token",
167 "permissions": ["read", "write", "admin"],
168 "createdAt": 1709900000000
169 }
170 ]
171}
172```
173
174### Revoke Token
175
176```http
177DELETE /api/admin/tokens/:hash
178Headers: Authorization: Bearer <admin-token>
179
180Response 200:
181{ "revoked": true }
182```
183
184## Permission Management Endpoints
185
186### Upgrade User Permissions
187
188```http
189POST /api/admin/users/:userId/permissions
190Headers: Authorization: Bearer <admin-token>
191Body:
192{
193 "resourceName": "my-project",
194 "permissions": ["read", "write", "admin"]
195}
196```
197
198### Revoke Identity
199
200```http
201POST /api/admin/identities/:identityId/revoke
202Headers: Authorization: Bearer <admin-token>
203Body:
204{
205 "reason": "Compromised device"
206}
207```
208
209### Restore Identity
210
211```http
212POST /api/admin/identities/:identityId/restore
213Headers: Authorization: Bearer <admin-token>
214```
215
216## Protected Route Pattern
217
218Standard pattern for protecting your application routes:
219
220```javascript
221import { AuthManager, sendAuthError } from 'hyper-identity'
222
223// Read-only endpoint (auth optional for public data)
224router.get('/api/items', async (req, res) => {
225 const items = await getItems()
226 json(res, { items })
227})
228
229// Write endpoint (auth required)
230router.post('/api/items', async (req, res) => {
231 const { authorized, auth, reason } = await authManager.authorizeRequest(req, 'write', 'items')
232 if (!auth) return sendAuthError(res, 'Authentication required', 401)
233 if (!authorized) return sendAuthError(res, reason, 403)
234
235 const item = await createItem(await readJson(req))
236 json(res, item, 201)
237})
238
239// Admin endpoint
240router.delete('/api/items/:id', async (req, res) => {
241 const { authorized, auth, reason } = await authManager.authorizeRequest(req, 'admin')
242 if (!authorized) return sendAuthError(res, reason || 'Unauthorized', auth ? 403 : 401)
243
244 await deleteItem(req.params.id)
245 json(res, { deleted: true })
246})
247```
docs/AUTH.md +218 -0
……@@ -0,0 +1,218 @@
1# Authentication System
2
3## Overview
4
5hyper-identity provides a pluggable authentication system with four built-in providers and support for custom providers. All providers follow the same interface: extract credentials from HTTP request, verify them, return an auth result.
6
7## Providers
8
9### Web Identity (ANProto)
10
11**Protocol:** Ed25519 challenge-response using hypercore-crypto.
12
13**Headers:**
14| Header | Required | Description |
15|--------|----------|-------------|
16| `X-Identity-PublicKey` | Yes | Base64-encoded Ed25519 public key |
17| `X-Identity-Signature` | For signed auth | Base64-encoded signature of challenge |
18| `X-Identity-Challenge` | For signed auth | Challenge ID from server |
19
20Also accepts legacy headers: `X-Wirecore-PublicKey`, `X-Wirecore-Signature`, `X-Wirecore-Challenge`.
21
22**Signed Auth Flow:**
231. Client: `POST /api/auth/challenge` → `{ challengeId, challenge, expiresAt }`
242. Client: Sign `challenge` with Ed25519 secret key → base64 signature
253. Client: Send request with all three headers
264. Server: Look up challenge, verify signature, authenticate
27
28**Unsigned Auth (Legacy):**
29- Send only `X-Identity-PublicKey` header
30- Server looks up existing identity by public key
31- No new identities auto-registered without signature
32
33**Keypair Format:**
34- Public key: 32 bytes → 44 characters base64
35- Secret key: 64 bytes → 88 characters base64
36- Generated client-side using `ANProto.gen()` or `@noble/ed25519`
37
38### Keet Identity
39
40**Protocol:** BIP39 mnemonic → Ed25519 keypair with proof chain attestation.
41
42**Header:**
43| Header | Required | Description |
44|--------|----------|-------------|
45| `X-Identity-Proof` | Yes | JSON-encoded proof chain |
46
47**Dependencies:** Requires `keet-identity-key` package. Pass as `IdentityKey` option to AuthManager.
48
49**Proof Chain Structure:**
50```json
51{
52 "publicKey": "hex-encoded-identity-public-key",
53 "deviceKey": "hex-encoded-device-public-key",
54 "proof": "hex-encoded-attestation-signature"
55}
56```
57
58### Bearer Token
59
60**Protocol:** Random token with SHA256 hash storage.
61
62**Header:**
63| Header | Required | Description |
64|--------|----------|-------------|
65| `Authorization` | Yes | `Bearer <64-char-hex-token>` |
66
67**Token Structure (stored):**
68```javascript
69{
70 hash: "sha256-of-token",
71 username: "admin",
72 description: "API access token",
73 permissions: ["read", "write", "admin"],
74 resources: ["*"],
75 createdAt: timestamp,
76 expiresAt: null, // null = never expires
77 revoked: false
78}
79```
80
81**Security:** Only the SHA256 hash is stored. The raw token is returned once at creation time and cannot be retrieved later.
82
83### SSH Key
84
85**Protocol:** SSH key signature verification (Ed25519 + RSA).
86
87**Headers:**
88| Header | Required | Description |
89|--------|----------|-------------|
90| `X-SSH-PublicKey` | Yes | Full SSH public key string |
91| `X-SSH-Signature` | Yes | Base64-encoded SSH signature |
92| `X-SSH-Challenge` | Yes | Challenge ID |
93
94**Supported Key Types:**
95- `ssh-ed25519` — Verified using hypercore-crypto (works in Bare runtime)
96- `ssh-rsa` — Verified using node:crypto (Node.js only)
97
98## Challenge System
99
100Server-side challenge generation prevents replay attacks.
101
102**Lifecycle:**
1031. Generate: Random 32-byte challenge + unique ID
1042. Store: In Hyperbee + in-memory cache
1053. Verify: Check exists, not expired, not consumed
1064. Consume: Mark as used (single-use)
1075. Cleanup: Remove expired challenges periodically
108
109**Configuration:**
110```javascript
111new ChallengeManager({
112 db: hyperbee,
113 ttl: 5 * 60 * 1000, // Challenge expiry (5 min)
114 cacheTimeout: 30 * 1000 // Memory cache lifetime (30s)
115})
116```
117
118## Rate Limiting
119
120Token bucket algorithm with per-client, per-operation limits.
121
122**Default Limits:**
123| Operation | Tokens | Refill Rate | Refill Interval |
124|-----------|--------|-------------|-----------------|
125| `auth` | 1,000 | 100/refill | 60 seconds |
126| `api` | 10,000 | 1,000/refill | 60 seconds |
127| `identity-generate` | 5 | 1/refill | 1 hour |
128
129**How It Works:**
1301. Each client+operation pair gets a token bucket
1312. Each request costs 1 token
1323. Tokens refill at configured rate
1334. When bucket is empty: `{ allowed: false, retryAfter: seconds }`
134
135**Configuration:**
136```javascript
137new RateLimiter({
138 limits: {
139 auth: { tokens: 1000, refillRate: 100, refillInterval: 60000 },
140 api: { tokens: 10000, refillRate: 1000, refillInterval: 60000 }
141 }
142})
143```
144
145## Permission Model
146
147### Built-in Levels
148
149| Permission | Description |
150|------------|-------------|
151| `read` | Read-only access to resources |
152| `write` | Create, update, delete resources |
153| `admin` | Full access + manage users/permissions |
154
155### Resource Permissions
156
157Users have both global and per-resource permissions:
158
159```javascript
160// Global: applies when no resource-specific match
161globalPermissions: ["read"]
162
163// Per-resource: checked first, overrides global
164resources: [
165 { name: "project-a", permissions: ["read", "write"] },
166 { name: "*", permissions: ["read"] } // Wildcard
167]
168```
169
170### Permission Resolution Order
171
1721. Check `auth.permissions.admin` → true = allow everything
1732. Check `auth.permissions[action]` → false = deny
1743. Check resource match → `resources.includes('*')` or `resources.includes(name)`
175
176### Custom Permission Check
177
178Override with injectable callback:
179
180```javascript
181const auth = new AuthManager({
182 checkPermission (auth, action, resourceName) {
183 // Your custom logic here
184 // Return true to allow, false to deny
185 }
186})
187```
188
189## Security
190
191### Signature Verification
192- Ed25519: Full verification via hypercore-crypto (works in Bare + Node.js)
193- RSA: Verification via node:crypto (Node.js only)
194
195### Replay Prevention
196- Challenges are single-use and time-limited (5 min default)
197- Challenge IDs are unique random values
198- Consumed challenges cannot be reused
199
200### Token Security
201- Bearer tokens stored as SHA256 hashes only
202- Raw token returned once at creation, never retrievable
203- Tokens can be revoked instantly
204
205### Rate Limiting
206- Per-IP token bucket prevents brute force
207- Persistent state survives restarts
208- Configurable per-operation limits
209
210### Identity Revocation
211- Revoked identities immediately fail authentication
212- Stored in both Autobase view and direct-write store
213- Revocation reason tracked for audit
214
215### Trusted Proxies
216- Configurable trusted proxy list for X-Forwarded-For parsing
217- Default: localhost only (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`)
218- Prevents IP spoofing from untrusted sources
docs/IDENTITY.md +198 -0
……@@ -0,0 +1,198 @@
1# Identity System Architecture
2
3## Overview
4
5hyper-identity provides a unified identity model where a single **User** can have multiple **Identities** (authentication methods). All identity data is stored in an Autobase-backed registry for multi-writer consensus.
6
7## Identity Types
8
9| Type | Protocol | Key Material | Auto-Register |
10|------|----------|-------------|---------------|
11| Web Identity | ANProto (Ed25519) | Browser-generated keypair | Yes |
12| Keet Identity | BIP39 mnemonic | HD-derived Ed25519 keypair | Yes |
13| Bearer Token | SHA256 hash | Random 32-byte token | No (admin creates) |
14| SSH Key | RFC 4253 | RSA or Ed25519 SSH key | Yes |
15
16## Data Model
17
18### User
19
20```javascript
21{
22 userId: "a1b2c3d4...", // Random hex (16 bytes)
23 username: "alice", // Optional, unique
24 name: "Alice", // Display name
25 email: null, // Optional
26 profilePicture: null, // Optional URL
27 globalPermissions: ["read"], // Default permissions
28 resources: [ // Per-resource permissions
29 { name: "*", permissions: ["read"] },
30 { name: "my-project", permissions: ["read", "write", "admin"] }
31 ],
32 identities: ["pubkey1", "pubkey2"], // Linked identity IDs
33 createdAt: 1709900000000,
34 updatedAt: 1709900000000
35}
36```
37
38### Identity
39
40```javascript
41{
42 identityId: "GXsPilmS0Ek/grGO...", // Usually the public key
43 method: "web-identity", // Provider name
44 publicKey: "GXsPilmS0Ek/grGO...", // For signature verification
45 userId: "a1b2c3d4...", // Linked user (after linking)
46 createdAt: 1709900000000,
47 revoked: false // Revocation status
48}
49```
50
51## Storage Schema
52
53### Direct-Write Store (Hyperbee)
54
55Identity metadata and reverse lookups are stored in a direct-write Hyperbee for fast lookups:
56
57```
58identity:{identityId} → Identity object
59pubkey:{publicKey} → { identityId } # Reverse lookup
60token:{sha256hash} → Bearer token data
61challenge:{challengeId} → Challenge state
62vault:{name} → Encrypted data
63```
64
65### Autobase View (Multi-Writer)
66
67User records and identity-to-user mappings are stored in the Autobase view for consensus:
68
69```
70user:{userId} → User object
71user-identity:{id} → { userId } # Identity→User mapping
72```
73
74## Authentication Flow
75
76```
77HTTP Request
78 │
79 ├─ Extract credentials (per provider)
80 │ ├─ X-Identity-PublicKey header → Web Identity
81 │ ├─ X-Identity-Proof header → Keet Identity
82 │ ├─ Authorization: Bearer header → Bearer Token
83 │ └─ X-SSH-PublicKey header → SSH Key
84 │
85 ├─ Verify credentials
86 │ ├─ Challenge-response signature check
87 │ ├─ Proof chain verification
88 │ ├─ Token hash lookup
89 │ └─ SSH signature verification
90 │
91 ├─ Look up Identity → User
92 │ ├─ pubkey:{key} → identity:{id} → user-identity:{id} → user:{userId}
93 │ └─ Auto-register if new (web/keet/ssh)
94 │
95 └─ Return auth result
96 ├─ userId, identityId, method
97 ├─ permissions (formatted from user object)
98 └─ verified: true
99```
100
101## Auto-Registration
102
103When a new identity authenticates for the first time:
104
1051. **Register identity** in direct-write store (pubkey index)
1062. **Create user** via Autobase (new userId, default read permissions)
1073. **Link identity to user** in both stores
1084. **Admin bootstrap**: If no admin exists, first user gets admin
109
110## Identity Linking
111
112A user can have multiple identities (e.g., web + keet + SSH):
113
114```javascript
115// User authenticates with web identity
116// Later, authenticates with keet identity
117// Admin links them:
118await registry.linkIdentityToUser(keetIdentityId, existingUserId)
119```
120
121After linking, authenticating with either identity resolves to the same user.
122
123## Account Merge
124
125When duplicate accounts are detected (e.g., same person registered twice):
126
127```javascript
128await registry.mergeAccounts(primaryUserId, secondaryUserId)
129```
130
131This:
1321. Transfers all identities from secondary to primary
1332. Merges resource permissions (union)
1343. Merges global permissions (union)
1354. Marks secondary user with `mergedInto: primaryUserId`
136
137## Revocation
138
139Identities can be revoked (e.g., compromised device):
140
141```javascript
142await registry.revokeIdentity(identityId, { reason: 'Device stolen' })
143// Later:
144await registry.restoreIdentity(identityId)
145```
146
147Revoked identities fail authentication immediately.
148
149## Federation
150
151Multiple servers share the identity registry via Autobase:
152
153```
154Server A (writer) ──┐
155 ├──→ Autobase ──→ Shared Hyperbee View
156Server B (writer) ──┘
157
158Both servers:
159- Accept identity operations independently
160- Autobase linearizes all writes deterministically
161- Both see identical state after sync
162```
163
164### Adding a Writer
165
166```javascript
167// Server A creates registry
168const registryA = new IdentityRegistry(storeA, { db: dbA })
169await registryA.ready()
170
171// Share registryA.base.key with Server B (out-of-band)
172
173// Server B joins
174const registryB = new IdentityRegistry(storeB, { db: dbB })
175await registryB.ready()
176
177// Add each other as writers
178await registryA.addWriter(registryB.base.local.key)
179await registryB.addWriter(registryA.base.local.key)
180```
181
182### Replication
183
184Autobase cores replicate automatically when corestores are connected via Hyperswarm. No additional wiring needed beyond `store.replicate(conn)`.
185
186## Proof Chains (Keet Identity)
187
188BIP39 mnemonic → identity keypair → device attestation:
189
190```
191Keet Identity (BIP39-derived Ed25519)
192 │
193 └─ SIGNS → Device Key (server's Ed25519 keypair)
194 │
195 └─ Proof chain stored with identity
196```
197
198The proof chain proves a device key belongs to a mnemonic holder without exposing the mnemonic itself. Verification uses `IdentityKey.verify(proof)` from `keet-identity-key`.