Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import { b64url, decrypt, encrypt, fromB64url, generateVapidKeys, sendPush, vapidAuthorization, vapidJwt } from "./webpush.ts"; | |
| 5 | ||
| 6 | // RFC 8291, Appendix A: the worked example, every input and the body. | |
| 7 | const RFC = { | |
| 8 | plaintext: "When I grow up, I want to be a watermelon", | |
| 9 | asPrivate: "yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw", | |
| 10 | asPublic: "BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8", | |
| 11 | uaPrivate: "q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94", | |
| 12 | uaPublic: "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4", | |
| 13 | auth: "BTBZMqHH6r4Tts7J_aSIgg", | |
| 14 | salt: "DGv6ra1nlYgDCS1FRnbzlw", | |
| 15 | body: | |
| 16 | "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN", | |
| 17 | }; | |
| 18 | ||
| 19 | test("encryption reproduces RFC 8291's example byte for byte", async () => { | |
| 20 | const body = await encrypt(new TextEncoder().encode(RFC.plaintext), { p256dh: RFC.uaPublic, auth: RFC.auth }, { | |
| 21 | asPrivate: RFC.asPrivate, | |
| 22 | asPublic: RFC.asPublic, | |
| 23 | salt: fromB64url(RFC.salt), | |
| 24 | }); | |
| 25 | assert.equal(b64url(body), RFC.body); | |
| 26 | }); | |
| 27 | ||
| 28 | test("the example's body decrypts with the subscriber's key", async () => { | |
| 29 | const plain = await decrypt(fromB64url(RFC.body), { privateKey: RFC.uaPrivate, publicKey: RFC.uaPublic, auth: RFC.auth }); | |
| 30 | assert.equal(new TextDecoder().decode(plain), RFC.plaintext); | |
| 31 | }); | |
| 32 | ||
| 33 | test("a fresh key and salt each time, and it still round-trips", async () => { | |
| 34 | const message = new TextEncoder().encode(JSON.stringify({ title: "Ana", body: "ship it? ✨" })); | |
| 35 | const a = await encrypt(message, { p256dh: RFC.uaPublic, auth: RFC.auth }); | |
| 36 | const b = await encrypt(message, { p256dh: RFC.uaPublic, auth: RFC.auth }); | |
| 37 | assert.notEqual(b64url(a), b64url(b)); | |
| 38 | // Header: 16 salt, record size 4096, a 65-byte key id. | |
| 39 | assert.equal(new DataView(a.buffer, a.byteOffset).getUint32(16), 4096); | |
| 40 | assert.equal(a[20], 65); | |
| 41 | assert.equal(a[21], 4); | |
| 42 | const plain = await decrypt(a, { privateKey: RFC.uaPrivate, publicKey: RFC.uaPublic, auth: RFC.auth }); | |
| 43 | assert.deepEqual(plain, message); | |
| 44 | }); | |
| 45 | ||
| 46 | test("too long a message for one record is refused", async () => { | |
| 47 | await assert.rejects(encrypt(new Uint8Array(4000), { p256dh: RFC.uaPublic, auth: RFC.auth })); | |
| 48 | }); | |
| 49 | ||
| 50 | test("the VAPID JWT: ES256, the push service's origin, a 12-hour expiry, a subject, and a signature that verifies", async () => { | |
| 51 | const keys = await generateVapidKeys(); | |
| 52 | assert.equal(fromB64url(keys.publicKey).length, 65); | |
| 53 | assert.equal(fromB64url(keys.privateKey).length, 32); | |
| 54 | const now = Date.UTC(2026, 9, 8, 12); | |
| 55 | const jwt = await vapidJwt("https://fcm.googleapis.com/fcm/send/abc:def", { ...keys, subject: "https://g1t.sh" }, now); | |
| 56 | const [header, claims, signature] = jwt.split("."); | |
| 57 | assert.deepEqual(JSON.parse(new TextDecoder().decode(fromB64url(header))), { typ: "JWT", alg: "ES256" }); | |
| 58 | assert.deepEqual(JSON.parse(new TextDecoder().decode(fromB64url(claims))), { | |
| 59 | aud: "https://fcm.googleapis.com", | |
| 60 | exp: now / 1000 + 12 * 3600, | |
| 61 | sub: "https://g1t.sh", | |
| 62 | }); | |
| 63 | // Raw r‖s, as JWS wants, not DER. | |
| 64 | assert.equal(fromB64url(signature).length, 64); | |
| 65 | const point = fromB64url(keys.publicKey); | |
| 66 | const verifier = await crypto.subtle.importKey("raw", point.slice().buffer as ArrayBuffer, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]); | |
| 67 | const ok = await crypto.subtle.verify( | |
| 68 | { name: "ECDSA", hash: "SHA-256" }, | |
| 69 | verifier, | |
| 70 | fromB64url(signature).slice().buffer as ArrayBuffer, | |
| 71 | new TextEncoder().encode(`${header}.${claims}`), | |
| 72 | ); | |
| 73 | assert.ok(ok); | |
| 74 | const authorization = await vapidAuthorization("https://push.example/x", { ...keys, subject: "https://g1t.sh" }, now); | |
| 75 | assert.match(authorization, /^vapid t=[\w-]+\.[\w-]+\.[\w-]+, k=[\w-]+$/); | |
| 76 | }); | |
| 77 | ||
| 78 | test("a push is posted encrypted with its headers; 404 and 410 mean gone", async () => { | |
| 79 | const keys = await generateVapidKeys(); | |
| 80 | const seen: { url: string; init: RequestInit }[] = []; | |
| 81 | const status = { value: 201 }; | |
| 82 | const fetcher = (async (url: string, init: RequestInit) => { | |
| 83 | seen.push({ url, init }); | |
| 84 | return new Response(null, { status: status.value }); | |
| 85 | }) as unknown as typeof fetch; | |
| 86 | const subscription = { endpoint: "https://push.example/sub/1", p256dh: RFC.uaPublic, auth: RFC.auth }; | |
| 87 | const sent = await sendPush(subscription, { title: "hi" }, { vapid: { ...keys, subject: "https://g1t.sh" }, urgency: "high", topic: "chat:chn_1/x" }, fetcher); | |
| 88 | assert.deepEqual(sent, { endpoint: subscription.endpoint, status: 201, gone: false }); | |
| 89 | const headers = seen[0].init.headers as Record<string, string>; | |
| 90 | assert.equal(headers["content-encoding"], "aes128gcm"); | |
| 91 | assert.equal(headers.urgency, "high"); | |
| 92 | assert.equal(headers.topic, "chatchn_1x"); | |
| 93 | assert.equal(headers.ttl, "86400"); | |
| 94 | const body = new Uint8Array(seen[0].init.body as ArrayBuffer); | |
| 95 | const plain = await decrypt(body, { privateKey: RFC.uaPrivate, publicKey: RFC.uaPublic, auth: RFC.auth }); | |
| 96 | assert.deepEqual(JSON.parse(new TextDecoder().decode(plain)), { title: "hi" }); | |
| 97 | for (const code of [404, 410]) { | |
| 98 | status.value = code; | |
| 99 | assert.equal((await sendPush(subscription, {}, { vapid: { ...keys, subject: "https://g1t.sh" } }, fetcher)).gone, true); | |
| 100 | } | |
| 101 | status.value = 429; | |
| 102 | assert.equal((await sendPush(subscription, {}, { vapid: { ...keys, subject: "https://g1t.sh" } }, fetcher)).gone, false); | |
| 103 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.