fix(wallets): use live JWT getter and retry stale JWT in NCS signer - #2006
fix(wallets): use live JWT getter and retry stale JWT in NCS signer#2006devin-ai-integration[bot] wants to merge 10 commits into
Conversation
- is now a getter that reads the latest at serialization time, so / retries pick up JWTs refreshed by while a request is in flight. - Added one-time retry in , and when the request returns an error and has changed. - Wire in before falling back to / so is surfaced when the signer frame reports . - Extend to read / from the nested field as well as the top-level response.
Original prompt from Robin
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
🦋 Changeset detectedLatest commit: 540f500 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 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 |
Prompt To Fix All With AI### Issue 1
packages/wallets/src/signers/non-custodial/ncs-signer.ts:455-479
**`_exportPrivateKey` still captures a static JWT snapshot**
`_exportPrivateKey` calls `getJwtOrThrow()` before sending to `exportTEEConnection`, so the JWT is snapshotted at call time rather than at serialization time. If the device is backgrounded while this request is in flight (the same scenario described in the PR), `setJwt` will update `crossmint.jwt` but the request will still carry the old value, and the response will be a silent `KeyExportError` masking a JWT-expiry underneath. The other three action paths (`get-status`, `start-onboarding`, `complete-onboarding`) have been migrated to `sendActionWithFreshJwt` / `createAuthData`, but this one was not.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "chore: add changeset for stale JWT retry..." | Re-trigger Greptile |
|
Slytherin spotted that |
|
Reviews (2): Last reviewed commit: "fix(wallets): apply live JWT getter and ..." | Re-trigger Greptile |
|
Reviews (3): Last reviewed commit: "refactor(wallets): rename sendActionWith..." | Re-trigger Greptile |
🔥 Smoke Test Results❌ Status: Failed Statistics
Test DetailsThis is a non-blocking smoke test. Full regression tests run separately. |
|
Reviews (4): Last reviewed commit: "refactor(wallets): rename sendWithFreshJ..." | Re-trigger Greptile |
|
Reviews (5): Last reviewed commit: "test(wallets): add logging test for JWT ..." | Re-trigger Greptile |
Prompt To Fix All With AI### Issue 1
packages/wallets/src/signers/non-custodial/ncs-signer.ts:392-395
**`_authPromise` can hang when `JWTExpiredError` bypasses `handleOnboardingVerificationFailure`**
For `OtpValidationError`, `handleOnboardingVerificationFailure` always calls `this._authPromise?.reject(error)` directly, so `_authPromise` is settled regardless of what the `onAuthRequired` callback does. The new `JWTExpiredError` path throws before reaching `handleOnboardingVerificationFailure`, so `_authPromise` is only rejected if `onAuthRequired` propagates the error all the way back to `handleAuthRequired`'s try/catch. If an `onAuthRequired` implementation swallows the error (or doesn't `await` `verifyOtp`), `_authPromise` will never settle, and `await promise` in `handleAuthRequired` will hang indefinitely — breaking the invariant that all auth-failure paths explicitly reject the promise.
Route JWT auth errors through `handleOnboardingVerificationFailure` as well to preserve the invariant.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (6): Last reviewed commit: "chore(wallets): remove customer-specific..." | Re-trigger Greptile |
…ailure and skip OTP reissue for auth errors
|
Reviews (7): Last reviewed commit: "fix(wallets): route auth errors through ..." | Re-trigger Greptile |
Description
This fixes the non-custodial signer TEE path so that JWTs refreshed while a
get-status,start-onboarding,complete-onboarding, orexport-signerrequest is in flight are picked up by retries instead of the request continuing with a stale token.The signer TEE can be initialized with a JWT that is valid when the request starts, but if
setJwtupdates the token while the request is still pending, the previous code passed the original JWT string into the TEEsendActionargs.WebViewParent/EventEmitterretries the sameargsobject, so the old JWT could reach the Crossmint backend and result in a401 ERROR_JWT_EXPIREDsurfaced as a genericSignerStatusError: HTTP 401.Changes:
createAuthData()returns anauthDataobject whosejwtproperty is a getter. JSON serialization (React Native) and structured clone (browser) evaluate getters at serialization time, so every retry reads the latestcrossmint.jwteven ifsetJwtis called while the request is pending.sendActionWithRetry()wraps the TEEsendActioncalls and retries once when the response is an error andcrossmint.jwthas changed since the request started.throwIfCrossmintApiAuthError()is called before falling back toSignerStatusError/OtpValidationError/KeyExportErrorsoJWTExpiredErroris surfaced when the signer frame reportsERROR_JWT_EXPIRED.sendMessageWithOtp/verifyOtpare routed throughhandleOnboardingVerificationFailure()so_authPromiseis always settled, and the OTP reissue path is skipped forNotAuthorizedErrors (re-issuing an OTP cannot fix an expired JWT).throwIfCrossmintApiAuthError()now readsexpiredAt/identifierKeyfrom the nesteddatafield as well as the top-level response, so it tolerates both response shapes.A companion PR in
Crossmint/open-signerpropagates the backendERROR_JWT_EXPIREDcode throughCrossmintHttpErrorto the SDK event response; this SDK change is defensive and already handles the retry via the live getter.Test plan
ncs-signer.test.ts:authData.jwtreads the latest value fromcrossmint.jwtaftersetJwt.get-status/export-signerretries once when the JWT is refreshed while the request is pending.ensureAuthenticated/_exportPrivateKeythrowsJWTExpiredErrorwhen the signer frame returnscode: "ERROR_JWT_EXPIRED".sendActionWithRetrylogs awalletsLogger.infoline when it retries after a JWT refresh.verifyOtprejects_authPromisewithJWTExpiredErroreven whenonAuthRequireddoes not await/propagate theverifycallback.utils/errors.test.tsfor readingexpiredAtfromresponse.data.pnpm test:vitestinpackages/wallets— all 670 tests pass (68 skipped).pnpm linton the modified files — clean.Package updates
.changeset/stale-jwt-retry.mdfor@crossmint/wallets-sdkpatch.Link to Devin session: https://crossmint.devinenterprise.com/sessions/9c62083f8c3b45c1aeb0001fb1325120
Requested by: @jcurbelo