import test from 'brittle'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import {
NotifyService,
NOTIFY_DOMAINS,
notifySignaturePayload,
verifyNotifySignature
} from '../../packages/services/builtin/notify-service.js'
import { OutboxLogApp } from '../../packages/services/builtin/outboxlog/index.js'
const NOW = 1782864000000
const HOUR = 60 * 60 * 1000
test('notify service: signed direct wake path stores redacted delivery event', async (t) => {
const relay = keyPair(1)
const user = keyPair(2)
const device = keyPair(3)
const sender = keyPair(4)
const provider = {
attempts: [],
async send (delivery) {
this.attempts.push(delivery)
return { status: 'accepted_by_provider', providerStatus: 'memory-ok' }
}
}
const notify = new NotifyService({ keyPair: relay, provider, clock: () => NOW })
await installHappyPath(notify, { relay, user, device, sender })
const intent = signed(sender, NOTIFY_DOMAINS.intent, {
type: 'hiverelay.notify.intent.v1',
intentId: hex(9),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
receiver: device.hex,
sender: sender.hex,
channel: 'message',
urgency: 'normal',
ttlSeconds: 3600,
collapseKey: 'thread-hash',
dedupeKey: 'event-hash-1',
createdAt: NOW,
payloadCiphertext: 'ciphertext',
payloadEncoding: 'hiverelay.notify.payload.box.v1',
privacyProfile: 'generic'
})
const sent = await notify.send(intent)
t.is(sent.ok, true)
t.is(sent.status, 'accepted_by_provider')
t.is(provider.attempts.length, 1)
t.is(provider.attempts[0].providerTokenCiphertext, 'encrypted-provider-token')
const events = await notify['delivery-event'](signed(device, NOTIFY_DOMAINS.deliveryEventRequest, { intentId: intent.intentId, device: device.hex }))
t.is(events.count, 1)
t.is(events.events[0].status, 'accepted_by_provider')
t.is(events.events[0].providerStatus, 'memory-ok')
t.absent(JSON.stringify(events.events[0]).includes('encrypted-provider-token'))
t.ok(verifyNotifySignature(events.events[0], NOTIFY_DOMAINS.deliveryEvent, relay.hex))
})
test('notify service: abuse limits are configurable and zero disables a scope (quota_exhausted)', async (t) => {
const relay = keyPair(1)
const user = keyPair(2)
const device = keyPair(3)
const sender = keyPair(4)
const provider = { attempts: [], async send () { this.attempts.push(1); return { status: 'accepted_by_provider' } } }
const notify = new NotifyService({
keyPair: relay,
provider,
clock: () => NOW,
abuseLimits: { device: { perHour: 0, burst: 0 } }
})
await installHappyPath(notify, { relay, user, device, sender })
const blocked = await notify.send(signed(sender, NOTIFY_DOMAINS.intent, {
type: 'hiverelay.notify.intent.v1',
intentId: hex(9),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
receiver: device.hex,
sender: sender.hex,
channel: 'message',
urgency: 'normal',
ttlSeconds: 3600,
createdAt: NOW,
payloadCiphertext: 'ciphertext',
privacyProfile: 'generic'
}))
t.is(blocked.ok, false)
t.is(blocked.status, 'rate_limited')
t.is(blocked.reason, 'quota_exhausted')
t.is(provider.attempts.length, 0, 'no egress when a scope is disabled')
})
test('notify service: replay guard is durable before provider egress (no double-send after crash)', async (t) => {
const dir = await mkdtemp(join(tmpdir(), 'notify-replay-'))
t.teardown(() => rm(dir, { recursive: true, force: true }))
const relay = keyPair(1)
const user = keyPair(2)
const device = keyPair(3)
const sender = keyPair(4)
const intent = () => signed(sender, NOTIFY_DOMAINS.intent, {
type: 'hiverelay.notify.intent.v1',
intentId: hex(9),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
receiver: device.hex,
sender: sender.hex,
channel: 'message',
urgency: 'normal',
ttlSeconds: 3600,
createdAt: NOW,
payloadCiphertext: 'ciphertext',
privacyProfile: 'generic'
})
// "Crash" mid-egress: the provider throws after the durability barrier.
const crashing = { async send () { throw new Error('socket died mid-egress') } }
const first = new NotifyService({ keyPair: relay, provider: crashing, clock: () => NOW })
await first.start({ config: { storage: dir } })
await installHappyPath(first, { relay, user, device, sender })
const attempted = await first.send(intent())
// provider_attempted is ok:true by contract — the push MAY have left; only
// the metering (billable:false) records that the relay cannot prove it.
t.is(attempted.ok, true)
t.is(attempted.status, 'provider_attempted', 'throw is recorded, not lost')
t.is(attempted.reason, 'provider_exception')
t.is(attempted.event.metering.billable, false, 'unknown outcome is not billable')
await first.stop()
// Restart from disk: the same intent must land on the replay guard, not the
// provider — replay/dedupe were persisted BEFORE egress.
const restoredProvider = { attempts: [], async send () { this.attempts.push(1); return { status: 'accepted_by_provider' } } }
const second = new NotifyService({ keyPair: relay, provider: restoredProvider, clock: () => NOW })
await second.start({ config: { storage: dir } })
const replayed = await second.send(intent())
t.is(replayed.ok, false)
t.is(replayed.reason, 'replay', 'restart rejects the replayed intent')
t.is(restoredProvider.attempts.length, 0, 'no double egress after restart')
await second.stop()
})
test('notify service: hot-path persistence is debounced, stop() flushes the final state (#144)', async (t) => {
const relay = keyPair(1)
const user = keyPair(2)
const device = keyPair(3)
const sender = keyPair(4)
let saves = 0
let lastSnapshot = null
const persistence = {
async load () { return null },
async save (snapshot) { saves++; lastSnapshot = snapshot }
}
const notify = new NotifyService({
keyPair: relay,
provider: { async send () { return { status: 'accepted_by_provider' } } },
clock: () => NOW,
persistence,
persistFlushMs: 60000 // park the debounce so only explicit barriers write
})
await notify.start({})
await installHappyPath(notify, { relay, user, device, sender })
const savesAfterSetup = saves
// A rejection flood (unknown caps -> rejected_by_relay) must not write
// per-intent: each rejection only schedules the debounced flush.
for (let i = 0; i < 50; i++) {
await notify.send(signed(sender, NOTIFY_DOMAINS.intent, {
type: 'hiverelay.notify.intent.v1',
intentId: hex(60 + (i % 100)),
receiveCap: hex(41), // no such cap
sendCap: hex(42),
app: hex(5),
receiver: device.hex,
sender: sender.hex,
channel: 'message',
urgency: 'normal',
ttlSeconds: 3600,
createdAt: NOW,
payloadCiphertext: 'ciphertext',
privacyProfile: 'generic'
}))
}
t.is(saves, savesAfterSetup, '50 rejections scheduled zero synchronous snapshot writes')
await notify.stop()
t.ok(saves > savesAfterSetup, 'stop() flushed the pending state')
t.is(lastSnapshot.deliveryEvents.length, 50, 'flushed snapshot carries every rejection event')
})
test('notify service: delivery-event requires a device-signed request (no cross-tenant IDOR)', async (t) => {
const relay = keyPair(1)
const user = keyPair(2)
const device = keyPair(3)
const sender = keyPair(4)
const attacker = keyPair(88)
const provider = { attempts: [], async send () { return { status: 'accepted_by_provider' } } }
const notify = new NotifyService({ keyPair: relay, provider, clock: () => NOW })
await installHappyPath(notify, { relay, user, device, sender })
const intent = signed(sender, NOTIFY_DOMAINS.intent, {
type: 'hiverelay.notify.intent.v1',
intentId: hex(9),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
receiver: device.hex,
sender: sender.hex,
channel: 'message',
urgency: 'normal',
ttlSeconds: 3600,
createdAt: NOW,
payloadCiphertext: 'ciphertext',
privacyProfile: 'generic'
})
const sent = await notify.send(intent)
t.is(sent.ok, true)
// Unsigned request is rejected outright.
await t.exception(notify['delivery-event']({ intentId: intent.intentId, device: device.hex }), /BAD_SIGNATURE/)
// A request with no device field cannot prove caller scope.
await t.exception(notify['delivery-event'](signed(device, NOTIFY_DOMAINS.deliveryEventRequest, { intentId: intent.intentId })), /BAD_SIGNATURE/)
// Attacker names the victim's device but signs with their own key: the
// signature fails to verify against the claimed device key -> rejected.
await t.exception(notify['delivery-event'](signed(attacker, NOTIFY_DOMAINS.deliveryEventRequest, { intentId: intent.intentId, device: device.hex })), /BAD_SIGNATURE/)
// Attacker validly proves control of their OWN device, but the victim's
// event.device != attacker -> reads nothing (no metadata leak).
const leaked = await notify['delivery-event'](signed(attacker, NOTIFY_DOMAINS.deliveryEventRequest, { intentId: intent.intentId, device: attacker.hex }))
t.is(leaked.count, 0)
// The legitimate device still reads its own event.
const ok = await notify['delivery-event'](signed(device, NOTIFY_DOMAINS.deliveryEventRequest, { intentId: intent.intentId, device: device.hex }))
t.is(ok.count, 1)
})
test('notify service: sender without matching SendCap is rejected before provider attempt', async (t) => {
const relay = keyPair(11)
const user = keyPair(12)
const device = keyPair(13)
const sender = keyPair(14)
const imposter = keyPair(15)
const provider = { attempts: [], async send (delivery) { this.attempts.push(delivery); return { status: 'accepted_by_provider' } } }
const notify = new NotifyService({ keyPair: relay, provider, clock: () => NOW })
await installHappyPath(notify, { relay, user, device, sender })
const intent = signed(imposter, NOTIFY_DOMAINS.intent, {
type: 'hiverelay.notify.intent.v1',
intentId: hex(19),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
receiver: device.hex,
sender: imposter.hex,
channel: 'message',
urgency: 'normal',
ttlSeconds: 3600,
createdAt: NOW,
payloadCiphertext: 'ciphertext',
privacyProfile: 'generic'
})
const sent = await notify.send(intent)
t.is(sent.ok, false)
t.is(sent.reason, 'send_cap_missing')
t.is(provider.attempts.length, 0)
})
test('notify service: revoked send capability suppresses provider egress', async (t) => {
const relay = keyPair(21)
const user = keyPair(22)
const device = keyPair(23)
const sender = keyPair(24)
const provider = { attempts: [], async send (delivery) { this.attempts.push(delivery); return { status: 'accepted_by_provider' } } }
const notify = new NotifyService({ keyPair: relay, provider, clock: () => NOW })
await installHappyPath(notify, { relay, user, device, sender })
const revoke = signed(user, NOTIFY_DOMAINS.revoke, {
type: 'hiverelay.notify.revocation.v1',
target: hex(7),
user: user.hex,
app: hex(5),
audience: relay.hex,
scope: 'send-cap',
createdAt: NOW,
nonce: 'c'.repeat(32)
})
await notify.revoke(revoke)
const intent = signed(sender, NOTIFY_DOMAINS.intent, {
type: 'hiverelay.notify.intent.v1',
intentId: hex(29),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
receiver: device.hex,
sender: sender.hex,
channel: 'message',
urgency: 'normal',
ttlSeconds: 3600,
createdAt: NOW,
payloadCiphertext: 'ciphertext',
privacyProfile: 'generic'
})
const sent = await notify.send(intent)
t.is(sent.ok, false)
t.is(sent.reason, 'cap_revoked')
t.is(provider.attempts.length, 0)
})
test('notify service: watch mode is opaque and requires receive/send caps', async (t) => {
const relay = keyPair(31)
const user = keyPair(32)
const device = keyPair(33)
const sender = keyPair(34)
const notify = new NotifyService({ keyPair: relay, clock: () => NOW })
notify.attachWatchSource('hypercore-head', () => () => {})
await installHappyPath(notify, { relay, user, device, sender, modes: ['direct', 'watch'] })
const watch = signed(user, NOTIFY_DOMAINS.watch, {
type: 'hiverelay.notify.watch.v1',
watchId: hex(39),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
audience: relay.hex,
source: {
kind: 'hypercore-head',
key: hex(40),
start: 42
},
channel: 'message',
policy: { minIntervalSeconds: 60 },
createdAt: NOW,
expiresAt: NOW + HOUR
})
const installed = await notify.watch(watch)
t.is(installed.ok, true)
t.alike(installed.source, { kind: 'hypercore-head', key: hex(40), start: 42 })
const status = await notify.status({ app: hex(5), device: device.hex })
t.is(status.counts.watches, 1)
t.is(status.privacy.plaintextAllowed, false)
})
test('notify service: watch requires an attached observer for the source kind (#142)', async (t) => {
const relay = keyPair(31)
const user = keyPair(32)
const device = keyPair(33)
const sender = keyPair(34)
const notify = new NotifyService({ keyPair: relay, clock: () => NOW })
await installHappyPath(notify, { relay, user, device, sender, modes: ['direct', 'watch'] })
// No observer attached for 'notify-feed-head': the watch is rejected
// instead of being accepted (and billed) as a dead registration.
await t.exception(notify.watch(signed(user, NOTIFY_DOMAINS.watch, {
type: 'hiverelay.notify.watch.v1',
watchId: hex(39),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
audience: relay.hex,
source: { kind: 'notify-feed-head', key: hex(40), start: 0 },
channel: 'message',
policy: { minIntervalSeconds: 30 },
createdAt: NOW,
expiresAt: NOW + HOUR
})), /SOURCE_UNAVAILABLE/)
})
test('notify service: notify-feed-head watch composes with outboxlog and coalesces wakes (#142/#145)', async (t) => {
let now = NOW
const relay = keyPair(31)
const user = keyPair(32)
const device = keyPair(33)
const sender = keyPair(34)
const writerApp = hex(40) // the outbox appId being watched
const provider = { attempts: [], async send (delivery) { this.attempts.push(delivery); return { status: 'accepted_by_provider' } } }
const outbox = new OutboxLogApp({ verifyAppend: () => true, persistence: false })
const notify = new NotifyService({ keyPair: relay, provider, clock: () => now })
// Same composition the relay wires at startup: feed-head watches observe
// the outbox's signed head row through the engine's own subscribe().
notify.attachWatchSource('notify-feed-head', (source, onChange) => {
return outbox.subscribe(source.key, {}, (event) => {
if (event && event.key === 'head!' + source.key && !event.replay) onChange(event)
})
})
await installHappyPath(notify, { relay, user, device, sender, modes: ['direct', 'watch'] })
const installed = await notify.watch(signed(user, NOTIFY_DOMAINS.watch, {
type: 'hiverelay.notify.watch.v1',
watchId: hex(39),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
audience: relay.hex,
source: { kind: 'notify-feed-head', key: writerApp, start: 0 },
channel: 'message',
policy: { minIntervalSeconds: 30 },
createdAt: NOW,
expiresAt: NOW + HOUR
}))
t.is(installed.ok, true)
const tick = () => new Promise(resolve => setImmediate(resolve))
outbox.create({ appId: writerApp })
// A non-head append does not wake anyone.
outbox.append({ appId: writerApp, op: { type: 'post', data: { id: 'p1', body: 'secret-plaintext' } } })
await tick()
t.is(provider.attempts.length, 0, 'non-head appends do not fire the watch')
// Head bump -> one opaque wake: no payload, no plaintext leakage.
outbox.append({ appId: writerApp, op: { type: 'head', data: { id: writerApp, version: 1 } } })
await tick()
t.is(provider.attempts.length, 1, 'head bump wakes the device')
t.is(provider.attempts[0].payloadCiphertext, '', 'wake carries no payload')
t.absent(JSON.stringify(provider.attempts[0]).includes('secret-plaintext'))
const wakeEvents = [...notify.deliveryEvents.values()].filter(e => e.reason === 'watch_wake')
t.is(wakeEvents.length, 1, 'wake recorded as a delivery event')
t.ok(verifyNotifySignature(wakeEvents[0], NOTIFY_DOMAINS.deliveryEvent, relay.hex), 'wake event is relay-signed')
// A second head bump inside minIntervalSeconds coalesces.
outbox.append({ appId: writerApp, op: { type: 'head', data: { id: writerApp, version: 2 } } })
await tick()
t.is(provider.attempts.length, 1, 'wake inside the interval coalesces')
// After the interval the next bump fires again.
now = NOW + 31000
outbox.append({ appId: writerApp, op: { type: 'head', data: { id: writerApp, version: 3 } } })
await tick()
t.is(provider.attempts.length, 2, 'wake fires again after minIntervalSeconds')
// unwatch() detaches the subscription: no further wakes.
await notify.unwatch(signed(user, NOTIFY_DOMAINS.watch, { watchId: hex(39) }))
now = NOW + 120000
outbox.append({ appId: writerApp, op: { type: 'head', data: { id: writerApp, version: 4 } } })
await tick()
t.is(provider.attempts.length, 2, 'no wake after unwatch')
})
test('notify service: revoking the send-cap suppresses watch wakes, not just direct sends (v0.21.1 audit)', async (t) => {
const now = NOW
const relay = keyPair(31)
const user = keyPair(32)
const device = keyPair(33)
const sender = keyPair(34)
const writerApp = hex(40)
const provider = { attempts: [], async send (delivery) { this.attempts.push(delivery); return { status: 'accepted_by_provider' } } }
const outbox = new OutboxLogApp({ verifyAppend: () => true, persistence: false })
const notify = new NotifyService({ keyPair: relay, provider, clock: () => now })
notify.attachWatchSource('notify-feed-head', (source, onChange) => {
return outbox.subscribe(source.key, {}, (event) => {
if (event && event.key === 'head!' + source.key && !event.replay) onChange(event)
})
})
await installHappyPath(notify, { relay, user, device, sender, modes: ['direct', 'watch'] })
await notify.watch(signed(user, NOTIFY_DOMAINS.watch, {
type: 'hiverelay.notify.watch.v1',
watchId: hex(39),
receiveCap: hex(6),
sendCap: hex(7),
app: hex(5),
audience: relay.hex,
source: { kind: 'notify-feed-head', key: writerApp, start: 0 },
channel: 'message',
policy: { minIntervalSeconds: 30 },
createdAt: NOW,
expiresAt: NOW + HOUR
}))
// Revoke the SEND capability the watch was installed under. The direct-send
// path already honored this ('cap_revoked'); the watch-fire path must too,
// otherwise a revoked sender keeps waking the device indefinitely.
await notify.revoke(signed(user, NOTIFY_DOMAINS.revoke, {
type: 'hiverelay.notify.revocation.v1',
target: hex(7),
user: user.hex,
app: hex(5),
audience: relay.hex,
scope: 'send-cap',
createdAt: NOW,
nonce: 'd'.repeat(32)
}))
const tick = () => new Promise(resolve => setImmediate(resolve))
outbox.create({ appId: writerApp })
outbox.append({ appId: writerApp, op: { type: 'head', data: { id: writerApp, version: 1 } } })
await tick()
t.is(provider.attempts.length, 0, 'head bump does not wake a device whose send-cap was revoked')
})
test('notify service: default file persistence restores signed caps, watches, revocations and events', async (t) => {
const dir = await mkdtemp(join(tmpdir(), 'notify-service-'))
t.teardown(() => rm(dir, { recursive: true, force: true }))
const relay = keyPair(41)
const user = keyPair(42)
const device = keyPair(43)
const sender = keyPair(44)
const provider = { attempts: [], async send (delivery) { this.attempts.push(delivery); return { status: 'accepted_by_provider' } } }
const first = new NotifyService({
keyPair: relay,
provider,
clock: () => NOW
})
await first.start({ config: { storage: dir } })
first.attachWatchSource('notify-feed-head', () => () => {})
const ids = await installHappyPath(first, { relay, user, device, sender, modes: ['direct', 'watch'] })
await first.watch(signed(user, NOTIFY_DOMAINS.watch, {
type: 'hiverelay.notify.watch.v1',
watchId: hex(45),
receiveCap: ids.receiveCap,
sendCap: ids.sendCap,
app: ids.app,
audience: relay.hex,
source: {
kind: 'notify-feed-head',
key: hex(46),
start: 7
},
channel: 'message',
policy: { minIntervalSeconds: 30 },
createdAt: NOW,
expiresAt: NOW + HOUR
}))
const intent = signed(sender, NOTIFY_DOMAINS.intent, {
type: 'hiverelay.notify.intent.v1',
intentId: hex(47),
receiveCap: ids.receiveCap,
sendCap: ids.sendCap,
app: ids.app,
receiver: device.hex,
sender: sender.hex,
channel: 'message',
urgency: 'normal',
ttlSeconds: 3600,
createdAt: NOW,
payloadCiphertext: 'ciphertext',
privacyProfile: 'generic'
})
const sent = await first.send(intent)
t.is(sent.ok, true)
await first.revoke(signed(user, NOTIFY_DOMAINS.revoke, {
type: 'hiverelay.notify.revocation.v1',
target: ids.sendCap,
user: user.hex,
app: ids.app,
audience: relay.hex,
scope: 'send-cap',
createdAt: NOW,
nonce: 'f'.repeat(32)
}))
await first.stop()
const restoredProvider = { attempts: [], async send (delivery) { this.attempts.push(delivery); return { status: 'accepted_by_provider' } } }
const second = new NotifyService({
keyPair: relay,
provider: restoredProvider,
clock: () => NOW
})
await second.start({ config: { storage: dir } })
const status = await second.status({ app: ids.app, device: device.hex })
t.is(status.counts.providerBindings, 1)
t.is(status.counts.devices, 1)
t.is(status.counts.receiveCaps, 1)
t.is(status.counts.sendCaps, 1)
t.is(status.counts.watches, 1)
t.is(status.counts.revocations, 1)
t.is(status.counts.deliveryEvents, 1)
const events = await second['delivery-event'](signed(device, NOTIFY_DOMAINS.deliveryEventRequest, { intentId: intent.intentId, device: device.hex }))
t.is(events.count, 1)
t.is(events.events[0].status, 'accepted_by_provider')
t.ok(verifyNotifySignature(events.events[0], NOTIFY_DOMAINS.deliveryEvent, relay.hex))
const blocked = await second.send(signed(sender, NOTIFY_DOMAINS.intent, {
type: 'hiverelay.notify.intent.v1',
intentId: hex(48),
receiveCap: ids.receiveCap,
sendCap: ids.sendCap,
app: ids.app,
receiver: device.hex,
sender: sender.hex,
channel: 'message',
urgency: 'normal',
ttlSeconds: 3600,
createdAt: NOW,
payloadCiphertext: 'ciphertext',
privacyProfile: 'generic'
}))
t.is(blocked.ok, false)
t.is(blocked.reason, 'cap_revoked')
t.is(restoredProvider.attempts.length, 0)
await second.stop()
const third = new NotifyService({
keyPair: relay,
clock: () => NOW
})
await third.start({ config: { storage: dir } })
const blockedEvents = await third['delivery-event'](signed(device, NOTIFY_DOMAINS.deliveryEventRequest, { intentId: blocked.intentId, device: device.hex }))
t.is(blockedEvents.count, 1)
t.is(blockedEvents.events[0].reason, 'cap_revoked')
await third.stop()
})
test('notify service: manifest exposes v1 service capabilities', (t) => {
const notify = new NotifyService()
const manifest = notify.manifest()
t.is(manifest.name, 'notify')
t.ok(manifest.capabilities.includes('bind-provider'))
t.ok(manifest.capabilities.includes('install-receive-cap'))
t.ok(manifest.capabilities.includes('install-send-cap'))
t.ok(manifest.capabilities.includes('delivery-event'))
})
async function installHappyPath (notify, { relay, user, device, sender, modes = ['direct'] }) {
const app = hex(5)
const bindingId = hex(8)
const receiveCap = hex(6)
const sendCap = hex(7)
await notify['bind-provider'](signed(device, NOTIFY_DOMAINS.providerBinding, {
type: 'hiverelay.notify.provider-binding.v1',
bindingId,
audience: relay.hex,
app,
device: device.hex,
mode: 'runtimePush',
provider: 'apns',
platform: 'ios',
scope: { bundle: 'org.hiverelay.runtime' },
credentialMode: 'runtime-owned',
tokenHash: hex(10),
tokenCiphertext: 'encrypted-provider-token',
generation: 1,
createdAt: NOW,
expiresAt: NOW + HOUR,
nonce: 'a'.repeat(32)
}))
await notify['register-device'](signed(device, NOTIFY_DOMAINS.deviceRegistration, {
type: 'hiverelay.notify.device-registration.v1',
audience: relay.hex,
app,
user: user.hex,
device: device.hex,
encryptionKey: hex(12),
bindingId,
createdAt: NOW,
expiresAt: NOW + HOUR,
nonce: 'b'.repeat(32)
}))
await notify['install-receive-cap'](signed(user, NOTIFY_DOMAINS.receiveCap, {
type: 'hiverelay.notify.receive-cap.v1',
capId: receiveCap,
audience: relay.hex,
user: user.hex,
app,
device: device.hex,
bindingId,
tokenHash: hex(10),
channels: ['message'],
modes,
quota: { perHour: 30, burst: 5, maxTtlSeconds: 3600, maxUrgency: 'normal' },
createdAt: NOW,
expiresAt: NOW + HOUR,
nonce: 'd'.repeat(32)
}))
await notify['install-send-cap'](signed(user, NOTIFY_DOMAINS.sendCap, {
type: 'hiverelay.notify.send-cap.v1',
capId: sendCap,
receiveCap,
audience: relay.hex,
app,
device: device.hex,
sender: sender.hex,
channel: 'message',
quota: { perHour: 10, burst: 3, maxTtlSeconds: 3600, maxUrgency: 'normal' },
createdAt: NOW,
expiresAt: NOW + HOUR,
nonce: 'e'.repeat(32)
}))
return { app, bindingId, receiveCap, sendCap }
}
function signed (key, domain, body) {
const signature = b4a.alloc(64)
sodium.crypto_sign_detached(signature, notifySignaturePayload(domain, body), key.secretKey)
return { ...body, signature: b4a.toString(signature, 'hex') }
}
function keyPair (seedByte) {
const publicKey = b4a.alloc(32)
const secretKey = b4a.alloc(64)
sodium.crypto_sign_seed_keypair(publicKey, secretKey, b4a.alloc(32, seedByte))
return {
publicKey,
secretKey,
hex: b4a.toString(publicKey, 'hex')
}
}
function hex (byte) {
return byte.toString(16).padStart(2, '0').repeat(32)
}