import assert from 'node:assert/strict'; import { createECDH, generateKeyPairSync, randomUUID } from 'node:crypto'; import { once } from 'node:events'; import { createServer } from 'node:http'; import { createRequire } from 'node:module'; import test from 'node:test'; import webpush from 'web-push'; import { createMessageText, MAX_PREVIEW_BYTES, previewPayload, previewText, WEB_PUSH_PAYLOAD_BYTES } from '../src/agent-push-preview.js'; import { createPushSender, type ProviderRequest } from '../src/agent-push-transports.js'; const { decrypt } = createRequire(import.meta.url)('http_ece'); test('preview lookup uses only the exact eligible completed root assistant content on the fixed public endpoint', async t => { const requests: string[] = []; const message = { id: 'outbound:11', role: 'assistant', content: 'Your **public answer** includes the requested passphrase: sky-blue.', metadata: { kind: 'message', status: 'complete', entryId: 11, reasoning: 'private reasoning', resultPreview: 'tool secret', output: 'worker output' } }; const original = { conversationId: '1', cursor: '11', beforeCursor: '11', messages: [message] }; let page: unknown = original, status = 200; const server = createServer((req, res) => { requests.push(req.url!); assert.equal(req.headers.authorization, 'Bearer fixture-key'); assert.equal(req.headers['x-sybil-client'], 'backend-proxy-v1'); res.writeHead(status, { 'Content-Type': 'application/json', Location: 'http://never-fetch.invalid/' }); res.end(JSON.stringify(page)); }); server.listen(0, '127.0.0.1'); await once(server, 'listening'); t.after(async () => { server.closeAllConnections(); await new Promise(resolve => server.close(() => resolve())); }); const load = createMessageText({ baseUrl: `http://127.0.0.1:${(server.address() as { port: number }).port}`, apiKey: 'fixture-key' }); const candidate = { conversationId: '1', cursor: '11', messageId: 'outbound:11' }; assert.equal(await load(candidate), message.content, 'explicit public assistant text is authorized without guessing/redacting secrets'); assert.deepEqual(requests, ['/api/conversations/root/messages?after=10&limit=1&includeTools=false']); page = { ...original, messages: [{ ...message, id: 'entry:11:0' }] }; assert.equal(await load({ ...candidate, messageId: 'entry:11:0' }), message.content); for (const invalid of [ { ...original, conversationId: 'worker-conversation' }, { ...original, cursor: '12' }, { ...original, beforeCursor: '10' }, { ...original, messages: [] }, { ...original, messages: [message, message] }, ...['tool', 'user', 'system', 'reasoning'].map(role => ({ ...original, messages: [{ ...message, role }] })), ...[{ kind: 'tool_call' }, { kind: 'worker' }, { status: 'initiated' }, { entryId: 12 }].map(metadata => ({ ...original, messages: [{ ...message, metadata: { ...message.metadata, ...metadata } }] })), { ...original, messages: [{ ...message, content: [{ type: 'reasoning', text: 'private reasoning' }] }] }, { ...original, messages: [{ ...message, id: 'tool:1:11' }] }, ]) { page = invalid; await assert.rejects(load(candidate)); } page = { ...original, messages: [{ ...message, content: '' }] }; assert.equal(await load(candidate), '', 'empty content never borrows metadata, tool output or reasoning'); page = { ...original, messages: [{ ...message, content: 'Caption stays inside authenticated chat.', metadata: { ...message.metadata, agentAttachments: [{ filename: 'private-name.png', workspacePath: '/private/source.png', url: 'https://never-fetch.invalid/image' }] } }] }; const imageText = await load(candidate); assert.equal(imageText, '', 'image captions and metadata use only a generic notification'); assert.deepEqual(JSON.parse(previewPayload(imageText, body => ({ body }), 4096).toString()), { body: 'You have a new message.' }); const before = requests.length; for (const messageId of ['tool:1:11', 'outbound:12', 'https://never-fetch.invalid/', 'entry:11:0/../../state']) { await assert.rejects(load({ ...candidate, messageId })); } assert.equal(requests.length, before); page = { ...original, messages: [{ ...message, content: 'x'.repeat(512 * 1024) }] }; await assert.rejects(load(candidate)); status = 302; page = original; await assert.rejects(load(candidate)); status = 503; await assert.rejects(load(candidate)); assert.ok(requests.every(url => url === requests[0]), 'no worker/raw-state/event/linked-resource requests or redirects'); }); test('Markdown becomes readable message text without link destinations, image sources or HTML internals', () => { const markdown = '# Status\n\nThe **report** is _ready_ & checked.\n\n- Read [the summary](https://private.invalid/secret)\n- `keep_snake_case()`\n\n> Done.\n\n```ts\nconst answer = 42;\n```\n\n![Chart](https://private.invalid/image)\n\n'; const result = previewText(markdown); for (const text of ['Status', 'The report is ready & checked.', 'Read the summary', 'keep_snake_case()', 'Done.', 'const answer = 42;', 'Chart']) assert.ok(result.includes(text), result); for (const hidden of ['https://', 'private.invalid', 'secret', 'hidden comment', 'internal script', '**', '```', '# Status']) assert.ok(!result.includes(hidden), result); assert.equal(previewText('Plain text: 日本語 🌧️ — keep_this_path.txt'), 'Plain text: 日本語 🌧️ — keep_this_path.txt'); assert.equal(previewText('A\u0000B\n\tC\ud800'), 'A B C�'); }); test('actual APNs and decrypted Web Push bodies carry public text with UTF-8/JSON/encryption bounds', async () => { const apnsKey = generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); const receiver = createECDH('prime256v1'); receiver.generateKeys(); const auth = Buffer.alloc(16, 1).toString('base64url'); const native = { transport: 'apns' as const, enabled: true as const, token: 'a'.repeat(64), environment: 'sandbox' as const, topic: 'net.buzzert.sybil2' }; const browser = { transport: 'web' as const, enabled: true as const, subscription: { endpoint: 'https://web.push.apple.com/fixture', keys: { p256dh: receiver.getPublicKey().toString('base64url'), auth } } }; const apns: ProviderRequest[] = [], web: ProviderRequest[] = []; const sender = createPushSender({ enabled: true, adminToken: 'fixture', apns: { key: apnsKey.privateKey.export({ format: 'pem', type: 'pkcs8' }).toString(), keyId: 'ABCDEFGHIJ', teamId: 'DQQH5H6GBD', environment: 'sandbox', topic: native.topic }, web: { ...webpush.generateVAPIDKeys(), subject: 'mailto:tests@example.com' } }, { apns: async request => { apns.push(request); return { status: 200 }; }, web: async request => { web.push(request); return { status: 201 }; }, }); const identity = { version: 1 as const, kind: 'message' as const, sourceId: randomUUID(), messageId: 'outbound:11', cursor: '11' }; const now = Date.now(); for (const content of ['The **report** is ready. 日本語 🌧️', 'Requested public passphrase: sky-blue.', 'a'.repeat(3000), 'a'.repeat(3001), '世界🌧️é'.repeat(2000), '"\\'.repeat(4000), '\ud800'.repeat(4000), ' ']) { assert.equal((await sender(native, identity, now + 60_000, now, content)).outcome, 'accepted'); assert.equal((await sender(browser, identity, now + 60_000, now, content)).outcome, 'accepted'); const apnsBytes = apns.at(-1)!.body, encrypted = web.at(-1)!.body; const plaintext: Buffer = decrypt(encrypted, { version: 'aes128gcm', privateKey: receiver, authSecret: auth }); const apnsPayload = JSON.parse(apnsBytes.toString()), webPayload = JSON.parse(plaintext.toString()); const body = apnsPayload.aps.alert.body; assert.equal(typeof body, 'string'); assert.equal(webPayload.body, body); assert.ok(Buffer.byteLength(body) <= MAX_PREVIEW_BYTES); assert.ok(apnsBytes.length <= 4096); assert.ok(plaintext.length <= 3993); assert.ok(encrypted.length <= 4096); assert.equal(encrypted.length, plaintext.length + 103); assert.equal(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(body)), body); for (const [key, value] of Object.entries(identity)) { assert.equal(apnsPayload[key], value); assert.equal(webPayload[key], value); } assert.equal(webPayload.url, '/?agent=root'); assert.equal(webPayload.tag, `sybil-agent:${identity.sourceId}`); const flattened = previewText(content) || 'You have a new message.'; if (Buffer.byteLength(JSON.stringify(flattened)) - 2 <= MAX_PREVIEW_BYTES) assert.equal(body, flattened); else { assert.ok(body.endsWith('…')); assert.ok(flattened.startsWith(body.slice(0, -1))); } assert.ok(!encrypted.includes(Buffer.from(body)), 'Web Push plaintext is encrypted'); } await sender(native, { version: 1, kind: 'read_state', sourceId: identity.sourceId, readCursor: '11' }, now + 60_000, now, 'never include text in a clear'); assert.deepEqual(JSON.parse(apns.at(-1)!.body.toString()).aps, { 'content-available': 1 }); assert.ok(!apns.at(-1)!.body.includes('never include')); }); test('payload budget includes identity overhead and JSON escaping before truncating on a Unicode boundary', () => { const result = previewPayload('界"\\🌧'.repeat(2000), body => ({ messageId: '"'.repeat(1000), body }), WEB_PUSH_PAYLOAD_BYTES); assert.ok(result.length <= WEB_PUSH_PAYLOAD_BYTES); const body = JSON.parse(result.toString()).body; assert.ok(body.endsWith('…')); assert.equal(body.includes('�'), false); assert.throws(() => previewPayload('hello', body => ({ extra: 'x'.repeat(5000), body }), 4096)); });