// deliver.test.js — the delivery path had NO tests, and it is the one module that holds key
// material and talks to the open internet.
//
// ⛔ THE PROPERTY UNDER TEST: `deliver` never sees a key. It asks a Signer.
//
// signer.js says the custody seam is the point of M2 and that "everything above it
// (http-signature, deliver) depends only on the interface". That was not true — `createDeliverer`
// took a raw `privateKey` and `signRequest` was synchronous, so a RemoteSigner could not be wired
// in even once its transport existed. These tests pin the interface so it cannot drift back.
import test from 'brittle'
import b4a from 'b4a'
import { createDeliverer } from '../lib/deliver.js'
const INBOX = 'https://remote.example/users/bob/inbox'
const ACTIVITY = { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Create', id: 'https://ap.7rnx.net/ap/activities/1' }
const SIG = b4a.from('a-signature-nobody-can-forge')
// Records every request and every signing call. Nothing here touches the network or a real key.
function harness ({ status = 202, signer = null, sleep = async () => {} } = {}) {
const sent = []
const s = signer || stubSigner()
const deliver = createDeliverer({
signer: s,
doFetch: async (url, opts) => {
sent.push({ url, ...opts })
return { ok: status >= 200 && status < 300, status, text: async () => '' }
},
log: () => {},
sleep
})
return { deliver, sent, signer: s }
}
function stubSigner ({ keyId = 'https://ap.7rnx.net/ap/actor#main-key', sig = SIG, delay = 0 } = {}) {
const calls = []
return {
keyId,
calls,
async sign (bytes) {
calls.push(b4a.toString(bytes))
if (delay) await new Promise((r) => setTimeout(r, delay))
return sig
}
}
}
test('★★ delivery asks the SIGNER — no key material reaches deliver.js', async (t) => {
const { deliver, sent, signer } = harness()
const res = await deliver({ to: 'https://remote.example/users/bob', inbox: INBOX, activity: ACTIVITY })
t.is(res.ok, true, 'delivered')
t.is(signer.calls.length, 1, 'the signer was asked exactly once')
const sig = sent[0].headers.signature
t.ok(sig.includes(`keyId="${signer.keyId}"`), 'keyId comes from the SIGNER, not from a separate argument')
t.ok(sig.includes(`signature="${b4a.toString(SIG, 'base64')}"`), 'the bytes the signer returned are what goes on the wire')
})
// ⛔ The failure this catches is invisible in our logs and fatal at the receiver: if the bytes we
// sign are not the bytes the headers describe, every remote server rejects us with "invalid
// signature" and nothing on our side knows why. http-signature.js:15 names exactly this.
test('★★ the bytes handed to the signer ARE the request that goes out', async (t) => {
const { deliver, sent, signer } = harness()
await deliver({ to: 'https://remote.example/users/bob', inbox: INBOX, activity: ACTIVITY })
const h = sent[0].headers
const expected = [
'(request-target): post /users/bob/inbox',
`host: ${h.host}`,
`date: ${h.date}`,
`digest: ${h.digest}`
].join('\n')
t.is(signer.calls[0], expected, 'signing string matches the headers actually sent')
t.ok(h.digest.startsWith('SHA-256='), 'digest is RFC 3230 shaped')
})
// The old path called a SYNCHRONOUS rsaSign inline, so a signer that has to cross a socket — which
// is every remote custody option — structurally could not be used.
test('★ an ASYNC signer works, which is the whole point', async (t) => {
const slow = stubSigner({ delay: 20 })
const { deliver, sent } = harness({ signer: slow })
await deliver({ to: 'https://remote.example/users/bob', inbox: INBOX, activity: ACTIVITY })
t.is(slow.calls.length, 1)
t.ok(sent[0].headers.signature.includes('signature='), 'the delivery waited for the signature')
})
test('⛔ a deliverer cannot be built without a signer', async (t) => {
t.exception(() => createDeliverer({ log: () => {} }), /signer/i,
'passing a raw key is no longer a thing you can do by accident')
})
// A signer that returns nothing produces a signature no remote server can verify, and the only
// symptom is a rejection that looks like our crypto is broken. signer.js makes the same argument.
test('⛔ a signer that returns nothing fails LOUDLY', async (t) => {
const empty = { keyId: 'k', async sign () { return null } }
const { deliver } = harness({ signer: empty })
await t.exception(
deliver({ to: 'https://remote.example/users/bob', inbox: INBOX, activity: ACTIVITY }),
/signature/i
)
})
// ⛔ A signature covers `date`, and receivers reject stale dates (ours allows 300s, config.js:30).
// So a retry minutes later CANNOT reuse the first signature — each attempt must be signed afresh.
test('★ every retry is signed again, because the signature covers the date', async (t) => {
const { deliver, signer } = harness({ status: 500 })
await t.exception(deliver({ to: 'https://remote.example/users/bob', inbox: INBOX, activity: ACTIVITY }))
t.is(signer.calls.length, 4, 'one signing call per attempt (initial + 3 retries)')
})
// PerUser exists and nothing constructs it (hyper-social #64 §8, #65 §7). One bridge fronting many
// people means one signer per person, so the signer has to be selectable per delivery.
test('★ a per-delivery signer overrides the default', async (t) => {
const alice = stubSigner({ keyId: 'https://ap.7rnx.net/ap/users/alice#main-key' })
const { deliver, sent, signer: primary } = harness()
await deliver({ to: 'https://remote.example/users/bob', inbox: INBOX, activity: ACTIVITY, signer: alice })
t.is(alice.calls.length, 1, 'the override signed it')
t.is(primary.calls.length, 0, 'the default did not')
t.ok(sent[0].headers.signature.includes('alice#main-key'), 'and the wire carries her keyId')
})