workers.js (3324B)
1 // src/lib/workers.js - worker registration tokens 2 // 3 // A worker authenticates with a bearer token. Only the SHA-256 of the token 4 // is stored, so the database never holds a usable credential; the plaintext 5 // is shown once at creation and cannot be recovered afterwards. 6 // 7 // A token may belong to a user, in which case it is only ever offered jobs 8 // from that user's projects. A token with no owner is shared and can run 9 // anything, which is how an administrator provides general capacity. That 10 // distinction is enforced in the scheduler, not here. 11 12 import { newToken, hashToken, newWorkerTokenId } from './ids.js'; 13 14 export function createWorkerTokens({ db }) { 15 return { 16 async create(name, { ownerId = null } = {}) { 17 if (!name || typeof name !== 'string') throw new Error('worker token needs a name'); 18 const token = newToken(); 19 const id = newWorkerTokenId(); 20 await db.run( 21 `INSERT INTO worker_tokens (id, name, token_hash, enabled, owner_id, created_at) 22 VALUES ({id}, {name}, {hash}, 1, {owner}, {now})`, 23 { id, name, hash: hashToken(token), owner: ownerId, now: Date.now() } 24 ); 25 // The only time the plaintext exists outside the worker. 26 return { id, name, token, owner_id: ownerId }; 27 }, 28 29 async get(id) { 30 return db.get( 31 'SELECT id, name, enabled, owner_id, created_at, last_seen_at, last_ip FROM worker_tokens WHERE id = {id}', 32 { id } 33 ); 34 }, 35 36 // Returns the token record, or null. Lookup is by hash, so a timing 37 // difference cannot reveal anything beyond whether a hash exists. 38 async verify(presented, { ip = null } = {}) { 39 if (typeof presented !== 'string' || presented.length === 0) return null; 40 const hash = hashToken(presented); 41 const row = await db.get( 42 'SELECT id, name, enabled, owner_id FROM worker_tokens WHERE token_hash = {hash}', 43 { hash } 44 ); 45 if (!row || row.enabled !== 1) return null; 46 47 await db.run( 48 'UPDATE worker_tokens SET last_seen_at = {now}, last_ip = {ip} WHERE id = {id}', 49 { id: row.id, now: Date.now(), ip } 50 ); 51 return { id: row.id, name: row.name, owner_id: row.owner_id }; 52 }, 53 54 async list() { 55 return db.all( 56 `SELECT id, name, enabled, owner_id, created_at, last_seen_at, last_ip 57 FROM worker_tokens ORDER BY created_at DESC` 58 ); 59 }, 60 61 async listVisible(user) { 62 if (user && user.role === 'admin') return this.list(); 63 if (!user) return []; 64 return db.all( 65 `SELECT id, name, enabled, owner_id, created_at, last_seen_at, last_ip 66 FROM worker_tokens WHERE owner_id = {owner} ORDER BY created_at DESC`, 67 { owner: user.id } 68 ); 69 }, 70 71 async setEnabled(id, enabled) { 72 const res = await db.run( 73 'UPDATE worker_tokens SET enabled = {enabled} WHERE id = {id}', 74 { id, enabled: enabled ? 1 : 0 } 75 ); 76 return res.changes > 0; 77 }, 78 79 async remove(id) { 80 const res = await db.run('DELETE FROM worker_tokens WHERE id = {id}', { id }); 81 return res.changes > 0; 82 }, 83 }; 84 } 85 86 export function canManageWorker(user, token) { 87 if (!user || !token) return false; 88 if (user.role === 'admin') return true; 89 return token.owner_id !== null && token.owner_id === user.id; 90 }