release(v4.8.0): sender-fingerprint attribution + Inbox.start race fix
Two unblocking changes for first-contact flows.
Sender attribution: relay captures shortHash(senderSigningKey) at
PUT time (after signature verification, no new trust surface) and
surfaces it on bridge push (IncomingMessage.from) + inbox-fetch
(FetchedBlob.from) + DecryptHandler raw arg. Apps receiving a prekey
envelope from a never-before-seen peer can now bootstrap X3DH via
shade.receive('fp:<hex>', env) — pre-4.8 the wire envelope didn't
authenticate the sender and there was no out-of-band hint to use.
Idempotent ALTER TABLE migrations for SQLite + Postgres add a
sender_fp TEXT column; legacy rows surface as from=undefined
(inter-version compat).
Inbox.start() race: pre-4.8 start() called register() fire-and-forget
AND schedulePoll(0) synchronously, so the first poll on a fresh
address often beat the register HTTP RTT and got SHADE_NOT_FOUND.
start() now defers; register() success kicks schedulePoll(0). Manual
tick() is unaffected (deliberate user action, no gating).
Both reported by Prism. Tests cover all five acceptance criteria
from the sender-attribution request (PUT capture, bridge surface,
fetch surface, inter-version compat, end-to-end pair smoke) plus
the three from the race-fix request.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -867,3 +867,87 @@ describe('PresenceBridge — subscribe to remote presence changes', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── V4.8 — sender-fingerprint propagation on bridge push ──────────
|
||||
|
||||
describe('Sender attribution — bridge push surfaces IncomingMessage.from', () => {
|
||||
test('SSE push carries from = 8-byte hex of SHA-256(senderSigningKey)', async () => {
|
||||
const h = await bootstrap();
|
||||
try {
|
||||
const received: IncomingMessage[] = [];
|
||||
const bridge = new SseBridge({
|
||||
baseUrl: h.baseUrl,
|
||||
auth: bobAuth(h),
|
||||
initialBackoffMs: 50,
|
||||
maxBackoffMs: 200,
|
||||
disableAutoReconnect: true,
|
||||
});
|
||||
await bridge.connect({ onMessage: (m) => received.push(m) });
|
||||
try {
|
||||
await putBlob(h, rand(48));
|
||||
await waitFor(() => received.length === 1, 5_000);
|
||||
const fp = received[0]!.from;
|
||||
expect(fp).toMatch(/^[0-9a-f]{16}$/);
|
||||
const digest = await globalThis.crypto.subtle.digest(
|
||||
'SHA-256',
|
||||
h.alice.signingPublicKey as unknown as ArrayBuffer,
|
||||
);
|
||||
const expected = Array.from(new Uint8Array(digest).slice(0, 8), (b) =>
|
||||
b.toString(16).padStart(2, '0'),
|
||||
).join('');
|
||||
expect(fp).toBe(expected);
|
||||
} finally {
|
||||
await bridge.disconnect();
|
||||
}
|
||||
} finally {
|
||||
h.server.stop(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('WS push carries from likewise', async () => {
|
||||
const h = await bootstrap();
|
||||
try {
|
||||
const received: IncomingMessage[] = [];
|
||||
const bridge = new WsBridge({
|
||||
baseUrl: h.baseUrl,
|
||||
auth: bobAuth(h),
|
||||
connectTimeoutMs: 2_000,
|
||||
disableAutoReconnect: true,
|
||||
});
|
||||
await bridge.connect({ onMessage: (m) => received.push(m) });
|
||||
try {
|
||||
await putBlob(h, rand(48));
|
||||
await waitFor(() => received.length === 1, 5_000);
|
||||
expect(received[0]!.from).toMatch(/^[0-9a-f]{16}$/);
|
||||
} finally {
|
||||
await bridge.disconnect();
|
||||
}
|
||||
} finally {
|
||||
h.server.stop(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('long-poll push carries from likewise', async () => {
|
||||
const h = await bootstrap();
|
||||
try {
|
||||
const received: IncomingMessage[] = [];
|
||||
const bridge = new LongPollBridge({
|
||||
baseUrl: h.baseUrl,
|
||||
auth: bobAuth(h),
|
||||
pollTimeoutMs: 500,
|
||||
requestTimeoutMs: 1_500,
|
||||
errorBackoffMs: 50,
|
||||
});
|
||||
await bridge.connect({ onMessage: (m) => received.push(m) });
|
||||
try {
|
||||
await putBlob(h, rand(48));
|
||||
await waitFor(() => received.length === 1, 5_000);
|
||||
expect(received[0]!.from).toMatch(/^[0-9a-f]{16}$/);
|
||||
} finally {
|
||||
await bridge.disconnect();
|
||||
}
|
||||
} finally {
|
||||
h.server.stop(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user