conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

ids.js (3001B)


      1 // src/lib/ids.js - identifier and token generation
      2 //
      3 // Identifiers are generated by the application rather than by the database,
      4 // because lastInsertId is not portable across the three supported dialects
      5 // and because a conductor needs an id before it has written anything.
      6 
      7 import crypto from 'node:crypto';
      8 
      9 // Crockford base32 without the ambiguous letters I, L, O and U. Case
     10 // insensitive and safe to read aloud or paste into a URL.
     11 const ALPHABET = '0123456789abcdefghjkmnpqrstvwxyz';
     12 
     13 export function randomId(length = 24) {
     14   // Rejection free: 256 is not a multiple of 32, so mask to 5 bits.
     15   const bytes = crypto.randomBytes(length);
     16   let out = '';
     17   for (let i = 0; i < length; i += 1) out += ALPHABET[bytes[i] & 31];
     18   return out;
     19 }
     20 
     21 // Sortable by creation time, which keeps job listings stable without relying
     22 // on a clock skewed created_at. 8 chars of millisecond timestamp in base32
     23 // followed by 12 random chars.
     24 export function timeOrderedId(now = Date.now()) {
     25   let stamp = '';
     26   let remaining = now;
     27   for (let i = 0; i < 8; i += 1) {
     28     stamp = ALPHABET[remaining % 32] + stamp;
     29     remaining = Math.floor(remaining / 32);
     30   }
     31   return stamp + randomId(12);
     32 }
     33 
     34 export const newJobId = () => timeOrderedId();
     35 export const newProjectId = () => randomId(16);
     36 export const newArtifactId = () => randomId(24);
     37 export const newUserId = () => randomId(16);
     38 export const newWorkerTokenId = () => randomId(16);
     39 
     40 // Task ids carry no structure at all. A worker holding one cannot tell which
     41 // job or project it belongs to, which is the point: a worker is given a
     42 // context and a script, and has no business knowing what they are for.
     43 //
     44 // It also removes a class of nuisance. The previous form embedded the task
     45 // name, so an id could contain ':' ',' and '=', and every use had to escape
     46 // it for a URL, sanitize it for a container name and encode it for a storage
     47 // key. Time ordered so that tasks of one job still group naturally.
     48 export const newTaskId = () => timeOrderedId();
     49 
     50 // Worker and session tokens. The plaintext is shown once and only its hash
     51 // is stored, so a database leak does not yield usable credentials.
     52 export function newToken() {
     53   return crypto.randomBytes(32).toString('hex');
     54 }
     55 
     56 export function hashToken(token) {
     57   return crypto.createHash('sha256').update(String(token)).digest('hex');
     58 }
     59 
     60 // Compares two hex digests without leaking position through timing.
     61 export function safeEqualHex(a, b) {
     62   if (typeof a !== 'string' || typeof b !== 'string' || a.length !== b.length) return false;
     63   return crypto.timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
     64 }
     65 
     66 // A project id derived from a human supplied name, for the common case where
     67 // an operator wants a readable slug rather than a random string.
     68 export function slugify(input, fallbackLength = 16) {
     69   const slug = String(input)
     70     .toLowerCase()
     71     .replace(/[^a-z0-9]+/g, '-')
     72     .replace(/^-+|-+$/g, '')
     73     .slice(0, 64);
     74   return slug || randomId(fallbackLength);
     75 }