fix(shared): Generate publishable keys with unpadded Base64 encoding to match backend output#8400
Conversation
🦋 Changeset detectedLatest commit: 5d4f79b The changes in this PR will be included in the next version bump. This PR includes changesets to release 20 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe pull request updates the Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/shared/src/keys.ts`:
- Line 43: The Base64 padding is being stripped in buildPublishableKey (the line
using isomorphicBtoa(...).replace(/=+$/, '')) which breaks browser atob() used
by isomorphicAtob and thus parsePublishableKey/isPublishableKey; either stop
removing the '=' padding when encoding in buildPublishableKey or, if you must
strip it, update isomorphicAtob/parsePublishableKey to restore padding to a
multiple of 4 before calling atob()/Buffer. Modify the code paths referencing
isomorphicBtoa/isomorphicAtob, buildPublishableKey, parsePublishableKey and
isPublishableKey so decoding always receives properly padded Base64 (restore
padding via '=' until length % 4 === 0) or retain padding on encode.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: f1e3fb79-03e8-415f-8fc0-e9a07d50f131
📒 Files selected for processing (2)
packages/shared/src/__tests__/keys.spec.tspackages/shared/src/keys.ts
| (frontendApi.startsWith('clerk.') && LEGACY_DEV_INSTANCE_SUFFIXES.some(s => frontendApi.endsWith(s))); | ||
| const keyPrefix = isDevKey ? PUBLISHABLE_KEY_TEST_PREFIX : PUBLISHABLE_KEY_LIVE_PREFIX; | ||
| return `${keyPrefix}${isomorphicBtoa(`${frontendApi}$`)}`; | ||
| return `${keyPrefix}${isomorphicBtoa(`${frontendApi}$`).replace(/=+$/, '')}`; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether unpadded base64 is normalized before decode.
# Expected for safety: either
# 1) isomorphicAtob adds '=' padding before decoding, or
# 2) callers in keys.ts pad the encoded segment before passing to isomorphicAtob.
set -euo pipefail
echo "Locate decoder and call sites..."
fd -i 'isomorphicAtob.ts' packages/shared/src
fd -i 'keys.ts' packages/shared/src
ISO_FILE="$(fd -i 'isomorphicAtob.ts' packages/shared/src | head -n1)"
KEYS_FILE="$(fd -i 'keys.ts' packages/shared/src | head -n1)"
echo
echo "---- isomorphicAtob implementation ----"
sed -n '1,220p' "$ISO_FILE"
echo
echo "---- build/decode usage in keys.ts ----"
rg -n -C4 'buildPublishableKey|isomorphicAtob\(|split\('\'_\'\)\[2\]|replace\(/=\+\$/, \x27\x27\)' "$KEYS_FILE"Repository: clerk/javascript
Length of output: 1057
🏁 Script executed:
#!/bin/bash
# Examine keys.ts to see how unpadded Base64 is handled in decode/parse functions
echo "=== Reading full keys.ts file ==="
cat -n packages/shared/src/keys.ts | head -100Repository: clerk/javascript
Length of output: 4722
🏁 Script executed:
cat -n packages/shared/src/keys.ts | tail -n +100Repository: clerk/javascript
Length of output: 8694
🏁 Script executed:
#!/bin/bash
# Search for tests of buildPublishableKey and parsePublishableKey round-trip
find . -type f \( -name '*.test.ts' -o -name '*.test.tsx' -o -name '*.spec.ts' -o -name '*.spec.tsx' \) | xargs rg -l 'buildPublishableKey|parsePublishableKey' 2>/dev/null | head -5Repository: clerk/javascript
Length of output: 325
🏁 Script executed:
cat packages/shared/src/__tests__/keys.spec.tsRepository: clerk/javascript
Length of output: 11820
🏁 Script executed:
python3 << 'EOF'
# Check if buildPublishableKey test output strings are valid for browser atob()
# Base64 strings must be multiple of 4 chars when using atob()
test_cases = [
'ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ', # fake-clerk-test...dev$
'Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', # foo-bar-13...dev$
'Y2xlcmsuYm9yaW5nLnNhd2ZseS05MS5sY2wuZGV2JA', # clerk.boring...dev$
'Y2xlcmsuYm9yaW5nLnNhd2ZseS05MS5sY2xjbGVyay5jb20k', # clerk.boring...com$
]
print("Checking buildPublishableKey test outputs for Base64 validity in atob():\n")
for b64_str in test_cases:
length = len(b64_str)
remainder = length % 4
is_valid_for_atob = remainder == 0
status = "✓ Valid for atob()" if is_valid_for_atob else f"✗ INVALID ({remainder} chars short of multiple of 4)"
print(f"Length: {length:2d} ({remainder} % 4 = {remainder}) - {status}")
print(f" String: {b64_str}\n")
EOFRepository: clerk/javascript
Length of output: 583
🏁 Script executed:
python3 << 'EOF'
# Verify what proper Base64 padding should be for these strings
test_cases = [
('ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ', 'fake-clerk-test.clerk.accounts.dev$'),
('Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', 'foo-bar-13.clerk.accounts.dev$'),
('Y2xlcmsuYm9yaW5nLnNhd2ZseS05MS5sY2wuZGV2JA', 'clerk.boring.sawfly-91.lcl.dev$'),
('Y2xlcmsuYm9yaW5nLnNhd2ZseS05MS5sY2xjbGVyay5jb20k', 'clerk.boring.sawfly-91.lclclerk.com$'),
]
import base64
print("Verifying Base64 encoding with proper padding:\n")
for unpadded_b64, expected_decoded in test_cases:
length = len(unpadded_b64)
remainder = length % 4
# Add proper padding
padding_needed = (4 - remainder) % 4
padded_b64 = unpadded_b64 + ('=' * padding_needed)
# Try to decode
try:
decoded = base64.b64decode(padded_b64).decode('utf-8')
match = "✓" if decoded == expected_decoded else "✗"
print(f"{match} Unpadded: {unpadded_b64}")
print(f" Padded: {padded_b64}")
print(f" Decoded: {decoded}")
print()
except Exception as e:
print(f"✗ Failed to decode: {e}")
print()
EOFRepository: clerk/javascript
Length of output: 765
Critical: Unpadded Base64 keys will fail in browser environments.
Line 43 strips all Base64 padding via .replace(/=+$/, ''), but isomorphicAtob passes the unpadded string directly to browser atob(), which requires length to be a multiple of 4. Some keys (e.g., those with decoded length not divisible by 4) will throw in browsers—parsePublishableKey/isPublishableKey will fail for valid keys generated by buildPublishableKey.
Tests pass in Node.js because Buffer.from() accepts unpadded Base64, but browser atob() is stricter. Either restore padding before decode or do not strip padding on encode.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/shared/src/keys.ts` at line 43, The Base64 padding is being stripped
in buildPublishableKey (the line using isomorphicBtoa(...).replace(/=+$/, ''))
which breaks browser atob() used by isomorphicAtob and thus
parsePublishableKey/isPublishableKey; either stop removing the '=' padding when
encoding in buildPublishableKey or, if you must strip it, update
isomorphicAtob/parsePublishableKey to restore padding to a multiple of 4 before
calling atob()/Buffer. Modify the code paths referencing
isomorphicBtoa/isomorphicAtob, buildPublishableKey, parsePublishableKey and
isPublishableKey so decoding always receives properly padded Base64 (restore
padding via '=' until length % 4 === 0) or retain padding on encode.
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/dev-cli
@clerk/expo
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
Description
Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change