feat!: 2.0, RFC 8291 aes128gcm and RFC 8292 vapid - #19
Merged
Conversation
…s not this libraries purpose
The block65 lint and format standard is oxc. @block65/shared-config is not published to npm, so the settings are inlined here rather than extended. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pnpm 11, typescript 7, vitest 4, node 26 as the development version. The peerDependencyRules block is dropped because pnpm no longer reads it, and allowBuilds records that none of the native packages need their build scripts. CI now tests node 22, 24 and 26, and runs oxlint and oxfmt as a lint job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The root tsconfig only pointed tsc at the single package, so the Makefile names it directly instead. The browser example referenced two projects that are not composite, which tsc rejects, and both wrote their build info to the same file inherited from @tsconfig/vite-react. The test project no longer sets outDir alongside noEmit. clean now removes dist outright. tsc -b --clean leaves behind output whose source has been deleted, and files is dist/lib/**/*, so those would ship. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BREAKING CHANGE: messages now use the aes128gcm content encoding from RFC 8291 and the vapid authorization scheme from RFC 8292, replacing the draft-04 aesgcm encoding and the WebPush scheme. Apple accepts neither of the old ones, so Safari and iOS never worked. Consumers that need aesgcm should stay on 1.x. encryptNotification now returns the complete RFC 8188 body rather than the ciphertext, salt and key separately, and takes an options argument. The crypto-key and encryption headers are gone, and buildPushPayload is otherwise unchanged. Every message is padded to a constant 4096 octets so the ciphertext length does not disclose the plaintext length, which also keeps the body within the size a push service is obliged to accept. The maximum payload is 3993 bytes. Subscription keys are validated: p256dh as a 65 byte uncompressed P-256 point, auth as 16 octets, and the endpoint as https. Ephemeral and HMAC keys are imported non-extractable and the private JWK is no longer materialised. hkdf expand now follows RFC 5869 for outputs longer than one block instead of truncating, and type-fest is gone, so the only dependency is uint8array-extras. Tested against the RFC 8291 section 5 vector, which the implementation matches byte for byte, and by decrypting as a subscriber would. The tests that posted to real push endpoints are gone; they proved transport, not decryptability. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tailwind 4 is CSS first, so the config file and the postcss pipeline are gone. Wrangler 4 reads wrangler.jsonc, and drizzle-kit 1.0 writes each migration to its own directory, which wrangler needs migrations_pattern to find. The nodejs example had the cloudflare example's package name, imported dotenv where node reads the env file itself, and never created its data directory. Its migration was also never applied on a clean checkout: the journal indexing it sits under migrations/meta, which was gitignored. The new layout has no meta directory, so the ignore is dropped and the migration is tracked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Subscribes a real browser to its real push service, sends a notification, and asserts the service worker decrypted it. The tests that were removed posted to push endpoints and checked for a 201, which says the request was accepted and nothing about whether a subscriber can read it. Chrome has to come from the system because playwright's chromium is built without the credentials to register with FCM, and the push api is unavailable in incognito, so the context has to be persistent. Firefox needs its push connection turned on explicitly. Not wired into make test, and not in CI, because it needs the network and a subscription that expires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fetch rejects Uint8Array<ArrayBufferLike> as a BodyInit, so the call in the readme did not typecheck in a project with the dom lib. The body is built with new Uint8Array, so it always was ArrayBuffer backed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed server node has a test runner and strips types itself at the version this repo targets, so the e2e package needs neither vitest nor a transpiler. The static server is hono, matching the examples. The subscribe now happens inside page.evaluate against real DOM types, and the decrypted message comes back through the document title, so nothing declares page globals on Window from a node file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@types/node, @tsconfig/node22, hono, @hono/node-server and the drizzle pair are used by more than one workspace, so they resolve through catalog: and move in one place. Everything is on the node 22 floor now that engines asks for it. The examples and the e2e package were pulling node 26 types, which would let them use APIs the published package does not support. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It is a playwright test, so playwright runs it. That also replaces the manual waitForFunction with toHaveTitle, which retries on its own. The persistent context is still launched by hand: the built in page fixture gives an incognito context, and chrome disables the push api there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The run was already logging a 404 for the favicon the browser asks for on its own, which nothing noticed. The page now declares an empty one. Listening on the context rather than the page picks up the service worker, which is where the decryption happens, though only chrome reports it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
10 tasks
new Date(2000, 1, 1, 13) is local time, so the iat and exp signed into the vapid token moved with the machine timezone and the snapshot only matched in the zone it was recorded in. CI runs in UTC and failed on every node version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flagged by CodeQL on #19. Neither workflow uses the token for anything but checkout, and npm publish authenticates with NODE_AUTH_TOKEN. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing in the repo uses yarn, so the install instructions and the leftover yarn log patterns are gone. The root readme leads with the package name rather than the repository name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #18, whose two remaining checklist items are done here.
The encoding
Messages now use
aes128gcmfrom RFC 8291 and thevapidauthorization scheme from RFC 8292, replacing the draft-04aesgcmencoding and theWebPushscheme.Apple accepts neither of the old ones, so Safari and iOS never worked with this library. That is the reason for the major.
Verified against the RFC 8291 section 5 worked example, which the implementation reproduces byte for byte, and by decrypting the payload the way a subscriber does.
Breaking
encryptNotificationreturns the complete RFC 8188 body rather than{ ciphertext, salt, localPublicKeyBytes }, and takes an options argumentcrypto-keyandencryptionheaders are gonebuildPushPayloadis otherwise unchanged, sofetch(subscription.endpoint, payload)still works as documentedAnything that needs the
aesgcmencoding should stay on 1.x.Security
A Fable security review found nothing exploitable, and a second pass re-verified the crypto after the review's own recommendations were applied. Its HKDF check compared the implementation against Node's native
hkdfSyncand a hand written RFC 5869 loop across 417 input combinations with no mismatches.Applied from it:
p256dhis validated as a 65 byte uncompressed P-256 point,authas 16 octets, and the endpoint ashttpsTests
The tests that posted to real FCM and WNS endpoints are gone. They asserted a
201, which says the push service accepted the request and nothing about whether a subscriber can read it, and they relied on fake subscriptions that expire.In their place:
p256dh,authand non-https rejectionse2epackage that subscribes real Chrome and real Firefox to their real push services, sends a notification, and asserts the service worker decrypted it. It is not in CI and not part ofmake test17 unit tests and 2 e2e tests.
Closes the #18 checklist
remove triple slash references, to avoid a repeat of #15 and #16. Done, and checked rather than assumed: a consumer with
lib: ["es2022"],types: []andskipLibCheck: falsetypechecks against the packed tarball. That took droppingtype-fest, which reintroduced the #16 class throughFloat16Array.JsonifiableandRequireAtLeastOneare now about ten lines intypes.ts, and the package ships one dependency.better integration tests that do not involve fake subscriptions. Done, see above.
Everything else
pnpm 11, typescript 7, vitest 4, node 26 for development on a node 22 floor. Biome replaced with oxlint and oxfmt. Shared versions move through a pnpm catalog. Tailwind 4, wrangler 4 with
wrangler.jsonc, drizzle 1.0 rc in the examples. CI tests node 22, 24 and 26 and runs lint.The nodejs example never created its table on a clean checkout: the journal indexing its migration was gitignored. Fixed.