feat(clerk-js,shared,react): supply a Protect assertion from the application - #9313
feat(clerk-js,shared,react): supply a Protect assertion from the application#9313zourzouvillys wants to merge 1 commit into
Conversation
…ication
A Protect assertion is a short-lived signed token an application mints from its
own backend, carrying key/value pairs its Protect rules can read. Until now the
only way to deliver one was the __clerk_protect_assertion cookie, which needs
the app and Frontend API to be same-site — true with a production CNAME setup,
not on development instances.
The token can now be handed to Clerk instead, and is attached to sign-in and
sign-up requests:
Clerk.load({ protectAssertion: token })
Clerk.load({ protectAssertion: () => readToken() })
clerk.setProtectAssertion(token)
A function is supported alongside a string because assertions are short-lived by
design and a page routinely outlives one. A string captured at load time stops
applying at expiry; a function is re-read per request, so a token refreshed in
the background takes effect without re-configuring Clerk.
It travels as a form param, not a header. A custom header would trigger a CORS
preflight — the same thing fapiClient already avoids by tunnelling PATCH/DELETE
through a _method query param. The merge has to happen in request(), before the
body is stringified: onBeforeRequest runs after stringification, so a callback
cannot add a body param.
Nothing here can fail a sign-in, which is the invariant the tests are built
around. A resolver that throws, rejects, or returns anything other than a
non-empty string yields no assertion and a warning, and the request proceeds. A
body that is not a plain object is left alone rather than spread — spreading a
FormData would discard the caller's payload instead of adding to it. With no
assertion configured the request is byte-for-byte what it was before, which is
why the params resolve to undefined rather than {}.
setProtectAssertion tracks whether it has been called, separately from the value
it was given. Without that, clearing with undefined would silently fall back to
the option, and a call before load() would be overwritten by it.
IsomorphicClerk implements it through the existing premountMethodCalls map,
which is keyed by method name — so a second call before load replaces the first,
which is the semantics a setter wants.
Bundlewatch: clerk.native.js gains ~0.2KB gzipped and crossed its 74KB ceiling;
raised to 75KB, matching the headroom the other entries carry.
The cookie path is unchanged. Where both are present, the SDK value wins.
Requires the matching server change to be deployed first.
🦋 Changeset detectedLatest commit: 93b1b36 The changes in this PR will be included in the next version bump. This PR includes changesets to release 23 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.
|
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/electron
@clerk/electron-passkeys
@clerk/eslint-plugin
@clerk/expo
@clerk/expo-google-signin
@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: |
On the two
|
Description
A Protect assertion is a short-lived, signed token an application mints from its own backend with the Clerk Backend API, carrying key/value pairs its Protect rules can read.
Until now the only way to deliver one to Protect was the
__clerk_protect_assertioncookie. That requires the app and the Frontend API to be same-site — true with a production CNAME setup, but not on development instances, where it is cross-site. It also means an application that has just minted a token has to work out a cookie domain scope in order to hand it over.The token can now be given to Clerk directly:
Clerk attaches it to sign-in and sign-up requests. The cookie continues to work unchanged; where both are present, the value given to the SDK wins.
Why a function as well as a string
Assertions are short-lived by design and a single-page app routinely outlives one. A string captured at configuration time silently stops applying at expiry; a function is re-read per request, so a token refreshed in the background takes effect without re-configuring Clerk.
Why a body param and not a header
fapiClientalready tunnelsPATCH/DELETEthrough a_methodquery param to keep requests CORS-simple, because the preflight otherwise breaks cookie dropping in Safari (see the existing comments in that file). A custom request header would reintroduce exactly that. The param rides the form-encoded body instead, on the requests Protect gates and nothing else.The merge has to happen inside
request(), before the body is stringified —onBeforeRequestcallbacks run after stringification, so a callback cannot add a body param.Behaviour worth reviewing
Nothing here can fail a sign-in. That is the invariant the tests are built around:
FormData, a string, aBlob) is left completely alone — spreading it would discard the caller's payload rather than add to itundefinedrather than{}setProtectAssertiontracks that it was called, separately from the value. Without that flag, clearing withundefinedwould silently fall back to theprotectAssertionoption, and a call made beforeClerk.load()would be overwritten by it.IsomorphicClerkimplements it through the existingpremountMethodCallsmap, which is keyed by method name — so a second call before load replaces the first, which is the semantics a setter wants.Checks
vitest run src/corein@clerk/clerk-js: 790 passed, including 16 new for the resolver and 12 new for the request wiring@clerk/react: 203 passed. One suite (ClerkProvider.test.tsx) fails to import in my local worktree because@clerk/localizationsisn't built there — unrelated to this changebuild:declarationsclean for@clerk/clerk-js;tsc --noEmitclean for@clerk/reacteslintclean on every file touchedclerk.native.jsgains ~0.2KB gzipped and crossed its 74KB ceiling, so it is raised to 75KB — the same headroom the other entries carry. Everything else passes unchanged.Note on ordering
This depends on a matching server-side change that must be deployed first — until then the new param is not accepted and would be rejected. Happy to hold this until that has shipped.
There is also an in-flight PR touching
fapiClient's request body for a separate Protect feature. Both introduce the samegetProtectParamsseam; whichever lands first, the other is a trivial resolve (keep one copy of the hook).