import { AuthProvider } from '../base-provider.js'
import crypto from 'bare-crypto'
/**
* BearerTokenProvider
*
* Implements token-based authentication:
* - Generates secure API tokens
* - Validates tokens from Authorization header or query params
* - Supports per-notebook or global permissions
* - Stores tokens in Hyperbee
*/
export class BearerTokenProvider extends AuthProvider {
constructor(registry) {
super(registry)
this.name = 'bearer-token'
this.tokenPrefix = 'token:'
this.userPrefix = 'user:'
}
/**
* Extract token from HTTP request
* Supports:
* - Authorization: Bearer <token>
* - Authorization: Token <token>
* - X-Auth-Token header
*/
async extractCredentials(req) {
// Check Authorization header
const authHeader = req.headers.authorization || req.headers.Authorization
if (authHeader) {
const match = authHeader.match(/^(Bearer|Token)\s+(.+)$/i)
if (match) return match[2]
}
// Check X-Auth-Token header
const tokenHeader = req.headers['x-auth-token'] || req.headers['X-Auth-Token']
if (tokenHeader) return tokenHeader
return null
}
/**
* Authenticate using bearer token
* @param {string} token - Bearer token
* @returns {Promise<Object>} { identityId, method, verified, credentials, permissions, notebooks }
*/
async authenticate(token) {
const tokenData = await this.validateToken(token)
if (!tokenData) {
throw new Error('Invalid or expired token')
}
// Check if token is revoked
const isRevoked = await this.registry.isRevoked(tokenData.tokenHash)
if (isRevoked) {
throw new Error('Bearer token has been revoked')
}
// For bearer tokens, the identityId is the tokenHash
return {
identityId: tokenData.tokenHash,
method: 'bearer-token',
verified: true,
credentials: {
username: tokenData.username,
tokenHash: tokenData.tokenHash
},
// Bearer tokens have permissions embedded (not from User object)
permissions: tokenData.permissions,
notebooks: tokenData.notebooks
}
}
/**
* Validate a token and return token data if valid
*/
async validateToken(token) {
if (!token) return null
const tokenHash = this.hashToken(token)
try {
const entry = await this.registry.db.get(`${this.tokenPrefix}${tokenHash}`)
if (!entry) return null
const tokenData = entry.value
// Check expiration
if (tokenData.expiresAt && Date.now() > tokenData.expiresAt) {
return null
}
// Update usage stats (async, don't wait)
this.updateTokenUsage(tokenHash).catch(console.error)
return tokenData
} catch {
return null
}
}
/**
* Hash a token for storage
*/
hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex')
}
/**
* Generate a secure random token
*/
generateToken() {
const bytes = crypto.randomBytes(32)
return bytes.toString('hex')
}
/**
* Create a new token
*/
async createToken(options = {}) {
const {
username,
description = 'API Token',
permissions = ['read'],
notebooks = ['*'],
expiresAt = null
} = options
if (!username) {
throw new Error('Username is required')
}
const token = this.generateToken()
const tokenHash = this.hashToken(token)
const tokenData = {
tokenHash,
username,
description,
permissions,
notebooks,
createdAt: Date.now(),
expiresAt,
lastUsedAt: null,
usageCount: 0
}
// Store in Hyperbee
await this.registry.db.put(`${this.tokenPrefix}${tokenHash}`, tokenData)
// Also add to user's token list
const userTokens = await this.getUserTokens(username)
userTokens.push(tokenHash)
await this.registry.db.put(`${this.userPrefix}${username}:tokens`, userTokens)
return {
token,
tokenHash,
...tokenData
}
}
/**
* Update token usage statistics
*/
async updateTokenUsage(tokenHash) {
const entry = await this.registry.db.get(`${this.tokenPrefix}${tokenHash}`)
if (entry) {
entry.value.lastUsedAt = Date.now()
entry.value.usageCount = (entry.value.usageCount || 0) + 1
await this.registry.db.put(`${this.tokenPrefix}${tokenHash}`, entry.value)
}
}
/**
* Get all tokens for a user
*/
async getUserTokens(username) {
try {
const entry = await this.registry.db.get(`${this.userPrefix}${username}:tokens`)
return entry ? entry.value : []
} catch {
return []
}
}
/**
* Revoke a token
*/
async revokeToken(tokenHash) {
const entry = await this.registry.db.get(`${this.tokenPrefix}${tokenHash}`)
if (!entry) return false
await this.registry.db.del(`${this.tokenPrefix}${tokenHash}`)
// Remove from user's token list
const username = entry.value.username
const userTokens = await this.getUserTokens(username)
const filtered = userTokens.filter(t => t !== tokenHash)
await this.registry.db.put(`${this.userPrefix}${username}:tokens`, filtered)
return true
}
/**
* List all tokens
*/
async listAllTokens() {
const tokens = []
for await (const entry of this.registry.db.createReadStream({
gt: this.tokenPrefix,
lt: this.tokenPrefix + '\xff'
})) {
tokens.push(entry.value)
}
return tokens
}
}