OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-identity commits / 148a177

Initial hyper-identity library — generic P2P identity management
Extracted from pear-git's battle-tested identity backend, genericized with injectable permission model (resources instead of repos/notebooks). Components: - AuthManager with multi-method auth (ANProto, BIP39, bearer, SSH) - Autobase-backed IdentityRegistry with user/identity CRUD - ANProto Ed25519 challenge-response protocol - IdentityManager for persistent keypairs + mnemonic proof chains - DeviceLinker for blind-pairing device linking - SecureStorage with Argon2id + XSalsa20-Poly1305 encryption - ChallengeManager + RateLimiter auth helpers - 4 auth providers (web-identity, keet-identity, bearer-token, ssh-key) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148a177cb08fa640996f4ac0ed079758dde304ce
by plankatron <fleeky@gmail.com> · 2026-03-10T21:08:58.000Z

Changed files (18) +2289 -0

index.js +37 -0
……@@ -0,0 +1,37 @@
1/**
2 * hyper-identity — Generic P2P identity management
3 *
4 * Provides multi-method authentication (ANProto, BIP39 mnemonic, bearer tokens, SSH keys),
5 * Autobase-backed identity registry, device linking, and encrypted storage.
6 *
7 * Extracted from pear-git's battle-tested identity backend, genericized
8 * with injectable permission model (resources instead of repos/notebooks).
9 */
10
11// Core authentication
12export { AuthManager, Permissions, sendAuthError } from './lib/auth.js'
13
14// ANProto protocol
15export { ANProto } from './lib/anproto.js'
16
17// Identity management
18export { IdentityManager } from './lib/identity-manager.js'
19export { DeviceLinker } from './lib/device-linker.js'
20export { SecureStorage } from './lib/secure-storage.js'
21
22// Autobase identity registry
23export { IdentityRegistry } from './lib/auth/identity-registry.js'
24export { createApplyFunction } from './lib/auth/identity-apply.js'
25export { OpTypes, encodeOp, decodeOp, validateOp } from './lib/auth/identity-operations.js'
26
27// Auth providers
28export { BaseProvider } from './lib/auth/base-provider.js'
29export { WebIdentityProvider } from './lib/auth/providers/web-identity.js'
30export { KeetIdentityProvider } from './lib/auth/providers/keet-identity.js'
31export { BearerTokenProvider } from './lib/auth/providers/bearer-token.js'
32export { SSHKeyProvider } from './lib/auth/providers/ssh-key.js'
33
34// Auth helpers
35export { ChallengeManager } from './lib/auth/challenge-manager.js'
36export { RateLimiter } from './lib/auth/rate-limiter.js'
37export { SSHKeyParser } from './lib/auth/ssh-parser.js'
lib/anproto.js +67 -0
……@@ -0,0 +1,67 @@
1import crypto from 'hypercore-crypto'
2import b4a from 'b4a'
3
4/**
5 * ANProto — Ed25519 challenge-response authentication protocol
6 *
7 * Used for browser-based identity verification.
8 * Generates keypairs, signs challenges, verifies signatures.
9 */
10export class ANProto {
11 /**
12 * Generate a new Ed25519 keypair
13 * @returns {{ publicKey: Buffer, secretKey: Buffer }}
14 */
15 static gen () {
16 return crypto.keyPair()
17 }
18
19 /**
20 * Load keypair from hex-encoded keys
21 * @param {{ publicKey: string, secretKey: string }} keys - Hex-encoded keys
22 * @returns {{ publicKey: Buffer, secretKey: Buffer }}
23 */
24 static loadKeypair (keys) {
25 return {
26 publicKey: b4a.from(keys.publicKey, 'hex'),
27 secretKey: b4a.from(keys.secretKey, 'hex')
28 }
29 }
30
31 /**
32 * Sign a challenge with secret key
33 * @param {Buffer|string} challenge - Challenge to sign
34 * @param {Buffer} secretKey - Secret key
35 * @returns {string} Base64-encoded signature
36 */
37 static sign (challenge, secretKey) {
38 const msg = typeof challenge === 'string' ? b4a.from(challenge) : challenge
39 const sig = crypto.sign(msg, secretKey)
40 return b4a.toString(sig, 'base64')
41 }
42
43 /**
44 * Verify a signature against a public key
45 * @param {Buffer|string} challenge - Original challenge
46 * @param {string} signature - Base64-encoded signature
47 * @param {Buffer|string} publicKey - Public key (Buffer or base64)
48 * @returns {boolean}
49 */
50 static verify (challenge, signature, publicKey) {
51 const msg = typeof challenge === 'string' ? b4a.from(challenge) : challenge
52 const sig = b4a.from(signature, 'base64')
53 const pk = typeof publicKey === 'string' ? b4a.from(publicKey, 'base64') : publicKey
54 return crypto.verify(msg, sig, pk)
55 }
56
57 /**
58 * Get hex-encoded public key
59 * @param {Buffer} publicKey - Public key buffer
60 * @returns {string}
61 */
62 static getPublicKey (publicKey) {
63 return b4a.toString(publicKey, 'hex')
64 }
65}
66
67export default ANProto
lib/auth.js +383 -0
……@@ -0,0 +1,383 @@
1import { IdentityRegistry } from './auth/identity-registry.js'
2import { WebIdentityProvider } from './auth/providers/web-identity.js'
3import { BearerTokenProvider } from './auth/providers/bearer-token.js'
4import { KeetIdentityProvider } from './auth/providers/keet-identity.js'
5import { SSHKeyProvider } from './auth/providers/ssh-key.js'
6import { ChallengeManager } from './auth/challenge-manager.js'
7import { RateLimiter } from './auth/rate-limiter.js'
8
9/**
10 * AuthManager — Generic multi-method authentication and authorization
11 *
12 * Supports:
13 * - Web Identity (ANProto Ed25519 challenge-response)
14 * - Keet Identity (BIP39 mnemonic proof chains)
15 * - Bearer Tokens (API tokens with embedded permissions)
16 * - SSH Keys (Ed25519 + RSA signature verification)
17 *
18 * Permission model is injectable via checkPermission callback.
19 * Uses 'resources' as the generic term (not repos, notebooks, etc).
20 */
21export class AuthManager {
22 constructor (options = {}) {
23 this.db = options.db || null // Direct-write Hyperbee for identity/token storage
24 this.trustedProxies = options.trustedProxies || ['127.0.0.1', '::1', '::ffff:127.0.0.1']
25
26 // Injectable permission check: (auth, action, resourceName) => boolean
27 // Default: check auth.permissions object
28 this.checkPermission = options.checkPermission || null
29
30 // Identity registry (Autobase-backed)
31 this.identityRegistry = options.identityRegistry || null
32
33 // Challenge manager
34 this.challengeManager = new ChallengeManager({
35 db: this.db,
36 ttl: options.challengeTTL || 5 * 60 * 1000,
37 cacheTimeout: options.challengeCacheTimeout || 30 * 1000
38 })
39
40 // Rate limiter
41 this.rateLimiter = new RateLimiter({
42 db: this.db,
43 limits: options.rateLimits || {
44 auth: { tokens: 1000, refillRate: 100, refillInterval: 60 * 1000 },
45 api: { tokens: 10000, refillRate: 1000, refillInterval: 60 * 1000 },
46 'identity-generate': { tokens: 5, refillRate: 1, refillInterval: 60 * 60 * 1000 }
47 },
48 cacheTimeout: options.rateLimitCacheTimeout || 10 * 1000
49 })
50
51 // Auth providers
52 this.providers = []
53
54 // Register default providers based on what's available
55 if (this.identityRegistry) {
56 this.providers.push(
57 new WebIdentityProvider(this.identityRegistry, this.challengeManager)
58 )
59
60 if (options.IdentityKey) {
61 this.providers.push(
62 new KeetIdentityProvider(this.identityRegistry, { IdentityKey: options.IdentityKey })
63 )
64 }
65
66 this.providers.push(
67 new BearerTokenProvider(this.identityRegistry, { db: this.db })
68 )
69
70 if (options.enableSSH !== false) {
71 this.providers.push(
72 new SSHKeyProvider(this.identityRegistry, this.challengeManager)
73 )
74 }
75 }
76
77 // Get bearer token provider reference
78 this.bearerProvider = this.providers.find(p => p.name === 'bearer-token') || null
79 }
80
81 /**
82 * Add a custom auth provider
83 * @param {BaseProvider} provider
84 */
85 addProvider (provider) {
86 this.providers.push(provider)
87 }
88
89 /**
90 * Authenticate request using all available providers
91 * @param {Object} req - HTTP request
92 * @param {Object} options - { skipRateLimit, operation }
93 * @returns {Promise<Object|null>} Auth result or null
94 */
95 async authenticate (req, options = {}) {
96 // Rate limiting
97 if (!options.skipRateLimit) {
98 const clientIp = this.getClientIp(req)
99 const operation = options.operation || 'auth'
100 const rateLimit = await this.rateLimiter.check(clientIp, operation)
101
102 if (!rateLimit.allowed) {
103 const error = new Error('Rate limit exceeded')
104 error.code = 'RATE_LIMIT'
105 error.retryAfter = rateLimit.retryAfter
106 error.resetAt = rateLimit.resetAt
107 throw error
108 }
109 }
110
111 // Try each provider
112 for (const provider of this.providers) {
113 const credentials = await provider.extractCredentials(req)
114 if (!credentials) continue
115
116 try {
117 const authResult = await provider.authenticate(credentials)
118
119 // Bearer tokens have permissions embedded
120 if (authResult.method === 'bearer-token') {
121 return {
122 userId: null,
123 identityId: authResult.identityId,
124 method: authResult.method,
125 username: authResult.credentials.username,
126 permissions: this.formatTokenPermissions(authResult),
127 verified: true
128 }
129 }
130
131 // For other methods, look up user via identity
132 let user = await this.identityRegistry.getUserByIdentity(authResult.identityId)
133
134 if (!user && authResult.method === 'web-identity') {
135 // Retry — web identity auto-registers
136 user = await this.identityRegistry.getUserByIdentity(authResult.identityId)
137 }
138
139 if (!user) continue
140
141 // Auto-promote to admin if no admin exists (bootstrap)
142 const hasAdmin = await this.identityRegistry._hasAnyAdmin()
143 if (!hasAdmin && user.globalPermissions && !user.globalPermissions.includes('admin')) {
144 await this.identityRegistry.upgradePermissions(user.userId, '*', ['read', 'write', 'admin'])
145 const upgraded = await this.identityRegistry.getUser(user.userId)
146 if (upgraded) user = upgraded
147 }
148
149 return {
150 userId: user.userId,
151 identityId: authResult.identityId,
152 method: authResult.method,
153 permissions: this.formatUserPermissions(user, null),
154 user,
155 verified: true
156 }
157 } catch {
158 continue
159 }
160 }
161
162 return null
163 }
164
165 /**
166 * Format user permissions
167 * Uses user.resources (generic) instead of application-specific terms
168 * @param {Object} user - User object
169 * @param {string} resourceName - Resource name (or null for global)
170 * @returns {Object} { read, write, admin, resources }
171 */
172 formatUserPermissions (user, resourceName) {
173 // Check resource-specific permissions
174 const resourcePerms = (user.resources || []).find(
175 r => r.name === resourceName || r.name === '*'
176 )
177
178 if (resourcePerms) {
179 return {
180 read: resourcePerms.permissions.includes('read') || resourcePerms.permissions.includes('admin'),
181 write: resourcePerms.permissions.includes('write') || resourcePerms.permissions.includes('admin'),
182 admin: resourcePerms.permissions.includes('admin'),
183 resources: [resourcePerms.name]
184 }
185 }
186
187 // Fall back to global permissions
188 return {
189 read: user.globalPermissions.includes('read') || user.globalPermissions.includes('admin'),
190 write: user.globalPermissions.includes('write') || user.globalPermissions.includes('admin'),
191 admin: user.globalPermissions.includes('admin'),
192 resources: ['*']
193 }
194 }
195
196 /**
197 * Format bearer token permissions
198 */
199 formatTokenPermissions (authResult) {
200 return {
201 read: authResult.permissions.includes('read') || authResult.permissions.includes('admin'),
202 write: authResult.permissions.includes('write') || authResult.permissions.includes('admin'),
203 admin: authResult.permissions.includes('admin'),
204 resources: authResult.resources || ['*']
205 }
206 }
207
208 /**
209 * Check if auth has permission for action on resource
210 * Uses injectable checkPermission callback if provided.
211 * @param {Object} auth - Auth result from authenticate()
212 * @param {string} resourceName - Resource name
213 * @param {string} action - 'read', 'write', or 'admin'
214 * @returns {boolean}
215 */
216 hasResourceAccess (auth, resourceName, action = 'read') {
217 if (!auth || !auth.verified) return false
218
219 // Use custom permission check if provided
220 if (this.checkPermission) {
221 return this.checkPermission(auth, action, resourceName)
222 }
223
224 // Default permission check
225 const perms = auth.permissions
226 if (perms.admin) return true
227 if (!perms[action]) return false
228
229 if (resourceName) {
230 return perms.resources.includes('*') || perms.resources.includes(resourceName)
231 }
232 return true
233 }
234
235 /**
236 * Middleware: authenticate and authorize request
237 * @param {Object} req - HTTP request
238 * @param {string} action - Required action
239 * @param {string} resourceName - Resource name
240 * @returns {Promise<Object>} { authorized, auth, reason }
241 */
242 async authorizeRequest (req, action, resourceName = null) {
243 const auth = await this.authenticate(req)
244 if (!auth) {
245 return { authorized: false, auth: null, reason: 'Not authenticated' }
246 }
247 if (!this.hasResourceAccess(auth, resourceName, action)) {
248 return { authorized: false, auth, reason: 'Insufficient permissions' }
249 }
250 return { authorized: true, auth, reason: null }
251 }
252
253 // ==================== TOKEN MANAGEMENT ====================
254
255 async createToken (options) {
256 if (!this.bearerProvider) throw new Error('Bearer token provider not configured')
257 return this.bearerProvider.createToken(options)
258 }
259
260 async validateToken (token) {
261 if (!this.bearerProvider) return null
262 return this.bearerProvider.validateToken(token)
263 }
264
265 async revokeToken (tokenHash) {
266 if (!this.bearerProvider) return
267 return this.bearerProvider.revokeToken(tokenHash)
268 }
269
270 async listAllTokens () {
271 if (!this.bearerProvider) return []
272 return this.bearerProvider.listAllTokens()
273 }
274
275 async getUserTokens (username) {
276 if (!this.bearerProvider) return []
277 return this.bearerProvider.getUserTokens(username)
278 }
279
280 async createDefaultAdmin () {
281 const tokens = await this.listAllTokens()
282 if (tokens.length > 0) return null
283 return this.createToken({
284 username: 'admin',
285 description: 'Default admin token (generated on first start)',
286 permissions: ['read', 'write', 'admin'],
287 resources: ['*']
288 })
289 }
290
291 async getStatus () {
292 const tokens = await this.listAllTokens()
293 const activeTokens = tokens.filter(t => !t.expiresAt || t.expiresAt > Date.now())
294 return {
295 enabled: tokens.length > 0,
296 totalTokens: tokens.length,
297 activeTokens: activeTokens.length,
298 users: [...new Set(tokens.map(t => t.username))].length
299 }
300 }
301
302 // ==================== IDENTITY METHODS ====================
303
304 async getUserWithIdentities (identityId) {
305 const user = await this.identityRegistry.getUserByIdentity(identityId)
306 if (!user) return null
307 const identities = await this.identityRegistry.listUserIdentities(user.userId)
308 return { user, identities }
309 }
310
311 async upgradeUserPermissions (userId, resourceName, permissions) {
312 return this.identityRegistry.upgradePermissions(userId, resourceName, permissions)
313 }
314
315 async generateChallenge (options = {}) {
316 return await this.challengeManager.generate(options)
317 }
318
319 async verifyChallenge (challengeId, challenge) {
320 return await this.challengeManager.verify(challengeId, challenge)
321 }
322
323 async revokeIdentity (identityId, options = {}) {
324 return this.identityRegistry.revokeIdentity(identityId, options)
325 }
326
327 async restoreIdentity (identityId) {
328 return this.identityRegistry.restoreIdentity(identityId)
329 }
330
331 async listRevokedIdentities () {
332 return this.identityRegistry.listRevokedIdentities()
333 }
334
335 getClientIp (req) {
336 const socketIp = req.socket?.remoteAddress || 'unknown'
337 const isTrustedProxy = this.trustedProxies.some(
338 ip => socketIp === ip || socketIp === '::ffff:' + ip
339 )
340 if (isTrustedProxy) {
341 const forwarded = req.headers['x-forwarded-for']
342 if (forwarded) return forwarded.split(',')[0].trim()
343 const realIp = req.headers['x-real-ip']
344 if (realIp) return realIp
345 }
346 return socketIp
347 }
348
349 async getRateLimitStats () {
350 return await this.rateLimiter.stats()
351 }
352
353 async getChallengeStats () {
354 return await this.challengeManager.stats()
355 }
356
357 destroy () {
358 this.challengeManager.destroy()
359 this.rateLimiter.destroy()
360 }
361}
362
363/**
364 * Permission constants
365 */
366export const Permissions = {
367 READ: 'read',
368 WRITE: 'write',
369 ADMIN: 'admin'
370}
371
372/**
373 * Send auth error response
374 */
375export function sendAuthError (res, message = 'Unauthorized', statusCode = 401) {
376 res.statusCode = statusCode
377 res.setHeader('WWW-Authenticate', 'Bearer realm="hyper-identity"')
378 res.setHeader('Content-Type', 'application/json')
379 res.end(JSON.stringify({
380 error: message,
381 hint: 'Use Authorization: Bearer <token> or X-Identity-PublicKey header'
382 }))
383}
lib/auth/base-provider.js +33 -0
……@@ -0,0 +1,33 @@
1/**
2 * BaseProvider — Abstract authentication provider
3 *
4 * All auth providers must implement:
5 * - name: string identifier
6 * - extractCredentials(req): Extract credentials from HTTP request
7 * - authenticate(credentials): Verify credentials, return auth result
8 */
9export class BaseProvider {
10 constructor (name) {
11 this.name = name
12 }
13
14 /**
15 * Extract credentials from HTTP request
16 * @param {Object} req - HTTP request
17 * @returns {Promise<Object|null>} Credentials or null if not applicable
18 */
19 async extractCredentials (req) {
20 throw new Error('Not implemented')
21 }
22
23 /**
24 * Authenticate with extracted credentials
25 * @param {Object} credentials - Extracted credentials
26 * @returns {Promise<Object>} Auth result { identityId, method, credentials, permissions? }
27 */
28 async authenticate (credentials) {
29 throw new Error('Not implemented')
30 }
31}
32
33export default BaseProvider
lib/auth/challenge-manager.js +106 -0
……@@ -0,0 +1,106 @@
1import crypto from 'hypercore-crypto'
2import b4a from 'b4a'
3
4/**
5 * ChallengeManager — Challenge-response authentication
6 *
7 * Generates time-limited challenges for signature-based auth.
8 * Supports persistent storage and in-memory caching.
9 */
10export class ChallengeManager {
11 constructor (opts = {}) {
12 this.db = opts.db || null
13 this.ttl = opts.ttl || 5 * 60 * 1000 // 5 minutes default
14 this.cacheTimeout = opts.cacheTimeout || 30 * 1000 // 30 seconds
15 this._cache = new Map()
16 this._cleanupInterval = setInterval(() => this._cleanup(), this.ttl)
17 }
18
19 /**
20 * Generate a new challenge
21 * @param {Object} opts - { publicKey?, metadata? }
22 * @returns {Promise<Object>} { challengeId, challenge, expiresAt }
23 */
24 async generate (opts = {}) {
25 const challengeId = b4a.toString(crypto.randomBytes(16), 'hex')
26 const challenge = b4a.toString(crypto.randomBytes(32), 'hex')
27 const expiresAt = Date.now() + this.ttl
28
29 const entry = {
30 challengeId,
31 challenge,
32 expiresAt,
33 publicKey: opts.publicKey || null,
34 metadata: opts.metadata || null,
35 consumed: false
36 }
37
38 // Store persistently
39 if (this.db) {
40 await this.db.put(`challenge:${challengeId}`, entry)
41 }
42
43 // Cache in memory
44 this._cache.set(challengeId, entry)
45
46 return { challengeId, challenge, expiresAt }
47 }
48
49 /**
50 * Verify and consume a challenge
51 * @param {string} challengeId - Challenge ID
52 * @param {string} challenge - Challenge value to verify
53 * @returns {Promise<Object|null>} Challenge entry or null
54 */
55 async verify (challengeId, challenge) {
56 let entry = this._cache.get(challengeId)
57
58 if (!entry && this.db) {
59 const stored = await this.db.get(`challenge:${challengeId}`)
60 if (stored) entry = stored.value
61 }
62
63 if (!entry) return null
64 if (entry.consumed) return null
65 if (entry.expiresAt < Date.now()) return null
66 if (entry.challenge !== challenge) return null
67
68 // Mark consumed
69 entry.consumed = true
70 this._cache.set(challengeId, entry)
71 if (this.db) {
72 await this.db.put(`challenge:${challengeId}`, entry)
73 }
74
75 return entry
76 }
77
78 /**
79 * Cleanup expired challenges
80 */
81 async _cleanup () {
82 const now = Date.now()
83 for (const [id, entry] of this._cache) {
84 if (entry.expiresAt < now || entry.consumed) {
85 this._cache.delete(id)
86 }
87 }
88 }
89
90 /**
91 * Get stats
92 */
93 async stats () {
94 return {
95 cached: this._cache.size,
96 active: [...this._cache.values()].filter(e => !e.consumed && e.expiresAt > Date.now()).length
97 }
98 }
99
100 destroy () {
101 clearInterval(this._cleanupInterval)
102 this._cache.clear()
103 }
104}
105
106export default ChallengeManager
lib/auth/identity-apply.js +236 -0
……@@ -0,0 +1,236 @@
1import { decodeOp, OpTypes, validateOp } from './identity-operations.js'
2
3/**
4 * Create the Autobase apply function for identity registry
5 *
6 * This is a deterministic function that processes identity operations
7 * into a Hyperbee view. Each operation type modifies the view state.
8 *
9 * @param {Object} opts - Options
10 * @param {string} opts.prefix - Key prefix for namespacing (default: '')
11 * @returns {Function} Autobase apply function
12 */
13export function createApplyFunction (opts = {}) {
14 const prefix = opts.prefix || ''
15
16 return async function apply (batch, view, base) {
17 const b = view.batch({ update: false })
18
19 try {
20 for (const node of batch) {
21 try {
22 const op = decodeOp(node.value)
23 const validation = validateOp(op)
24 if (!validation.valid) {
25 console.error(`[IdentityApply] Invalid op: ${validation.error}`)
26 continue
27 }
28
29 switch (op.type) {
30 case OpTypes.CREATE_USER: {
31 const key = `${prefix}user:${op.userId}`
32 const existing = await b.get(key)
33 if (existing) continue // Don't overwrite
34
35 const user = {
36 userId: op.userId,
37 username: op.username || null,
38 name: op.name || 'Anonymous',
39 email: op.email || null,
40 profilePicture: op.profilePicture || null,
41 globalPermissions: op.globalPermissions || ['read'],
42 resources: op.resources || [{ name: '*', permissions: ['read'] }],
43 identities: op.identities || [],
44 createdAt: op.createdAt || Date.now(),
45 updatedAt: op.updatedAt || Date.now()
46 }
47 await b.put(key, user)
48
49 // Index identities
50 for (const id of user.identities) {
51 await b.put(`${prefix}user-identity:${id}`, { userId: op.userId })
52 }
53 break
54 }
55
56 case OpTypes.UPDATE_USER: {
57 const key = `${prefix}user:${op.userId}`
58 const existing = await b.get(key)
59 if (!existing) continue
60
61 const user = existing.value
62 if (op.username !== undefined) user.username = op.username
63 if (op.name !== undefined) user.name = op.name
64 if (op.email !== undefined) user.email = op.email
65 if (op.profilePicture !== undefined) user.profilePicture = op.profilePicture
66 if (op.globalPermissions !== undefined) user.globalPermissions = op.globalPermissions
67 user.updatedAt = Date.now()
68 await b.put(key, user)
69 break
70 }
71
72 case OpTypes.DELETE_USER: {
73 const key = `${prefix}user:${op.userId}`
74 const existing = await b.get(key)
75 if (!existing) continue
76
77 // Remove identity index entries
78 for (const id of (existing.value.identities || [])) {
79 await b.del(`${prefix}user-identity:${id}`)
80 }
81 await b.del(key)
82 break
83 }
84
85 case OpTypes.ADD_IDENTITY: {
86 const key = `${prefix}user:${op.userId}`
87 const existing = await b.get(key)
88 if (!existing) continue
89
90 const user = existing.value
91 if (!user.identities.includes(op.identityId)) {
92 user.identities.push(op.identityId)
93 user.updatedAt = Date.now()
94 await b.put(key, user)
95 await b.put(`${prefix}user-identity:${op.identityId}`, { userId: op.userId })
96 }
97 break
98 }
99
100 case OpTypes.REMOVE_IDENTITY: {
101 const key = `${prefix}user:${op.userId}`
102 const existing = await b.get(key)
103 if (!existing) continue
104
105 const user = existing.value
106 user.identities = user.identities.filter(id => id !== op.identityId)
107 user.updatedAt = Date.now()
108 await b.put(key, user)
109 await b.del(`${prefix}user-identity:${op.identityId}`)
110 break
111 }
112
113 case OpTypes.LINK_IDENTITY: {
114 await b.put(`${prefix}user-identity:${op.identityId}`, { userId: op.userId })
115 const key = `${prefix}user:${op.userId}`
116 const existing = await b.get(key)
117 if (existing) {
118 const user = existing.value
119 if (!user.identities.includes(op.identityId)) {
120 user.identities.push(op.identityId)
121 user.updatedAt = Date.now()
122 await b.put(key, user)
123 }
124 }
125 break
126 }
127
128 case OpTypes.UPGRADE_PERMISSIONS: {
129 const key = `${prefix}user:${op.userId}`
130 const existing = await b.get(key)
131 if (!existing) continue
132
133 const user = existing.value
134 const resourceName = op.resourceName
135
136 if (resourceName === '*') {
137 // Global permissions
138 user.globalPermissions = op.permissions
139 } else {
140 // Resource-specific permissions
141 if (!user.resources) user.resources = []
142 const idx = user.resources.findIndex(r => r.name === resourceName)
143 if (idx >= 0) {
144 user.resources[idx].permissions = op.permissions
145 } else {
146 user.resources.push({ name: resourceName, permissions: op.permissions })
147 }
148 }
149 user.updatedAt = Date.now()
150 await b.put(key, user)
151 break
152 }
153
154 case OpTypes.MERGE_ACCOUNTS: {
155 const primaryKey = `${prefix}user:${op.primaryUserId}`
156 const secondaryKey = `${prefix}user:${op.secondaryUserId}`
157
158 const primary = await b.get(primaryKey)
159 const secondary = await b.get(secondaryKey)
160
161 if (!primary || !secondary) continue
162
163 const pUser = primary.value
164 const sUser = secondary.value
165
166 // Merge identities
167 for (const id of sUser.identities) {
168 if (!pUser.identities.includes(id)) {
169 pUser.identities.push(id)
170 }
171 await b.put(`${prefix}user-identity:${id}`, { userId: op.primaryUserId })
172 }
173
174 // Merge resources
175 for (const res of (sUser.resources || [])) {
176 const idx = (pUser.resources || []).findIndex(r => r.name === res.name)
177 if (idx < 0) {
178 if (!pUser.resources) pUser.resources = []
179 pUser.resources.push(res)
180 }
181 }
182
183 // Merge global permissions
184 const allPerms = new Set([...pUser.globalPermissions, ...sUser.globalPermissions])
185 pUser.globalPermissions = [...allPerms]
186
187 pUser.updatedAt = Date.now()
188 await b.put(primaryKey, pUser)
189
190 // Mark secondary as merged
191 sUser.mergedInto = op.primaryUserId
192 sUser.updatedAt = Date.now()
193 await b.put(secondaryKey, sUser)
194 break
195 }
196
197 case OpTypes.REVOKE_IDENTITY: {
198 const idKey = `${prefix}identity:${op.identityId}`
199 const existing = await b.get(idKey)
200 if (existing) {
201 const identity = existing.value
202 identity.revoked = true
203 identity.revokedAt = op.revokedAt || Date.now()
204 identity.revokedReason = op.reason || null
205 await b.put(idKey, identity)
206 }
207 break
208 }
209
210 case OpTypes.RESTORE_IDENTITY: {
211 const idKey = `${prefix}identity:${op.identityId}`
212 const existing = await b.get(idKey)
213 if (existing) {
214 const identity = existing.value
215 delete identity.revoked
216 delete identity.revokedAt
217 delete identity.revokedReason
218 await b.put(idKey, identity)
219 }
220 break
221 }
222 }
223 } catch (err) {
224 console.error('[IdentityApply] Skipping malformed op:', err.message)
225 continue
226 }
227 }
228
229 await b.flush()
230 } catch (err) {
231 console.error('[IdentityApply] Batch error:', err.message)
232 await b.close()
233 throw err
234 }
235 }
236}
lib/auth/identity-operations.js +89 -0
……@@ -0,0 +1,89 @@
1import b4a from 'b4a'
2
3/**
4 * Identity operation types for Autobase
5 */
6export const OpTypes = {
7 CREATE_USER: 'create-user',
8 UPDATE_USER: 'update-user',
9 DELETE_USER: 'delete-user',
10 ADD_IDENTITY: 'add-identity',
11 REMOVE_IDENTITY: 'remove-identity',
12 LINK_IDENTITY: 'link-identity',
13 UPGRADE_PERMISSIONS: 'upgrade-permissions',
14 MERGE_ACCOUNTS: 'merge-accounts',
15 REVOKE_IDENTITY: 'revoke-identity',
16 RESTORE_IDENTITY: 'restore-identity'
17}
18
19/**
20 * Encode operation for Autobase
21 * @param {Object} op - Operation object
22 * @returns {Buffer}
23 */
24export function encodeOp (op) {
25 return b4a.from(JSON.stringify(op))
26}
27
28/**
29 * Decode operation from Autobase
30 * @param {Buffer} buf - Encoded operation
31 * @returns {Object}
32 */
33export function decodeOp (buf) {
34 return JSON.parse(b4a.toString(buf))
35}
36
37/**
38 * Validate operation structure
39 * @param {Object} op - Operation to validate
40 * @returns {{ valid: boolean, error?: string }}
41 */
42export function validateOp (op) {
43 if (!op || !op.type) return { valid: false, error: 'Missing op type' }
44
45 switch (op.type) {
46 case OpTypes.CREATE_USER:
47 if (!op.userId) return { valid: false, error: 'Missing userId' }
48 if (!op.name) return { valid: false, error: 'Missing name' }
49 return { valid: true }
50
51 case OpTypes.UPDATE_USER:
52 if (!op.userId) return { valid: false, error: 'Missing userId' }
53 return { valid: true }
54
55 case OpTypes.DELETE_USER:
56 if (!op.userId) return { valid: false, error: 'Missing userId' }
57 return { valid: true }
58
59 case OpTypes.ADD_IDENTITY:
60 case OpTypes.REMOVE_IDENTITY:
61 if (!op.userId) return { valid: false, error: 'Missing userId' }
62 if (!op.identityId) return { valid: false, error: 'Missing identityId' }
63 return { valid: true }
64
65 case OpTypes.LINK_IDENTITY:
66 if (!op.identityId) return { valid: false, error: 'Missing identityId' }
67 if (!op.userId) return { valid: false, error: 'Missing userId' }
68 return { valid: true }
69
70 case OpTypes.UPGRADE_PERMISSIONS:
71 if (!op.userId) return { valid: false, error: 'Missing userId' }
72 if (!op.resourceName) return { valid: false, error: 'Missing resourceName' }
73 if (!Array.isArray(op.permissions)) return { valid: false, error: 'permissions must be array' }
74 return { valid: true }
75
76 case OpTypes.MERGE_ACCOUNTS:
77 if (!op.primaryUserId) return { valid: false, error: 'Missing primaryUserId' }
78 if (!op.secondaryUserId) return { valid: false, error: 'Missing secondaryUserId' }
79 return { valid: true }
80
81 case OpTypes.REVOKE_IDENTITY:
82 case OpTypes.RESTORE_IDENTITY:
83 if (!op.identityId) return { valid: false, error: 'Missing identityId' }
84 return { valid: true }
85
86 default:
87 return { valid: false, error: `Unknown op type: ${op.type}` }
88 }
89}
lib/auth/identity-registry.js +370 -0
……@@ -0,0 +1,370 @@
1import Autobase from 'autobase'
2import Hyperbee from 'hyperbee'
3import crypto from 'hypercore-crypto'
4import b4a from 'b4a'
5import { createApplyFunction } from './identity-apply.js'
6import { encodeOp, OpTypes } from './identity-operations.js'
7
8/**
9 * IdentityRegistry — Autobase-backed user and identity management
10 *
11 * Provides CRUD operations for users, identities, and permissions
12 * using Autobase for multi-writer consensus.
13 *
14 * Generic: uses 'resources' instead of application-specific terms.
15 */
16export class IdentityRegistry {
17 constructor (store, opts = {}) {
18 this._store = store
19 this._prefix = opts.prefix || ''
20 this._base = null
21 this._view = null
22 this._ready = false
23
24 // Separate Hyperbee for direct key lookups (identity records, pubkey index)
25 // This is NOT the Autobase view — it's a direct-write store for identity metadata
26 this.db = opts.db || null
27 }
28
29 /**
30 * Initialize Autobase with Hyperbee view
31 */
32 async ready () {
33 if (this._ready) return
34
35 const apply = createApplyFunction({ prefix: this._prefix })
36
37 this._base = new Autobase(this._store, null, {
38 apply,
39 open (store) {
40 return new Hyperbee(store.get('identity-view'), {
41 extension: false,
42 keyEncoding: 'utf-8',
43 valueEncoding: 'json'
44 })
45 },
46 valueEncoding: 'binary'
47 })
48
49 await this._base.ready()
50 this._view = this._base.view
51 this._ready = true
52 }
53
54 /**
55 * Get Autobase instance (for adding writers, etc.)
56 */
57 get base () { return this._base }
58
59 /**
60 * Get Hyperbee view
61 */
62 get view () { return this._view }
63
64 // ==================== USER OPERATIONS ====================
65
66 /**
67 * Create a new user
68 */
69 async createUser (opts = {}) {
70 const userId = opts.userId || crypto.randomBytes(16).toString('hex')
71 const op = {
72 type: OpTypes.CREATE_USER,
73 userId,
74 username: opts.username || null,
75 name: opts.name || 'Anonymous',
76 email: opts.email || null,
77 profilePicture: opts.profilePicture || null,
78 globalPermissions: opts.globalPermissions || ['read'],
79 resources: opts.resources || [{ name: '*', permissions: ['read'] }],
80 identities: opts.identities || [],
81 createdAt: Date.now()
82 }
83 await this._base.append(encodeOp(op))
84 await this._base.update()
85 return op
86 }
87
88 /**
89 * Get user by userId
90 */
91 async getUser (userId) {
92 const entry = await this._view.get(`${this._prefix}user:${userId}`)
93 return entry ? entry.value : null
94 }
95
96 /**
97 * Get user by identity ID
98 */
99 async getUserByIdentity (identityId) {
100 const link = await this._view.get(`${this._prefix}user-identity:${identityId}`)
101 if (!link) return null
102 return this.getUser(link.value.userId)
103 }
104
105 /**
106 * Get user by username
107 */
108 async getUserByUsername (username) {
109 if (!username) return null
110 for await (const entry of this._view.createReadStream({
111 gte: `${this._prefix}user:`,
112 lt: `${this._prefix}user:\xff`
113 })) {
114 if (entry.value.username === username) return entry.value
115 }
116 return null
117 }
118
119 /**
120 * Update user
121 */
122 async updateUser (userId, updates) {
123 const op = {
124 type: OpTypes.UPDATE_USER,
125 userId,
126 ...updates
127 }
128 await this._base.append(encodeOp(op))
129 await this._base.update()
130 }
131
132 /**
133 * Delete user
134 */
135 async deleteUser (userId) {
136 await this._base.append(encodeOp({ type: OpTypes.DELETE_USER, userId }))
137 await this._base.update()
138 }
139
140 /**
141 * List all users
142 */
143 async listUsers () {
144 const users = []
145 for await (const entry of this._view.createReadStream({
146 gte: `${this._prefix}user:`,
147 lt: `${this._prefix}user:\xff`
148 })) {
149 if (!entry.value.mergedInto) users.push(entry.value)
150 }
151 return users
152 }
153
154 // ==================== IDENTITY OPERATIONS ====================
155
156 /**
157 * Register identity in direct-write store
158 * @param {Object} identity - { identityId, method, publicKey, ... }
159 */
160 async registerIdentity (identity) {
161 if (!this.db) throw new Error('No direct-write db configured')
162 await this.db.put(`identity:${identity.identityId}`, identity)
163 if (identity.publicKey) {
164 await this.db.put(`pubkey:${identity.publicKey}`, { identityId: identity.identityId })
165 }
166 }
167
168 /**
169 * Get identity by ID
170 */
171 async getIdentity (identityId) {
172 if (!this.db) return null
173 const entry = await this.db.get(`identity:${identityId}`)
174 return entry ? entry.value : null
175 }
176
177 /**
178 * Get identity by public key
179 */
180 async getIdentityByPublicKey (publicKey) {
181 if (!this.db) return null
182 const entry = await this.db.get(`pubkey:${publicKey}`)
183 if (!entry) return null
184 return this.getIdentity(entry.value.identityId)
185 }
186
187 /**
188 * Link identity to user via Autobase
189 */
190 async linkIdentityToUser (identityId, userId) {
191 await this._base.append(encodeOp({
192 type: OpTypes.LINK_IDENTITY,
193 identityId,
194 userId
195 }))
196 await this._base.update()
197
198 // Also update direct-write identity record
199 if (this.db) {
200 const identity = await this.getIdentity(identityId)
201 if (identity) {
202 identity.userId = userId
203 await this.db.put(`identity:${identityId}`, identity)
204 }
205 }
206 }
207
208 /**
209 * Add identity to user
210 */
211 async addIdentityToUser (userId, identityId) {
212 await this._base.append(encodeOp({
213 type: OpTypes.ADD_IDENTITY,
214 userId,
215 identityId
216 }))
217 await this._base.update()
218 }
219
220 /**
221 * Remove identity from user
222 */
223 async removeIdentityFromUser (userId, identityId) {
224 await this._base.append(encodeOp({
225 type: OpTypes.REMOVE_IDENTITY,
226 userId,
227 identityId
228 }))
229 await this._base.update()
230 }
231
232 /**
233 * List user's identities
234 */
235 async listUserIdentities (userId) {
236 const user = await this.getUser(userId)
237 if (!user) return []
238
239 const identities = []
240 for (const id of user.identities) {
241 const identity = await this.getIdentity(id)
242 if (identity) identities.push(identity)
243 }
244 return identities
245 }
246
247 // ==================== PERMISSIONS ====================
248
249 /**
250 * Upgrade permissions for user on resource
251 * @param {string} userId
252 * @param {string} resourceName - Resource name or '*' for global
253 * @param {string[]} permissions - ['read', 'write', 'admin']
254 */
255 async upgradePermissions (userId, resourceName, permissions) {
256 await this._base.append(encodeOp({
257 type: OpTypes.UPGRADE_PERMISSIONS,
258 userId,
259 resourceName,
260 permissions
261 }))
262 await this._base.update()
263 }
264
265 /**
266 * Check if any admin user exists
267 */
268 async _hasAnyAdmin () {
269 for await (const entry of this._view.createReadStream({
270 gte: `${this._prefix}user:`,
271 lt: `${this._prefix}user:\xff`
272 })) {
273 const user = entry.value
274 if (user.mergedInto) continue
275 if (user.globalPermissions && user.globalPermissions.includes('admin')) return true
276 }
277 return false
278 }
279
280 // ==================== ACCOUNT MERGE ====================
281
282 /**
283 * Merge two accounts
284 */
285 async mergeAccounts (primaryUserId, secondaryUserId) {
286 await this._base.append(encodeOp({
287 type: OpTypes.MERGE_ACCOUNTS,
288 primaryUserId,
289 secondaryUserId
290 }))
291 await this._base.update()
292 }
293
294 // ==================== REVOCATION ====================
295
296 /**
297 * Revoke an identity
298 */
299 async revokeIdentity (identityId, opts = {}) {
300 await this._base.append(encodeOp({
301 type: OpTypes.REVOKE_IDENTITY,
302 identityId,
303 reason: opts.reason || null,
304 revokedAt: Date.now()
305 }))
306 await this._base.update()
307
308 // Also update direct-write store
309 if (this.db) {
310 const identity = await this.getIdentity(identityId)
311 if (identity) {
312 identity.revoked = true
313 identity.revokedAt = Date.now()
314 await this.db.put(`identity:${identityId}`, identity)
315 }
316 }
317 }
318
319 /**
320 * Restore a revoked identity
321 */
322 async restoreIdentity (identityId) {
323 await this._base.append(encodeOp({
324 type: OpTypes.RESTORE_IDENTITY,
325 identityId
326 }))
327 await this._base.update()
328
329 if (this.db) {
330 const identity = await this.getIdentity(identityId)
331 if (identity) {
332 delete identity.revoked
333 delete identity.revokedAt
334 await this.db.put(`identity:${identityId}`, identity)
335 }
336 }
337 }
338
339 /**
340 * List revoked identities
341 */
342 async listRevokedIdentities () {
343 if (!this.db) return []
344 const revoked = []
345 for await (const entry of this.db.createReadStream({
346 gte: 'identity:',
347 lt: 'identity:\xff'
348 })) {
349 if (entry.value.revoked) revoked.push(entry.value)
350 }
351 return revoked
352 }
353
354 /**
355 * Add Autobase writer key
356 */
357 async addWriter (key) {
358 const keyBuf = typeof key === 'string' ? b4a.from(key, 'hex') : key
359 await this._base.addWriter(keyBuf)
360 }
361
362 /**
363 * Close registry
364 */
365 async close () {
366 if (this._base) await this._base.close()
367 }
368}
369
370export default IdentityRegistry
lib/auth/providers/bearer-token.js +122 -0
……@@ -0,0 +1,122 @@
1import { BaseProvider } from '../base-provider.js'
2import crypto from 'hypercore-crypto'
3import b4a from 'b4a'
4
5/**
6 * BearerTokenProvider — API token authentication
7 *
8 * Manages bearer tokens with embedded permissions.
9 * Tokens are stored as SHA256 hashes for security.
10 */
11export class BearerTokenProvider extends BaseProvider {
12 constructor (identityRegistry, opts = {}) {
13 super('bearer-token')
14 this.registry = identityRegistry
15 // Use separate db for tokens (not Autobase view)
16 this.db = opts.db || (identityRegistry && identityRegistry.db) || null
17 }
18
19 async extractCredentials (req) {
20 const authHeader = req.headers.authorization
21 if (!authHeader || !authHeader.startsWith('Bearer ')) return null
22 return { token: authHeader.substring(7) }
23 }
24
25 async authenticate (credentials) {
26 const { token } = credentials
27 const tokenData = await this.validateToken(token)
28 if (!tokenData) throw new Error('Invalid token')
29
30 return {
31 identityId: `token:${tokenData.hash.substring(0, 16)}`,
32 method: 'bearer-token',
33 credentials: { username: tokenData.username, tokenHash: tokenData.hash },
34 permissions: tokenData.permissions || ['read'],
35 resources: tokenData.resources || ['*']
36 }
37 }
38
39 /**
40 * Create a new bearer token
41 */
42 async createToken (opts = {}) {
43 const token = b4a.toString(crypto.randomBytes(32), 'hex')
44 const hash = this._hashToken(token)
45
46 const tokenData = {
47 hash,
48 username: opts.username || 'anonymous',
49 description: opts.description || '',
50 permissions: opts.permissions || ['read'],
51 resources: opts.resources || ['*'],
52 createdAt: Date.now(),
53 expiresAt: opts.expiresAt || null
54 }
55
56 if (this.db) {
57 await this.db.put(`token:${hash}`, tokenData)
58 }
59
60 return { token, ...tokenData }
61 }
62
63 /**
64 * Validate a token
65 */
66 async validateToken (token) {
67 if (!this.db) return null
68 const hash = this._hashToken(token)
69 const entry = await this.db.get(`token:${hash}`)
70 if (!entry) return null
71
72 const data = entry.value
73 if (data.expiresAt && data.expiresAt < Date.now()) return null
74 if (data.revoked) return null
75
76 return data
77 }
78
79 /**
80 * Revoke a token
81 */
82 async revokeToken (tokenHash) {
83 if (!this.db) return
84 const entry = await this.db.get(`token:${tokenHash}`)
85 if (entry) {
86 entry.value.revoked = true
87 entry.value.revokedAt = Date.now()
88 await this.db.put(`token:${tokenHash}`, entry.value)
89 }
90 }
91
92 /**
93 * List all tokens
94 */
95 async listAllTokens () {
96 if (!this.db) return []
97 const tokens = []
98 for await (const entry of this.db.createReadStream({
99 gte: 'token:',
100 lt: 'token:\xff'
101 })) {
102 tokens.push(entry.value)
103 }
104 return tokens
105 }
106
107 /**
108 * Get tokens for specific user
109 */
110 async getUserTokens (username) {
111 const all = await this.listAllTokens()
112 return all.filter(t => t.username === username)
113 }
114
115 _hashToken (token) {
116 // Use hypercore-crypto for hashing (works in both Node and Bare)
117 const data = b4a.from(token)
118 return b4a.toString(crypto.data(data), 'hex')
119 }
120}
121
122export default BearerTokenProvider
lib/auth/providers/keet-identity.js +62 -0
……@@ -0,0 +1,62 @@
1import { BaseProvider } from '../base-provider.js'
2
3/**
4 * KeetIdentityProvider — BIP39 mnemonic-based authentication
5 *
6 * Uses keet-identity-key for mnemonic → identity keypair verification.
7 */
8export class KeetIdentityProvider extends BaseProvider {
9 constructor (identityRegistry, opts = {}) {
10 super('keet-identity')
11 this.registry = identityRegistry
12 this._IdentityKey = opts.IdentityKey || null
13 }
14
15 async extractCredentials (req) {
16 const identityProof = req.headers['x-identity-proof']
17 if (!identityProof) return null
18
19 try {
20 const proof = JSON.parse(identityProof)
21 return { proof }
22 } catch {
23 return null
24 }
25 }
26
27 async authenticate (credentials) {
28 if (!this._IdentityKey) throw new Error('keet-identity-key not configured')
29
30 const { proof } = credentials
31
32 // Verify proof chain
33 const verified = this._IdentityKey.verify(proof)
34 if (!verified) throw new Error('Invalid identity proof')
35
36 const publicKey = proof.publicKey || proof.identityPublicKey
37 if (!publicKey) throw new Error('No public key in proof')
38
39 // Look up identity
40 let identity = await this.registry.getIdentityByPublicKey(publicKey)
41
42 if (!identity) {
43 // Auto-register
44 const identityId = publicKey
45 await this.registry.registerIdentity({
46 identityId,
47 method: 'keet-identity',
48 publicKey,
49 createdAt: Date.now()
50 })
51 identity = { identityId, method: 'keet-identity', publicKey }
52 }
53
54 return {
55 identityId: identity.identityId,
56 method: 'keet-identity',
57 credentials: { publicKey, proof }
58 }
59 }
60}
61
62export default KeetIdentityProvider
lib/auth/providers/ssh-key.js +87 -0
……@@ -0,0 +1,87 @@
1import { BaseProvider } from '../base-provider.js'
2import { SSHKeyParser } from '../ssh-parser.js'
3import { ANProto } from '../../anproto.js'
4import b4a from 'b4a'
5
6/**
7 * SSHKeyProvider — SSH key authentication
8 *
9 * Verifies Ed25519 and RSA SSH key signatures.
10 * Requires node:crypto for RSA verification (Ed25519 uses hypercore-crypto).
11 */
12export class SSHKeyProvider extends BaseProvider {
13 constructor (identityRegistry, challengeManager) {
14 super('ssh-key')
15 this.registry = identityRegistry
16 this.challengeManager = challengeManager
17 }
18
19 async extractCredentials (req) {
20 const sshPublicKey = req.headers['x-ssh-publickey']
21 const sshSignature = req.headers['x-ssh-signature']
22 const challengeId = req.headers['x-ssh-challenge']
23
24 if (!sshPublicKey || !sshSignature || !challengeId) return null
25
26 return { sshPublicKey, sshSignature, challengeId }
27 }
28
29 async authenticate (credentials) {
30 const { sshPublicKey, sshSignature, challengeId } = credentials
31
32 // Verify challenge exists and is valid
33 const challenge = await this.challengeManager.verify(challengeId, challengeId)
34 if (!challenge) throw new Error('Invalid or expired challenge')
35
36 // Parse SSH key
37 const parsedKey = SSHKeyParser.parse(sshPublicKey)
38 const parsedSig = SSHKeyParser.parseSignature(sshSignature)
39
40 let valid = false
41
42 if (parsedKey.type === 'ed25519') {
43 // Use hypercore-crypto for Ed25519
44 valid = ANProto.verify(
45 challenge.challenge,
46 b4a.toString(parsedSig.signature, 'base64'),
47 parsedKey.publicKey
48 )
49 } else if (parsedKey.type === 'rsa') {
50 // RSA requires node:crypto
51 try {
52 const nodeCrypto = await import('node:crypto')
53 const pem = SSHKeyParser.toPEM(parsedKey)
54 const verifier = nodeCrypto.createVerify('SHA256')
55 verifier.update(challenge.challenge)
56 valid = verifier.verify(pem, parsedSig.signature)
57 } catch {
58 throw new Error('RSA verification requires node:crypto')
59 }
60 }
61
62 if (!valid) throw new Error('Invalid SSH signature')
63
64 // Use SSH public key string as identity ID
65 const identityId = `ssh:${sshPublicKey.substring(0, 64)}`
66 let identity = await this.registry.getIdentity(identityId)
67
68 if (!identity) {
69 await this.registry.registerIdentity({
70 identityId,
71 method: 'ssh-key',
72 publicKey: sshPublicKey,
73 keyType: parsedKey.type,
74 createdAt: Date.now()
75 })
76 identity = { identityId, method: 'ssh-key' }
77 }
78
79 return {
80 identityId,
81 method: 'ssh-key',
82 credentials: { sshPublicKey, keyType: parsedKey.type }
83 }
84 }
85}
86
87export default SSHKeyProvider
lib/auth/providers/web-identity.js +92 -0
……@@ -0,0 +1,92 @@
1import { BaseProvider } from '../base-provider.js'
2import { ANProto } from '../../anproto.js'
3import b4a from 'b4a'
4
5/**
6 * WebIdentityProvider — Browser ANProto authentication
7 *
8 * Handles Ed25519 challenge-response auth for web clients.
9 * Supports both signed (challenge-based) and legacy (unsigned) auth.
10 */
11export class WebIdentityProvider extends BaseProvider {
12 constructor (identityRegistry, challengeManager) {
13 super('web-identity')
14 this.registry = identityRegistry
15 this.challengeManager = challengeManager
16 }
17
18 async extractCredentials (req) {
19 const publicKey = req.headers['x-wirecore-publickey'] || req.headers['x-identity-publickey']
20 if (!publicKey) return null
21
22 const signature = req.headers['x-wirecore-signature'] || req.headers['x-identity-signature']
23 const challengeId = req.headers['x-wirecore-challenge'] || req.headers['x-identity-challenge']
24
25 return { publicKey, signature, challengeId }
26 }
27
28 async authenticate (credentials) {
29 const { publicKey, signature, challengeId } = credentials
30
31 if (signature && challengeId) {
32 // Signed auth — verify challenge signature
33 const challenge = await this.challengeManager.verify(challengeId, challengeId)
34 if (!challenge) {
35 // Try direct challenge verification
36 const stored = await this._getChallenge(challengeId)
37 if (!stored) throw new Error('Invalid or expired challenge')
38
39 const valid = ANProto.verify(stored.challenge, signature, publicKey)
40 if (!valid) throw new Error('Invalid signature')
41 } else {
42 const valid = ANProto.verify(challenge.challenge, signature, publicKey)
43 if (!valid) throw new Error('Invalid signature')
44 }
45 }
46
47 // Look up or auto-register identity
48 let identity = await this.registry.getIdentityByPublicKey(publicKey)
49
50 if (!identity) {
51 // Auto-register new web identity
52 const identityId = publicKey // Use public key as identity ID
53 await this.registry.registerIdentity({
54 identityId,
55 method: 'web-identity',
56 publicKey,
57 createdAt: Date.now()
58 })
59
60 // Auto-create user
61 const user = await this.registry.createUser({
62 name: 'Anonymous',
63 globalPermissions: ['read'],
64 resources: [{ name: '*', permissions: ['read'] }],
65 identities: [identityId]
66 })
67
68 // Link identity to user
69 if (user && user.userId) {
70 await this.registry.linkIdentityToUser(identityId, user.userId)
71 }
72
73 identity = await this.registry.getIdentity(identityId)
74 }
75
76 return {
77 identityId: identity.identityId || publicKey,
78 method: 'web-identity',
79 credentials: { publicKey }
80 }
81 }
82
83 async _getChallenge (challengeId) {
84 try {
85 return await this.challengeManager.verify(challengeId, challengeId)
86 } catch {
87 return null
88 }
89 }
90}
91
92export default WebIdentityProvider
lib/auth/rate-limiter.js +94 -0
……@@ -0,0 +1,94 @@
1/**
2 * RateLimiter — Token bucket rate limiting
3 *
4 * Per-client, per-operation rate limiting with persistent storage.
5 */
6export class RateLimiter {
7 constructor (opts = {}) {
8 this.db = opts.db || null
9 this.limits = opts.limits || {
10 auth: { tokens: 1000, refillRate: 100, refillInterval: 60 * 1000 },
11 api: { tokens: 10000, refillRate: 1000, refillInterval: 60 * 1000 }
12 }
13 this.cacheTimeout = opts.cacheTimeout || 10 * 1000
14 this._buckets = new Map()
15 this._cleanupInterval = setInterval(() => this._cleanup(), 60 * 1000)
16 }
17
18 /**
19 * Check rate limit for client/operation
20 * @param {string} clientId - Client identifier (IP, user ID, etc.)
21 * @param {string} operation - Operation type
22 * @returns {Promise<Object>} { allowed, remaining, retryAfter?, resetAt? }
23 */
24 async check (clientId, operation = 'api') {
25 const key = `${clientId}:${operation}`
26 const limit = this.limits[operation] || this.limits.api
27
28 let bucket = this._buckets.get(key)
29 if (!bucket) {
30 bucket = {
31 tokens: limit.tokens,
32 lastRefill: Date.now(),
33 operation
34 }
35 this._buckets.set(key, bucket)
36 }
37
38 // Refill tokens
39 const now = Date.now()
40 const elapsed = now - bucket.lastRefill
41 const refills = Math.floor(elapsed / limit.refillInterval)
42 if (refills > 0) {
43 bucket.tokens = Math.min(limit.tokens, bucket.tokens + refills * limit.refillRate)
44 bucket.lastRefill = now
45 }
46
47 // Check
48 if (bucket.tokens <= 0) {
49 const retryAfter = Math.ceil(limit.refillInterval / 1000)
50 return {
51 allowed: false,
52 remaining: 0,
53 retryAfter,
54 resetAt: bucket.lastRefill + limit.refillInterval
55 }
56 }
57
58 // Consume token
59 bucket.tokens--
60
61 return {
62 allowed: true,
63 remaining: bucket.tokens
64 }
65 }
66
67 /**
68 * Cleanup old buckets
69 */
70 _cleanup () {
71 const cutoff = Date.now() - 10 * 60 * 1000 // 10 min
72 for (const [key, bucket] of this._buckets) {
73 if (bucket.lastRefill < cutoff) {
74 this._buckets.delete(key)
75 }
76 }
77 }
78
79 /**
80 * Get stats
81 */
82 async stats () {
83 return {
84 activeBuckets: this._buckets.size
85 }
86 }
87
88 destroy () {
89 clearInterval(this._cleanupInterval)
90 this._buckets.clear()
91 }
92}
93
94export default RateLimiter
lib/auth/ssh-parser.js +107 -0
……@@ -0,0 +1,107 @@
1import b4a from 'b4a'
2
3/**
4 * SSH Key Parser
5 *
6 * Parses SSH public keys (RFC 4253 format) and extracts RSA components.
7 * Supports: ssh-rsa, ssh-ed25519
8 */
9export class SSHKeyParser {
10 static parse (sshPublicKey) {
11 const parts = sshPublicKey.trim().split(/\s+/)
12 if (parts.length < 2) throw new Error('Invalid SSH key format')
13
14 const keyType = parts[0]
15 const keyData = parts[1]
16 const buffer = b4a.from(keyData, 'base64')
17
18 switch (keyType) {
19 case 'ssh-rsa': return this.parseRSA(buffer)
20 case 'ssh-ed25519': return this.parseEd25519(buffer)
21 default: throw new Error(`Unsupported key type: ${keyType}`)
22 }
23 }
24
25 static parseRSA (buffer) {
26 let offset = 0
27 const algoLen = buffer.readUInt32BE(offset); offset += 4
28 const algorithm = buffer.toString('utf8', offset, offset + algoLen); offset += algoLen
29 if (algorithm !== 'ssh-rsa') throw new Error(`Expected ssh-rsa, got ${algorithm}`)
30
31 const eLen = buffer.readUInt32BE(offset); offset += 4
32 const e = buffer.subarray(offset, offset + eLen); offset += eLen
33 const nLen = buffer.readUInt32BE(offset); offset += 4
34 const n = buffer.subarray(offset, offset + nLen)
35
36 return { type: 'rsa', e: b4a.from(e), n: b4a.from(n) }
37 }
38
39 static parseEd25519 (buffer) {
40 let offset = 0
41 const algoLen = buffer.readUInt32BE(offset); offset += 4
42 const algorithm = buffer.toString('utf8', offset, offset + algoLen); offset += algoLen
43 if (algorithm !== 'ssh-ed25519') throw new Error(`Expected ssh-ed25519, got ${algorithm}`)
44
45 const keyLen = buffer.readUInt32BE(offset); offset += 4
46 const publicKey = buffer.subarray(offset, offset + keyLen)
47 return { type: 'ed25519', publicKey: b4a.from(publicKey) }
48 }
49
50 static parseSignature (signatureBase64) {
51 const buffer = b4a.from(signatureBase64, 'base64')
52 let offset = 0
53 const algoLen = buffer.readUInt32BE(offset); offset += 4
54 const algorithm = buffer.toString('utf8', offset, offset + algoLen); offset += algoLen
55 const sigLen = buffer.readUInt32BE(offset); offset += 4
56 const signature = buffer.subarray(offset, offset + sigLen)
57
58 let type
59 if (algorithm === 'ssh-rsa') type = 'rsa'
60 else if (algorithm === 'ssh-ed25519') type = 'ed25519'
61 else throw new Error(`Unsupported signature algorithm: ${algorithm}`)
62
63 return { type, signature: b4a.from(signature) }
64 }
65
66 static toPEM (key) {
67 if (key.type !== 'rsa') throw new Error('Only RSA keys can be converted to PEM')
68 const der = this.encodeRSAPublicKeyDER(key.n, key.e)
69 const base64 = b4a.toString(der, 'base64')
70 const lines = base64.match(/.{1,64}/g) || []
71 return ['-----BEGIN PUBLIC KEY-----', ...lines, '-----END PUBLIC KEY-----'].join('\n')
72 }
73
74 static encodeRSAPublicKeyDER (n, e) {
75 const encodeInteger = (buf) => {
76 const needsPadding = buf[0] & 0x80
77 const length = buf.length + (needsPadding ? 1 : 0)
78 const result = b4a.alloc(2 + length)
79 result[0] = 0x02
80 result[1] = length
81 if (needsPadding) { result[2] = 0x00; buf.copy(result, 3) }
82 else { buf.copy(result, 2) }
83 return result
84 }
85
86 const nInt = encodeInteger(n)
87 const eInt = encodeInteger(e)
88 const innerSeq = b4a.concat([nInt, eInt])
89 const innerSeqEncoded = b4a.concat([b4a.from([0x30]), this.encodeLength(innerSeq.length), innerSeq])
90 const bitString = b4a.concat([b4a.from([0x03]), this.encodeLength(innerSeqEncoded.length + 1), b4a.from([0x00]), innerSeqEncoded])
91 const rsaOID = b4a.from([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01])
92 const algNull = b4a.from([0x05, 0x00])
93 const algSeq = b4a.concat([b4a.from([0x30]), this.encodeLength(rsaOID.length + algNull.length), rsaOID, algNull])
94 const outerContent = b4a.concat([algSeq, bitString])
95 return b4a.concat([b4a.from([0x30]), this.encodeLength(outerContent.length), outerContent])
96 }
97
98 static encodeLength (length) {
99 if (length < 128) return b4a.from([length])
100 const bytes = []
101 let len = length
102 while (len > 0) { bytes.unshift(len & 0xff); len >>= 8 }
103 return b4a.from([0x80 | bytes.length, ...bytes])
104 }
105}
106
107export default SSHKeyParser
lib/device-linker.js +83 -0
……@@ -0,0 +1,83 @@
1import BlindPairing from 'blind-pairing'
2import b4a from 'b4a'
3
4/**
5 * DeviceLinker — Blind-pairing device linking for mnemonic exchange
6 *
7 * Uses blind-pairing over Hyperswarm for secure mnemonic transfer
8 * between devices owned by the same user.
9 */
10export class DeviceLinker {
11 constructor (swarm, opts = {}) {
12 this._swarm = swarm
13 this._identityManager = opts.identityManager || null
14 this._onLinked = opts.onLinked || null // callback(mnemonic, remoteInfo)
15 }
16
17 /**
18 * Create a link invite (source device)
19 * Generates invite hex that the target device uses to pair.
20 * @returns {Promise<{ invite: string, promise: Promise<Object> }>}
21 */
22 async createInvite () {
23 return new Promise((resolve, reject) => {
24 const mnemonic = this._identityManager ? this._identityManager.getMnemonic() : null
25 if (!mnemonic) {
26 reject(new Error('No mnemonic available to share'))
27 return
28 }
29
30 const member = BlindPairing.member(this._swarm, {
31 onadd (candidate) {
32 // Send mnemonic to paired device
33 candidate.open(b4a.from(JSON.stringify({
34 mnemonic,
35 instanceKey: this._identityManager ? this._identityManager.getPublicKeyHex() : null
36 })))
37 }
38 })
39
40 member.invite().then(invite => {
41 resolve({
42 invite: b4a.toString(invite, 'hex'),
43 member
44 })
45 }).catch(reject)
46 })
47 }
48
49 /**
50 * Join with invite (target device)
51 * Uses invite hex from source device to receive mnemonic.
52 * @param {string} inviteHex - Hex-encoded invite from source
53 * @returns {Promise<Object>} { mnemonic, instanceKey }
54 */
55 async joinWithInvite (inviteHex) {
56 return new Promise((resolve, reject) => {
57 const invite = b4a.from(inviteHex, 'hex')
58
59 const candidate = BlindPairing.candidate(this._swarm, invite, {
60 onopen (data) {
61 try {
62 const payload = JSON.parse(b4a.toString(data))
63 if (this._identityManager && payload.mnemonic) {
64 this._identityManager.setMnemonic(payload.mnemonic).catch(() => {})
65 }
66 if (this._onLinked) this._onLinked(payload.mnemonic, payload)
67 resolve(payload)
68 } catch (err) {
69 reject(err)
70 }
71 }
72 })
73
74 candidate.on('error', reject)
75 })
76 }
77
78 destroy () {
79 // Cleanup if needed
80 }
81}
82
83export default DeviceLinker
lib/identity-manager.js +155 -0
……@@ -0,0 +1,155 @@
1import crypto from 'hypercore-crypto'
2import b4a from 'b4a'
3
4/**
5 * IdentityManager — Persistent identity keypairs + mnemonic proof chains
6 *
7 * Manages:
8 * - Instance keypair (persistent Ed25519 for this device)
9 * - Mnemonic-based identity (keet-identity-key proof chains)
10 * - Device attestation for multi-writer
11 */
12export class IdentityManager {
13 constructor (store, opts = {}) {
14 this._store = store // Hyperbee or similar key-value store
15 this._keypair = null
16 this._mnemonic = null
17 this._identityKey = null
18 this._IdentityKey = opts.IdentityKey || null // keet-identity-key class (optional)
19 }
20
21 /**
22 * Initialize — load or generate persistent keypair
23 */
24 async ready () {
25 // Try to load existing keypair
26 const existing = await this._store.get('instance-keypair')
27 if (existing) {
28 this._keypair = {
29 publicKey: b4a.from(existing.value.publicKey, 'hex'),
30 secretKey: b4a.from(existing.value.secretKey, 'hex')
31 }
32 } else {
33 // Generate new persistent keypair
34 this._keypair = crypto.keyPair()
35 await this._store.put('instance-keypair', {
36 publicKey: b4a.toString(this._keypair.publicKey, 'hex'),
37 secretKey: b4a.toString(this._keypair.secretKey, 'hex')
38 })
39 }
40
41 // Load mnemonic if stored
42 const mnemonicEntry = await this._store.get('identity-mnemonic')
43 if (mnemonicEntry) {
44 this._mnemonic = mnemonicEntry.value
45 await this._loadIdentityKey()
46 }
47 }
48
49 /**
50 * Load keet-identity-key from stored mnemonic
51 */
52 async _loadIdentityKey () {
53 if (!this._IdentityKey || !this._mnemonic) return
54 try {
55 this._identityKey = this._IdentityKey.from({ mnemonic: this._mnemonic })
56 } catch {
57 this._identityKey = null
58 }
59 }
60
61 /**
62 * Get instance public key as hex string
63 */
64 getPublicKeyHex () {
65 if (!this._keypair) return null
66 return b4a.toString(this._keypair.publicKey, 'hex')
67 }
68
69 /**
70 * Get instance public key buffer
71 */
72 getPublicKey () {
73 return this._keypair ? this._keypair.publicKey : null
74 }
75
76 /**
77 * Get instance keypair
78 */
79 getKeypair () {
80 return this._keypair
81 }
82
83 /**
84 * Set mnemonic and derive identity key
85 * @param {string} mnemonic - BIP39 mnemonic
86 */
87 async setMnemonic (mnemonic) {
88 this._mnemonic = mnemonic
89 await this._store.put('identity-mnemonic', mnemonic)
90 await this._loadIdentityKey()
91 }
92
93 /**
94 * Get mnemonic (if stored)
95 */
96 getMnemonic () {
97 return this._mnemonic
98 }
99
100 /**
101 * Get keet-identity-key instance (if mnemonic loaded)
102 */
103 getIdentityKey () {
104 return this._identityKey
105 }
106
107 /**
108 * Bootstrap device attestation proof chain
109 * @param {Buffer} devicePublicKey - This device's public key
110 * @returns {Object} Proof chain for device attestation
111 */
112 async bootstrap (devicePublicKey) {
113 if (!this._identityKey) throw new Error('No identity key loaded')
114 return this._identityKey.bootstrap(devicePublicKey)
115 }
116
117 /**
118 * Sign data with instance keypair
119 * @param {Buffer} data - Data to sign
120 * @returns {Buffer} Signature
121 */
122 sign (data) {
123 if (!this._keypair) throw new Error('Not initialized')
124 return crypto.sign(data, this._keypair.secretKey)
125 }
126
127 /**
128 * Verify signature with instance public key
129 * @param {Buffer} data - Original data
130 * @param {Buffer} signature - Signature to verify
131 * @returns {boolean}
132 */
133 verify (data, signature) {
134 if (!this._keypair) throw new Error('Not initialized')
135 return crypto.verify(data, signature, this._keypair.publicKey)
136 }
137
138 /**
139 * Get identity public key (from mnemonic, distinct from instance key)
140 */
141 getIdentityPublicKey () {
142 if (!this._identityKey) return null
143 return this._identityKey.publicKey
144 }
145
146 /**
147 * Get identity public key as hex
148 */
149 getIdentityPublicKeyHex () {
150 const pk = this.getIdentityPublicKey()
151 return pk ? b4a.toString(pk, 'hex') : null
152 }
153}
154
155export default IdentityManager
lib/secure-storage.js +126 -0
……@@ -0,0 +1,126 @@
1import sodium from 'sodium-native'
2import b4a from 'b4a'
3
4/**
5 * SecureStorage — Encrypted vault for sensitive data
6 *
7 * Uses Argon2id for key derivation and XSalsa20-Poly1305 for encryption.
8 * Stores encrypted data in a Hyperbee or similar key-value store.
9 */
10export class SecureStorage {
11 constructor (store) {
12 this._store = store // Hyperbee or similar key-value store
13 }
14
15 /**
16 * Derive encryption key from password using Argon2id
17 * @param {string} password - User password
18 * @param {Buffer} salt - 16-byte salt
19 * @returns {Buffer} 32-byte key
20 */
21 static deriveKey (password, salt) {
22 const key = b4a.alloc(32)
23 const pw = b4a.from(password)
24 sodium.crypto_pwhash(
25 key,
26 pw,
27 salt,
28 sodium.crypto_pwhash_OPSLIMIT_MODERATE,
29 sodium.crypto_pwhash_MEMLIMIT_MODERATE,
30 sodium.crypto_pwhash_ALG_ARGON2ID13
31 )
32 return key
33 }
34
35 /**
36 * Encrypt data with key
37 * @param {Buffer} data - Data to encrypt
38 * @param {Buffer} key - 32-byte encryption key
39 * @returns {{ nonce: Buffer, ciphertext: Buffer }}
40 */
41 static encrypt (data, key) {
42 const nonce = b4a.alloc(24)
43 sodium.randombytes_buf(nonce)
44 const ciphertext = b4a.alloc(data.length + sodium.crypto_secretbox_MACBYTES)
45 sodium.crypto_secretbox_easy(ciphertext, data, nonce, key)
46 return { nonce, ciphertext }
47 }
48
49 /**
50 * Decrypt data with key
51 * @param {Buffer} ciphertext - Encrypted data
52 * @param {Buffer} nonce - 24-byte nonce
53 * @param {Buffer} key - 32-byte decryption key
54 * @returns {Buffer} Decrypted data
55 */
56 static decrypt (ciphertext, nonce, key) {
57 const plaintext = b4a.alloc(ciphertext.length - sodium.crypto_secretbox_MACBYTES)
58 const ok = sodium.crypto_secretbox_open_easy(plaintext, ciphertext, nonce, key)
59 if (!ok) throw new Error('Decryption failed')
60 return plaintext
61 }
62
63 /**
64 * Store encrypted value
65 * @param {string} name - Vault entry name
66 * @param {string} value - Value to encrypt
67 * @param {string} password - Encryption password
68 */
69 async put (name, value, password) {
70 const salt = b4a.alloc(16)
71 sodium.randombytes_buf(salt)
72 const key = SecureStorage.deriveKey(password, salt)
73 const { nonce, ciphertext } = SecureStorage.encrypt(b4a.from(value), key)
74
75 await this._store.put(`vault:${name}`, {
76 salt: b4a.toString(salt, 'hex'),
77 nonce: b4a.toString(nonce, 'hex'),
78 ciphertext: b4a.toString(ciphertext, 'hex')
79 })
80 }
81
82 /**
83 * Retrieve and decrypt value
84 * @param {string} name - Vault entry name
85 * @param {string} password - Decryption password
86 * @returns {string|null} Decrypted value or null
87 */
88 async get (name, password) {
89 const entry = await this._store.get(`vault:${name}`)
90 if (!entry) return null
91
92 const { salt, nonce, ciphertext } = entry.value
93 const key = SecureStorage.deriveKey(password, b4a.from(salt, 'hex'))
94
95 try {
96 const plaintext = SecureStorage.decrypt(
97 b4a.from(ciphertext, 'hex'),
98 b4a.from(nonce, 'hex'),
99 key
100 )
101 return b4a.toString(plaintext)
102 } catch {
103 return null
104 }
105 }
106
107 /**
108 * Check if vault entry exists
109 * @param {string} name - Vault entry name
110 * @returns {boolean}
111 */
112 async has (name) {
113 const entry = await this._store.get(`vault:${name}`)
114 return entry !== null
115 }
116
117 /**
118 * Delete vault entry
119 * @param {string} name - Vault entry name
120 */
121 async del (name) {
122 await this._store.del(`vault:${name}`)
123 }
124}
125
126export default SecureStorage
package.json +40 -0
……@@ -0,0 +1,40 @@
1{
2 "name": "hyper-identity",
3 "version": "1.0.0",
4 "description": "Generic P2P identity management with Autobase, ANProto, and multi-method authentication",
5 "type": "module",
6 "main": "index.js",
7 "exports": {
8 ".": "./index.js",
9 "./anproto": "./lib/anproto.js",
10 "./auth": "./lib/auth.js",
11 "./identity-manager": "./lib/identity-manager.js",
12 "./device-linker": "./lib/device-linker.js",
13 "./secure-storage": "./lib/secure-storage.js",
14 "./auth/identity-registry": "./lib/auth/identity-registry.js",
15 "./auth/identity-apply": "./lib/auth/identity-apply.js",
16 "./auth/identity-operations": "./lib/auth/identity-operations.js",
17 "./auth/challenge-manager": "./lib/auth/challenge-manager.js",
18 "./auth/rate-limiter": "./lib/auth/rate-limiter.js",
19 "./auth/base-provider": "./lib/auth/base-provider.js",
20 "./auth/ssh-parser": "./lib/auth/ssh-parser.js",
21 "./auth/providers/web-identity": "./lib/auth/providers/web-identity.js",
22 "./auth/providers/keet-identity": "./lib/auth/providers/keet-identity.js",
23 "./auth/providers/bearer-token": "./lib/auth/providers/bearer-token.js",
24 "./auth/providers/ssh-key": "./lib/auth/providers/ssh-key.js"
25 },
26 "dependencies": {
27 "autobase": "^7.25.1",
28 "b4a": "^1.6.4",
29 "bip39-mnemonic": "^2.5.0",
30 "blind-pairing": "^2.3.1",
31 "corestore": "^7.8.0",
32 "hyperbee": "^2.27.3",
33 "hypercore": "^11.24.0",
34 "hypercore-crypto": "^3.0.0",
35 "hyperswarm": "^4.16.0",
36 "keet-identity-key": "github:holepunchto/keet-identity-key",
37 "sodium-native": "^4.3.1"
38 },
39 "license": "MIT"
40}