feat(jose): add compact JWE encryption and decryption utilities#123
feat(jose): add compact JWE encryption and decryption utilities#123halvaradop merged 5 commits intomasterfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughRenames JOSE key/option fields from Changes
sequenceDiagram
autonumber
actor Client
participant JWT as Encode/Decode Module
participant KDF as KeyDerivation
participant JWS as signJWS/verifyJWS
participant JWE as compactEncryptJWE/decryptCompactJWE
Client->>JWT: encodeJWT(payload, options)
JWT->>KDF: derive sign & encrypt secrets (concurrently)
KDF-->>JWT: derived keys
JWT->>JWS: signJWS(payload, options?.sign)
JWS-->>JWT: signed JWT
JWT->>JWE: compactEncryptJWE(signedJWT, options?.encrypt)
JWE-->>Client: return compact JWE
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/core/CHANGELOG.md`:
- Around line 11-12: Add a changelog entry under the existing "Unreleased" ->
"Changed" subsection documenting the new session option added to createAuth:
briefly describe that createAuth now accepts a session option (what it
does/affects and any default behavior or breaking changes), reference the
function name createAuth and the "session" option explicitly, and remove the
empty subsection header if you instead move the content elsewhere so the
changelog is not left empty.
In `@packages/jose/CHANGELOG.md`:
- Around line 11-12: The changelog’s "### Added" subsection is empty; update the
Unreleased changelog by adding a concise entry under the "### Added" header
describing the new "JWT headers support" feature (e.g., "Add JWT headers support
— allow custom and standard JWT header fields to be set and verified"),
optionally referencing the PR or commit ID and author for traceability so
consumers can see what was added.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e2e8b3b3-bf24-48f8-9177-2e367dfd5880
📒 Files selected for processing (2)
packages/core/CHANGELOG.mdpackages/jose/CHANGELOG.md
- compactEncryptJWE - decryptCompactJWE - createCompactJWE
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/core/src/jose.ts`:
- Around line 103-107: The decodeJWTOptions object is wired to the wrong branch:
swap the assignments so sign receives jwtVerificationOptions and encrypt only
contains JWE-specific constraints; update decodeJWTOptions (used by decodeJWT())
to set sign: jwtVerificationOptions and move any HS256/ signature allow-list out
of encrypt into sign, leaving only JWE-related constraints under encrypt.
- Around line 56-60: The CSRF key derivation is using the wrong context: change
the third createDeriveKey call that produces derivedCsrfTokenKey to use the
CSRF/JWS-specific label instead of "encryption" (i.e., call
createDeriveKey(secret, salt, "jws") or the CSRF label used elsewhere) so
derivedCsrfTokenKey is derived with a distinct context; update the array where
derivedSigningKey, derivedEncryptionKey, derivedCsrfTokenKey are created to use
that CSRF/JWS label.
In `@packages/jose/src/encrypt.ts`:
- Around line 65-73: compactEncryptJWE is dropping nested-JWT header fields
because setProtectedHeader currently only sets alg and enc; when wrapping a
signed JWT you must preserve or add the content type. Update compactEncryptJWE
(the CompactEncrypt(...).setProtectedHeader(...) call) to include cty: "JWT"
(and preserve any provided typ/cty from options) when the payload is a JWT or
when options doesn't already specify cty, so the outer JWE retains the
nested-JWT header expected by validators.
- Around line 118-126: Change the parameter type for decryptCompactJWE from
JWTDecryptOptions to DecryptOptions: update the function signature of
decryptCompactJWE to accept options?: DecryptOptions (it delegates to
compactDecrypt which only accepts DecryptOptions), and then update the type for
DecodeJWTOptions.encrypt in index.ts to use DecryptOptions instead of
JWTDecryptOptions so the propagated API matches compactDecrypt; ensure any
imports/types are adjusted accordingly (reference: decryptCompactJWE and
compactDecrypt, and DecodeJWTOptions.encrypt).
In `@packages/jose/src/index.ts`:
- Around line 71-72: The change makes encodeJWT encrypt the raw JWS string
(signed via signJWS) which breaks backwards compatibility because previous
tokens encrypted a JSON claims set (e.g., { payload: signedJws }); to fix,
restore the prior wire format or add a dual-decode path: when creating tokens
with compactEncryptJWE keep wrapping the signed JWS in an envelope object (e.g.,
{ payload: signedJws }) so existing decryptors still get a claims set, or update
compactDecryptJWE/decodeJWT to detect and accept both the legacy JSON envelope
and a raw JWS string (handle string vs object) so sessions minted before rollout
remain valid; locate and change the logic in encodeJWT (where signJWS and
compactEncryptJWE are called) and in the complementary decode/verification code
that calls compactDecryptJWE to implement the compatibility branch.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e98aa884-b8cf-4961-a7d0-2950a9366b40
📒 Files selected for processing (9)
packages/core/CHANGELOG.mdpackages/core/src/jose.tspackages/jose/CHANGELOG.mdpackages/jose/src/encrypt.tspackages/jose/src/index.tspackages/jose/src/secret.tspackages/jose/src/sign.tspackages/jose/test/index.test.tspackages/jose/test/types.test-d.ts
✅ Files skipped from review due to trivial changes (1)
- packages/core/CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/jose/CHANGELOG.md
createAuth and JWT headersThere was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/tanstack-start/src/lib/auth-server.ts (1)
44-50:⚠️ Potential issue | 🟠 MajorRedirecting to
undefinedhref when sign-in fails.When
api.signIn()throws an error,.catch()returnsnull, makingresponse?.signInURLevaluate toundefined. Callingredirect({ href: undefined })will result in invalid redirect behavior—either a runtime error or navigation to an unintended location.Unlike the
signOutFncase (where headers are optional and redirect to "/" still works), thesignInFnrequires a valid destination URL for the redirect to function correctly.🛠️ Proposed fix: Handle the error case explicitly
.catch((error) => { console.error("[error:server] signIn", error) return null }) + if (!response) { + throw redirect({ to: "/" }) + } throw redirect({ - href: response?.signInURL, + href: response.signInURL, })Alternatively, redirect to a dedicated error page:
+ if (!response) { + throw redirect({ to: "/auth/error" }) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/tanstack-start/src/lib/auth-server.ts` around lines 44 - 50, The current sign-in flow calls api.signIn() and then unconditionally throws redirect({ href: response?.signInURL }), which can pass undefined when api.signIn() errors; update the sign-in handler (the api.signIn() call, the response variable and the redirect usage) to explicitly handle the null/error case: in the .catch() return pathway log the error and set a fallback destination (e.g., "/" or a dedicated error page) or throw a redirect to that fallback only if response?.signInURL is falsy, otherwise redirect to response.signInURL; ensure you do not call redirect with an undefined href and keep error logging for diagnostics.
🧹 Nitpick comments (1)
packages/jose/test/index.test.ts (1)
370-390: Remove duplicate test block.This
createJWTdescribe block duplicates tests already present in theJWTsdescribe block:
- Lines 371-384 duplicate lines 265-278 ("create a signed and encrypted JWT using createJWT with separate JWS and JWE secrets")
- Lines 386-389 duplicate lines 260-263 ("createJWT with invalid secret")
♻️ Suggested fix
-describe("createJWT", () => { - test("createJWT with separate JWS and JWE secrets", async () => { - const secret = getRandomBytes(32) - const derivedSigningKey = await createDeriveKey(secret, "salt", "signing") - const derivedEncryptionKey = await createDeriveKey(secret, "salt", "encryption") - - const { encodeJWT, decodeJWT } = createJWT({ sign: derivedSigningKey, encrypt: derivedEncryptionKey }) - - const jwt = await encodeJWT(payload) - expect(jwt).toBeDefined() - const decodedPayload = await decodeJWT(jwt) - expect(decodedPayload.sub).toBe(payload.sub) - expect(decodedPayload.name).toBe(payload.name) - expect(decodedPayload.email).toBe(payload.email) - }) - - test("createJWT with invalid secret", async () => { - const { encodeJWT } = createJWT("short") - await expect(encodeJWT(payload)).rejects.toThrow("Secret string must be at least 32 bytes long") - }) -})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/jose/test/index.test.ts` around lines 370 - 390, The describe("createJWT") block duplicates tests already present in the "JWTs" suite; remove the duplicate describe block (including the tests that use createDeriveKey, createJWT, encodeJWT, decodeJWT and the invalid-secret test) so only the original tests in the "JWTs" describe remain; ensure no other unique tests are lost and that imports/helpers (createDeriveKey, createJWT, payload) remain referenced only where needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/jose/src/index.ts`:
- Around line 39-42: The DecodeJWTOptions type declares decrypt as
JWTDecryptOptions but decryptCompactJWE (which calls compactDecrypt) expects
DecryptOptions; update the DecodeJWTOptions interface to use DecryptOptions for
the decrypt property (replace JWTDecryptOptions with DecryptOptions) so the API
accurately reflects what decryptCompactJWE/compactDecrypt will accept and avoid
exposing JWT-specific verification fields.
---
Outside diff comments:
In `@apps/tanstack-start/src/lib/auth-server.ts`:
- Around line 44-50: The current sign-in flow calls api.signIn() and then
unconditionally throws redirect({ href: response?.signInURL }), which can pass
undefined when api.signIn() errors; update the sign-in handler (the api.signIn()
call, the response variable and the redirect usage) to explicitly handle the
null/error case: in the .catch() return pathway log the error and set a fallback
destination (e.g., "/" or a dedicated error page) or throw a redirect to that
fallback only if response?.signInURL is falsy, otherwise redirect to
response.signInURL; ensure you do not call redirect with an undefined href and
keep error logging for diagnostics.
---
Nitpick comments:
In `@packages/jose/test/index.test.ts`:
- Around line 370-390: The describe("createJWT") block duplicates tests already
present in the "JWTs" suite; remove the duplicate describe block (including the
tests that use createDeriveKey, createJWT, encodeJWT, decodeJWT and the
invalid-secret test) so only the original tests in the "JWTs" describe remain;
ensure no other unique tests are lost and that imports/helpers (createDeriveKey,
createJWT, payload) remain referenced only where needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e65f28d9-824f-459c-b179-501fa19e96d5
📒 Files selected for processing (8)
apps/astro/astro.config.mjsapps/tanstack-start/src/lib/auth-server.tspackages/core/src/jose.tspackages/core/src/secure.tspackages/jose/src/encrypt.tspackages/jose/src/index.tspackages/jose/src/sign.tspackages/jose/test/index.test.ts
💤 Files with no reviewable changes (1)
- apps/astro/astro.config.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/jose/src/sign.ts
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/jose/src/index.ts (1)
76-77:⚠️ Potential issue | 🟠 MajorBreaking change: JWT wire format has changed.
The encoding now directly encrypts the signed JWS string via
compactEncryptJWE(signed, ...)instead of wrapping it in a JSON envelope (e.g.,{ payload: signedJws }). While this aligns with RFC 7519 Section 5.2 for nested JWTs, it's a breaking change—existing tokens minted with the previous format will fail to decode.Ensure this is documented as a breaking change, or consider implementing a migration path that can detect and handle both formats during a transition period.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/jose/src/index.ts` around lines 76 - 77, The output wire format changed because encode now calls compactEncryptJWE(signed, ...) with the raw signed JWS from signJWS, breaking consumers expecting a JSON envelope (e.g., { payload: signedJws }); fix by either restoring the previous envelope when encrypting (wrap the signed value into { payload: signed } before calling compactEncryptJWE in the function that currently calls signJWS -> compactEncryptJWE) or implement backward-compatibility in the decrypt/verify path (in the JWE decrypt/verify function, after decrypting check if the plaintext is JSON with a payload field and extract that, otherwise treat plaintext as a raw signed JWS) and add a short breaking-change note in the package docs mentioning the new default and the migration support.
🧹 Nitpick comments (1)
packages/jose/src/encrypt.ts (1)
151-173: Minor: inconsistent parameter naming in factory methods.On line 157, the
decryptJWEmethod acceptspayload: string, but semantically this is a token to decrypt, not a payload. Similarly, line 171 usespayloadfordecryptCompactJWE. Consider renaming totokenfor consistency with the standalone functions.♻️ Suggested fix
- decryptJWE: <Decrypted extends JWTPayload = Payload>(payload: string, options?: JWTDecryptOptions) => - decryptJWE<Decrypted>(payload, secret, options), + decryptJWE: <Decrypted extends JWTPayload = Payload>(token: string, options?: JWTDecryptOptions) => + decryptJWE<Decrypted>(token, secret, options),- decryptCompactJWE: (payload: string, options?: DecryptOptions) => decryptCompactJWE(payload, secret, options), + decryptCompactJWE: (token: string, options?: DecryptOptions) => decryptCompactJWE(token, secret, options),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/jose/src/encrypt.ts` around lines 151 - 173, Rename the parameter `payload` to `token` in the `decryptJWE` method inside the `createJWE` function and in the `decryptCompactJWE` method inside the `createCompactJWE` function for semantic consistency. This aligns the parameter names with their purpose, matching the standalone functions they wrap.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/jose/src/index.ts`:
- Around line 76-77: The output wire format changed because encode now calls
compactEncryptJWE(signed, ...) with the raw signed JWS from signJWS, breaking
consumers expecting a JSON envelope (e.g., { payload: signedJws }); fix by
either restoring the previous envelope when encrypting (wrap the signed value
into { payload: signed } before calling compactEncryptJWE in the function that
currently calls signJWS -> compactEncryptJWE) or implement
backward-compatibility in the decrypt/verify path (in the JWE decrypt/verify
function, after decrypting check if the plaintext is JSON with a payload field
and extract that, otherwise treat plaintext as a raw signed JWS) and add a short
breaking-change note in the package docs mentioning the new default and the
migration support.
---
Nitpick comments:
In `@packages/jose/src/encrypt.ts`:
- Around line 151-173: Rename the parameter `payload` to `token` in the
`decryptJWE` method inside the `createJWE` function and in the
`decryptCompactJWE` method inside the `createCompactJWE` function for semantic
consistency. This aligns the parameter names with their purpose, matching the
standalone functions they wrap.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ea367f5a-6ca5-41ff-af78-d0b54c8bfddb
📒 Files selected for processing (2)
packages/jose/src/encrypt.tspackages/jose/src/index.ts
Description
This pull request introduces compact JWE utilities to the
@aura-stack/josepackage, providing a clear and focused API for encrypting and decrypting plain text content.Previously, JWE utilities accepted and returned raw strings, which made the distinction between encrypted JWT payloads and generic encrypted content unclear. With this update:
encryptJWEanddecryptJWEare now strictly typed to work withJWTPayloadobjects.The new compact utilities (
createCompactJWE) allow developers to explicitly encrypt and decrypt string-based payloads, improving clarity and separation of concerns between JWT-based flows and generic encryption.Additionally:
encodeJWTanddecodeJWTnow internally rely on the compact JWE implementation for encryption flows.Usage
Changes