// The firehose frame's `time` must be REAL, not derived from the rev.
//
// serve-membrane used to recompute `time` from the rev (revTime) so every device projected a
// byte-identical frame. That worked while revs were wall-clock TIDs. Revs are now a deterministic
// prev+1µs chain, so a derived time is pinned at the chain's ORIGIN and recedes from reality forever:
// on a chain whose head was minted 30 days ago, five more commits still derive a time 30 days stale.
// Relays read `time` as liveness metadata, and this fleet already fights non-archival relays dropping
// accounts that look stale — so a permanently-receding timestamp is a federation risk, not a nit.
//
// Fix: the SIGNER records the real emission time into the shared __commit__ log, and the membrane
// projects that recorded fact. Real AND identical on every device (all read one replicated value).
// Frames written before the field existed fall back to revTime — correct for those, since their revs
// WERE wall-clock TIDs. `time` is frame metadata only, never part of the signed commit.
//
// Run: bare test/firehose-time.test.js
import '../lib/bare-globals.js'
import test from 'brittle'
import b4a from 'b4a'
import { SwarmDevice } from '../lib/swarm-device.js'
import { revTime } from 'atproto-room/lib/serve-membrane.js'
import { nextRev, tidMicros } from 'atproto-store/lib/tid.js'
const DID = 'did:plc:firehosetime222222222222'
const KEY = (() => { const k = b4a.alloc(32); k[31] = 11; return k })()
const POST = 'app.bsky.feed.post'
const post = (t) => ({ $type: POST, text: t, createdAt: '2026-07-24T00:00:00.000Z' })
const S32 = '234567abcdefghijklmnopqrstuvwxyz'
const tidFor = (ms) => { let n = (BigInt(ms) * 1000n) << 10n; let s = ''; for (let i = 0; i < 13; i++) { s = S32[Number(n % 32n)] + s; n /= 32n } return s }
function memKV () {
const m = new Map()
return { map: m,
base: { writable: true },
async set (k, v) { m.set(k, v) }, async del (k) { m.delete(k) },
async get (k) { return m.has(k) ? m.get(k) : null }, async getNow (k) { return this.get(k) },
async list (p = '') { const o = {}; for (const [k, v] of m) if (k.startsWith(p)) o[k] = v; return o },
async listNow (p = '') { return this.list(p) },
holdsSigner: () => true }
}
const frames = (sb) => [...sb.map.keys()].filter((k) => k.startsWith('__commit__/')).sort().map((k) => sb.map.get(k))
test('a signed #commit frame records the REAL emission time', async (t) => {
const sb = memKV()
const A = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'A', firehose: true })
const before = Date.now()
await A.putRecord(POST, '3a', post('a')); await A.sign()
await A.putRecord(POST, '3b', post('b')); await A.sign()
const after = Date.now()
const fs = frames(sb)
t.is(fs.length, 2, 'two frames emitted')
for (const f of fs) {
t.ok(typeof f.time === 'string' && !Number.isNaN(Date.parse(f.time)), `frame ${f.rev} carries a parseable ISO time`)
const ts = Date.parse(f.time)
t.ok(ts >= before - 1000 && ts <= after + 1000, `frame ${f.rev} time is the real emission instant, not derived`)
}
})
test('on an OLD chain the recorded time stays current while a derived one would be stale', async (t) => {
const sb = memKV()
const A = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'A', firehose: true })
// An account whose chain head was minted 30 days ago — the state of a live DID after deterministic
// revs ship. Every subsequent rev is prev+1µs, so revTime() can never leave that origin.
//
// The head has to be old in the LOG, not just in memory: sign()'s forward-only gate re-anchors
// this.head to the highest-rev frame it can see, so poking A.head alone is correctly overridden.
// And tidNow() carries a process-global monotonic floor, so an injected old clock can't produce an
// old rev either. So: sign normally, then re-key that frame under a 30-day-old rev.
await A.putRecord(POST, '3a', post('a')); await A.sign()
const oldRev = tidFor(Date.now() - 30 * 86400000)
const [freshKey] = [...sb.map.keys()].filter((k) => k.startsWith('__commit__/'))
const seeded = { ...sb.map.get(freshKey), rev: oldRev }
sb.map.delete(freshKey)
sb.map.set('__commit__/' + oldRev, seeded)
// A fresh device on that log restores the old head, then continues deterministically from it.
const B = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'B', firehose: true })
await B.putRecord(POST, '3b', post('b'))
const h = await B.sign()
t.ok(h && h.rev === nextRev(oldRev), 'the new rev continues deterministically from the 30-day-old head')
const f = frames(sb).find((x) => x.rev === h.rev)
t.ok(f, 'frame for the continued rev exists')
const derivedStaleDays = (Date.now() - Date.parse(revTime(f.rev))) / 86400000
t.ok(derivedStaleDays > 29, `deriving time from the rev would be ~30 days stale (was ${derivedStaleDays.toFixed(1)}d)`)
const recordedStaleSec = (Date.now() - Date.parse(f.time)) / 1000
t.ok(Math.abs(recordedStaleSec) < 60, `the RECORDED time is current (${recordedStaleSec.toFixed(1)}s off)`)
t.ok(tidMicros(f.rev) > 0n, 'rev is still a valid TID')
})
test('a legacy frame with no recorded time falls back to revTime (back-compat)', async (t) => {
const sb = memKV()
const A = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'A', firehose: true })
await A.putRecord(POST, '3a', post('a')); await A.sign()
const f = frames(sb)[0]
const legacy = { ...f }; delete legacy.time // a frame written before the field existed
// This is exactly the expression serve-membrane's projectFirehose uses.
t.is(legacy.time || revTime(legacy.rev), revTime(f.rev), 'projection falls back to the rev-derived time')
t.ok(f.time && (f.time || revTime(f.rev)) === f.time, 'a modern frame prefers its recorded time')
})