OverviewFilesCommitsIssuesDocsReleasesPeers

favela-identity / lib / bridge-claim.js

78 lines · javascript ref:
// bridge-claim.js — the exact bytes a peer signs to claim a name on a bridge.
//
// ⛔ ONE DEFINITION, BECAUSE A MISMATCH IS SILENT. The claimant builds these bytes and the bridge
// rebuilds them; if the two ever disagree by a byte, `readAttested` returns null and the bridge
// reports "proof does not attest that identity" — which reads as a hostile or broken caller, not as
// a version skew. There is no error that says "we disagree about the format". So the format lives in
// the one package both sides already depend on, next to the verifier that consumes it, rather than
// being written twice and eventually written wrong.
//
// ⛔ VERSIONED AND PREFIXED. The prefix keeps a claim proof from ever verifying as a release proof —
// releasing BURNS a name permanently, and a proof that could be repurposed between the two would let
// a captured claim destroy what it was meant to create. The version means what is bound can change
// later without both shapes being silently accepted at once.
//
// ⚠ Colons are safe separators here and that is a property of the inputs, not an accident: a bridge
// username cannot contain one (it must be a usable path segment), and nonces and feed keys are hex.

import b4a from 'b4a'

export const CLAIM = 'favela-bridge:v1:claim'
export const RELEASE = 'favela-bridge:v1:release'
// ⛔ A THIRD PREFIX, not a reuse of CLAIM. Asking a bridge to CARRY an identity is a different act
// from taking a NAME on one, and a proof that could serve as either would let a request for one be
// replayed as a request for the other.
export const FRONT = 'favela-bridge:v1:front'
// ⛔ A FOURTH PREFIX. Collecting is a READ of what a bridge holds for a name, not an act on the name
// itself, and a proof that could serve as either would let a request to read someone's inbound graph
// be replayed as a request to BURN their actor. Each act gets its own prefix for that reason.
export const COLLECT = 'favela-bridge:v1:collect'

/**
 * What a claimant signs. The REQUEST is inside it, not just the nonce: a proof covering only the
 * challenge could be captured and replayed to claim a DIFFERENT name under the signer's root —
 * consent proven, control proven, and the wrong name registered.
 */
export function claimBytes ({ nonce, username, feedKey }) {
  return b4a.from([CLAIM, nonce, username, feedKey || ''].join(':'), 'utf8')
}

/** What a releaser signs. No feed: giving up a name says nothing about where a feed lives. */
export function releaseBytes ({ nonce, username }) {
  return b4a.from([RELEASE, nonce, username].join(':'), 'utf8')
}

/**
 * What a peer signs to ask a bridge to carry an identity it holds.
 *
 * ⚠ The identifier is opaque here on purpose — this package does not know what network it names, and
 * the bridge that verifies this is the only thing that needs to.
 */
export function frontBytes ({ nonce, id }) {
  return b4a.from([FRONT, nonce, id].join(':'), 'utf8')
}

/**
 * What a peer signs to collect the observations a bridge holds for its name.
 *
 * ⛔ THE CURSOR IS INSIDE THE BYTES, for the same reason the username is. A proof covering only the
 * challenge could be lifted in flight and resubmitted with `since` wound back to 0, turning a routine
 * incremental poll into a demand for the whole buffer. Binding it costs one concatenation.
 *
 * ⚠ NORMALISED HERE, not by the callers. The claimant has a number and the bridge has whatever came
 * off the wire — JSON will hand it back as a number, but `0` and `'0'` and `undefined` must all
 * produce the SAME bytes or the two sides disagree and the only symptom is "proof does not attest
 * that identity", which reads as a hostile caller rather than a format skew.
 */
export function collectBytes ({ nonce, username, since }) {
  return b4a.from([COLLECT, nonce, username, cursor(since)].join(':'), 'utf8')
}

/** One definition of what a cursor looks like as text. Non-finite, negative and absent all mean 0. */
export function cursor (since) {
  const n = Math.floor(Number(since))
  return String(Number.isFinite(n) && n > 0 ? n : 0)
}

export default { claimBytes, releaseBytes, frontBytes, collectBytes, cursor, CLAIM, RELEASE, FRONT, COLLECT }