// uri.js — the `favela://` scheme.
//
// ⛔ WHY THIS FILE EXISTS. `isMention` was `/^(favela:\/\/|did:)/` — it matched ANY favela URI. The
// blob form was noticed and worked around by ORDERING: dialect.js:43 says "every caller must test
// for a blob FIRST — otherwise an embed renders as a mention span and the picture silently
// disappears". The RECORD form was never specified, so nobody worked around it, and
// `favela://<x>/<collection>/<name>` — which favela-surface already emits for a quote
// (router.js:291) — was classified as a mention of a user. A quote rendered as a mention of nobody.
//
// A scheme with three forms needs a parse, not a chain of predicates in the right order.
//
// ── The authority (hyper-social #61 §12 #1) ───────────────────────────────────────────────────
//
// The authority was TWO things at once. A mention carries an IDENTITY ROOT (dialect.js:37,
// favela-peer/lib/mentions.js:116). The quote path carries a FEED KEY. Both are 64 hex characters,
// so no reader could tell which it had.
//
// ★ IT IS THE IDENTITY ROOT. One space, everywhere:
//
// 1. The mention form already uses it, and those records are written. Changing that would move
// every existing mention.
// 2. `NAMING.md` §3 — a name resolves to an identity ROOT, and only then to a room key.
// 3. hyper-social #64 §1 — handle claims anchor to the root. pgh shipped the device-key version
// first and needed `pgh names reanchor --all --force` to repair it; favela stored something
// worse still (a manifest-derived writer key, which can never sign).
// 4. #61 §2a — deriving an address from a rotatable locator is the exact failure `favela://`
// exists to prevent. `roomKey = hash(NS('room-key') || did)` broke because a DID can rotate.
//
// A FEED KEY IS A LOCATOR, not an identity: it says where bytes are, and #61 §12 #3 leaves oplog
// stability under `setLocal` writer rotation unverified. The root says who. Same split as a record's
// name versus its oplog — identity is not the same question as where to fetch.
//
// Resolution root → feed already exists: `profiles()` returns feed → root for feeds you hold, and a
// handle claim binds root → feed and REPLICATES, which is the namespace layer's whole job.
//
// ⚠ PERMISSIVE WHEN READING, STRICT WHEN WRITING. `parseUri` does not police the authority's shape,
// because it must read bytes already written — including a conformance vector that mentions
// `favela://abc123`. The 64-hex requirement is enforced in `recordUri`, where we mint.
const PREFIX = 'favela://'
const HEX64 = /^[0-9a-f]{64}$/i
const OID = /^[0-9a-f]{8,128}$/i
// One path segment, matching favela-peer's `isSafeSeg`: no separator, not a dot entry.
const isSeg = (s) => typeof s === 'string' && s.length > 0 && !s.includes('/') && s !== '.' && s !== '..'
/**
* Parse a favela URI into exactly one of three kinds, or null.
*
* ⛔ BY SHAPE, never by call order:
* favela://blob/<oid> → blob (authority is the literal "blob")
* favela://<root> → user (no path)
* favela://<root>/<collection>/<name> → record (two path segments)
*
* Anything else is null. A URI we cannot classify must not be guessed at — rendering an
* unrecognised form as the nearest familiar one is how the picture disappeared.
*/
export function parseUri (uri) {
const s = String(uri == null ? '' : uri)
if (!s.toLowerCase().startsWith(PREFIX)) return null
const rest = s.slice(PREFIX.length)
if (!rest) return null
const parts = rest.split('/')
if (parts[0].toLowerCase() === 'blob') {
if (parts.length !== 2 || !OID.test(parts[1])) return null
return { kind: 'blob', root: null, collection: null, name: null, oid: parts[1].toLowerCase() }
}
if (parts.length === 1) {
return { kind: 'user', root: parts[0], collection: null, name: null, oid: null }
}
if (parts.length === 3) {
if (!isSeg(parts[1]) || !isSeg(parts[2])) return null
return { kind: 'record', root: parts[0], collection: parts[1], name: parts[2], oid: null }
}
return null
}
export const isUserUri = (uri) => { const u = parseUri(uri); return !!u && u.kind === 'user' }
export const isRecordUri = (uri) => { const u = parseUri(uri); return !!u && u.kind === 'record' }
export const isBlobUri = (uri) => { const u = parseUri(uri); return !!u && u.kind === 'blob' }
export const userUri = (root) => PREFIX + String(root)
export const blobUri = (oid) => PREFIX + 'blob/' + String(oid).toLowerCase()
/**
* Mint a record URI.
*
* ⛔ STRICT ABOUT THE AUTHORITY, deliberately. This is the door a feed key would otherwise walk
* through — favela-surface's quote path was putting one here — and once a URI is written into a
* record's bytes it cannot be corrected, because SOCIAL-MODEL §8 forbids rewriting a record you did
* not author. Refusing at mint time is the only place this can be enforced.
*/
export function recordUri ({ root, collection, name }) {
if (!HEX64.test(String(root || ''))) {
throw new Error('recordUri: the authority must be a 64-hex identity root, not a feed key')
}
if (!isSeg(collection) || !isSeg(name)) {
throw new Error('recordUri: collection and name must each be one path segment')
}
return PREFIX + String(root).toLowerCase() + '/' + collection + '/' + name
}
export default { parseUri, isUserUri, isRecordUri, isBlobUri, userUri, recordUri, blobUri }