// name.js — what a record is called.
//
// ⛔ THE DEFECT. A record's name was `String(Date.now())` (rpc.js) or a clock plus four hex
// characters (room.js). hyper-social #59 §2 named both faults: a millisecond clock COLLIDES, and the
// name came in as `a.name || …` — caller-supplied — so a collision was CHOOSABLE rather than merely
// improbable. Apply wrote it with an unconditional `set`, so a collision silently replaced a record,
// and in a room it silently reattributed one to whoever collided.
//
// ⛔ AND A NAME IS NOT A POSITION. #59 §2 proposed assigning it inside apply, the way keet assigns
// `index: p.count`. #61 §2b measured keet and found the reverse: `{thread, index}` is a view
// COORDINATE, and every cross-reference — parent, replyTo, and the edit target — resolves through
// `@keet/chat-by-message-id`, keyed by `oplog` (contract/chat.js:37-43, :89-101). A position is a
// function of the linearisation; the linearisation is re-derived on every boot and every reorg. So a
// permalink built from a position can change AFTER it has been handed out and embedded in bytes that
// #8 forbids rewriting. `oplog` is collision-free by construction, known at append time and stable
// under reorg — that is the property being reproduced here, without the coordination.
//
// So a name is: minted at write time (bytes go to a path before any append lands), carrying enough
// entropy that a collision is not a practical event, ordered by time because timelines and every
// bridge's rkey depend on it, and never supplied by a caller.
//
// ⚠ WHAT THIS IS NOT. This does not make a name an identity in keet's sense — provenance still lives
// in the log, and two records with the same bytes in two rooms are still two disclosures
// (SOCIAL-MODEL §9 #5). It removes the collision and the caller's ability to choose one. The
// address question — what the authority segment of `favela://` is — is #61 §12 #1 and stays open.
import crypto from 'hypercore-crypto'
// base32hex, lowercase (RFC 4648 §7 alphabet, downcased). Chosen because ASCII order matches value
// order — '0' < '9' < 'a' < 'v' — so a plain string sort is a chronological sort. That is a property
// the timeline, the record store's byte-ordered scan, and the bsky TID projection all lean on.
const ALPHABET = '0123456789abcdefghijklmnopqrstuv'
const TIME_LEN = 10 // 32^10 ms ≈ year 38,000. Fixed width, because sorting needs it.
const RAND_LEN = 16 // 80 bits
export const NAME_LEN = TIME_LEN + RAND_LEN
const NAME_RE = new RegExp('^[' + ALPHABET + ']{' + NAME_LEN + '}$')
function encode (value, width) {
let n = BigInt(value)
let out = ''
for (let i = 0; i < width; i++) {
out = ALPHABET[Number(n % 32n)] + out
n /= 32n
}
return out
}
/**
* Mint a name for a new record.
*
* ⛔ `at` and `bytes` are injectable FOR TESTS ONLY. Nothing in the daemon passes them, and no RPC
* surface may ever expose them — a caller who can choose either can choose a collision, which is
* precisely the defect this file exists to remove.
*/
export function recordName (at = Date.now(), bytes = null) {
const r = bytes || crypto.randomBytes(10) // 80 bits
let n = 0n
for (let i = 0; i < 10; i++) n = (n << 8n) | BigInt(r[i])
return encode(Math.floor(at), TIME_LEN) + encode(n, RAND_LEN)
}
/** True if `s` is one of ours. Deliberately strict: a legacy clock name is NOT this format. */
export function isRecordName (s) {
return typeof s === 'string' && NAME_RE.test(s)
}
/**
* The moment a name was minted, or null.
*
* ⛔ Returns null for a legacy clock name rather than parsing it. Records written before this change
* are live on three machines and `record-store.js` treats a name as an opaque path segment, so the
* two formats coexist and nothing needs migrating — but a caller must be told which it is holding
* rather than handed a number that means something different.
*/
export function nameTime (s) {
if (!isRecordName(s)) return null
let n = 0n
for (const ch of s.slice(0, TIME_LEN)) {
const v = ALPHABET.indexOf(ch)
if (v < 0) return null
n = n * 32n + BigInt(v)
}
return Number(n)
}
export default { recordName, isRecordName, nameTime, NAME_LEN }