@rakomi/react-native 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/THIRD-PARTY-NOTICES +1 -1
- package/dist/index.cjs +122 -55
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +123 -56
- package/package.json +3 -3
- package/sbom.cdx.json +6 -6
package/README.md
CHANGED
|
@@ -211,6 +211,7 @@ have to branch:
|
|
|
211
211
|
| What happened | Code your UI handles |
|
|
212
212
|
|---|---|
|
|
213
213
|
| The user dismissed the sheet | `PASSKEY_CEREMONY_CANCELLED` |
|
|
214
|
+
| Nobody finished the sheet in time | `PASSKEY_CEREMONY_TIMED_OUT` — retryable; the SDK asked your module to cancel the request |
|
|
214
215
|
| The device cannot do passkeys, or no adapter is wired | `PASSKEY_NOT_SUPPORTED` |
|
|
215
216
|
| The credential is already registered for this user | `PASSKEY_ALREADY_REGISTERED` |
|
|
216
217
|
| The action needs a fresh re-authentication first | `PASSKEY_STEP_UP_REQUIRED` |
|
package/THIRD-PARTY-NOTICES
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
|
|
2
2
|
|
|
3
|
-
For the npm package: @rakomi/react-native@0.
|
|
3
|
+
For the npm package: @rakomi/react-native@0.7.0
|
|
4
4
|
|
|
5
5
|
This package's published bundle (`dist/`) INLINES the third-party components listed below.
|
|
6
6
|
Their copyright and permission notices are reproduced verbatim, as required by their licenses
|
package/dist/index.cjs
CHANGED
|
@@ -212,8 +212,8 @@ function createRnHttpClient(options = {}) {
|
|
|
212
212
|
}
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
|
-
function err(cls, message) {
|
|
216
|
-
return { ok: false, error: { class: cls, code: `auth/${cls}`, message } };
|
|
215
|
+
function err(cls, message, cause) {
|
|
216
|
+
return { ok: false, error: { class: cls, code: `auth/${cls}`, message, ...cause && { cause } } };
|
|
217
217
|
}
|
|
218
218
|
function proverUnavailable(message = "DPoP prover unavailable") {
|
|
219
219
|
return err("dpop_prover_unavailable", message);
|
|
@@ -271,32 +271,30 @@ async function refreshWithDpop(input) {
|
|
|
271
271
|
return mapFailure(result, true);
|
|
272
272
|
}
|
|
273
273
|
function mapFailure(result, attachedProof) {
|
|
274
|
-
const message = result.error.message
|
|
274
|
+
const message = "message" in result.error ? result.error.message : "refresh failed";
|
|
275
275
|
if (attachedProof && (result.dpopProofRejected || result.dpopNonce !== void 0)) {
|
|
276
|
-
return err("invalid_dpop_proof", message);
|
|
276
|
+
return err("invalid_dpop_proof", message, result.error);
|
|
277
277
|
}
|
|
278
|
-
if (result.error.code === "REFRESH_FAILED" && result.error.reason === "
|
|
279
|
-
return err("
|
|
278
|
+
if (result.error.code === "REFRESH_FAILED" && (result.error.reason === "revoked" || result.error.reason === "expired")) {
|
|
279
|
+
return err("invalid_refresh_token", message, result.error);
|
|
280
280
|
}
|
|
281
|
-
return err("
|
|
281
|
+
return err("network", message, result.error);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
// src/internal/token-runtime.ts
|
|
285
|
-
|
|
285
|
+
function originOf(url) {
|
|
286
|
+
try {
|
|
287
|
+
return new URL(url).origin;
|
|
288
|
+
} catch {
|
|
289
|
+
return void 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
286
292
|
function bytesToBase64Url(bytes) {
|
|
287
293
|
let binary = "";
|
|
288
294
|
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
289
295
|
const std = btoa(binary);
|
|
290
296
|
return std.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
291
297
|
}
|
|
292
|
-
function deriveDefaultJwksUri(tokenEndpoint) {
|
|
293
|
-
try {
|
|
294
|
-
const url = new URL(tokenEndpoint);
|
|
295
|
-
return `${url.origin}/.well-known/jwks.json`;
|
|
296
|
-
} catch {
|
|
297
|
-
return tokenEndpoint.replace(/\/[^/]*$/, "") + "/.well-known/jwks.json";
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
298
|
var RAKOMI_PLATFORM_ISSUER_DEFAULT = "https://api.rakomi.com";
|
|
301
299
|
var RAKOMI_PLATFORM_AUDIENCE_DEFAULT = "https://api.rakomi.com";
|
|
302
300
|
function deriveDefaultIssuer(options) {
|
|
@@ -335,7 +333,9 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
335
333
|
storageKeyResolved = null;
|
|
336
334
|
jwksKeyResolved = null;
|
|
337
335
|
jwksCache = null;
|
|
338
|
-
|
|
336
|
+
explicitJwksUri;
|
|
337
|
+
deploymentBaseUrl;
|
|
338
|
+
jwksUriResolver;
|
|
339
339
|
jwksTtlMs;
|
|
340
340
|
expectedIssuer;
|
|
341
341
|
expectedAudience;
|
|
@@ -347,8 +347,8 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
347
347
|
refreshAttempts = 0;
|
|
348
348
|
retryTimer = null;
|
|
349
349
|
destroyed = false;
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
lastRefreshError = null;
|
|
351
|
+
restorePending = false;
|
|
352
352
|
submitNonces = /* @__PURE__ */ new Map();
|
|
353
353
|
static NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
354
354
|
static MAX_PENDING_NONCES = 100;
|
|
@@ -369,7 +369,18 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
369
369
|
this.now = options.now ?? Date.now;
|
|
370
370
|
this.setTimeoutFn = options.setTimeout ?? globalThis.setTimeout.bind(globalThis);
|
|
371
371
|
this.clearTimeoutFn = options.clearTimeout ?? globalThis.clearTimeout.bind(globalThis);
|
|
372
|
-
this.
|
|
372
|
+
this.explicitJwksUri = options.jwksUri;
|
|
373
|
+
this.deploymentBaseUrl = options.baseUrl ?? originOf(options.tokenEndpoint);
|
|
374
|
+
this.jwksUriResolver = sdkCore.createIssuerJwksUriResolver({
|
|
375
|
+
fetchDiscoveryDocument: async (url) => {
|
|
376
|
+
const response = await this.http.fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
|
377
|
+
if (response.status === 404) return null;
|
|
378
|
+
if (!response.ok) {
|
|
379
|
+
throw new Error(`discovery fetch failed: HTTP ${response.status}`);
|
|
380
|
+
}
|
|
381
|
+
return response.json();
|
|
382
|
+
}
|
|
383
|
+
});
|
|
373
384
|
this.jwksTtlMs = options.jwksTtlMs ?? 24 * 60 * 60 * 1e3;
|
|
374
385
|
this.expectedIssuer = options.expectedIssuer ?? deriveDefaultIssuer({ baseUrl: options.baseUrl, tokenEndpoint: options.tokenEndpoint });
|
|
375
386
|
this.expectedAudience = options.expectedAudience ?? RAKOMI_PLATFORM_AUDIENCE_DEFAULT;
|
|
@@ -468,7 +479,10 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
468
479
|
}
|
|
469
480
|
const success = await this.performRefresh();
|
|
470
481
|
if (!success || !this.accessToken || this.expiresAtMs === null) {
|
|
471
|
-
return {
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
error: this.lastRefreshError ?? { code: "REFRESH_FAILED", reason: "expired", message: "No valid token available" }
|
|
485
|
+
};
|
|
472
486
|
}
|
|
473
487
|
const expiresIn = Math.max(0, Math.floor((this.expiresAtMs - this.now()) / 1e3));
|
|
474
488
|
return { ok: true, token: this.accessToken, tokenType: "Bearer", headers: {}, expiresIn };
|
|
@@ -492,6 +506,10 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
492
506
|
*/
|
|
493
507
|
async refreshOnForeground() {
|
|
494
508
|
if (this.refreshInFlight) return;
|
|
509
|
+
if (this.restorePending) {
|
|
510
|
+
await this.performRefresh({ duringRestore: true });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
495
513
|
if (!this.currentUser) return;
|
|
496
514
|
this.dispatch({ type: "REFRESH_START" });
|
|
497
515
|
await this.performRefresh();
|
|
@@ -506,6 +524,8 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
506
524
|
this.currentSession = null;
|
|
507
525
|
this.refreshInFlight = null;
|
|
508
526
|
this.refreshAttempts = 0;
|
|
527
|
+
this.lastRefreshError = null;
|
|
528
|
+
this.restorePending = false;
|
|
509
529
|
if (this.retryTimer) {
|
|
510
530
|
this.clearTimeoutFn(this.retryTimer);
|
|
511
531
|
this.retryTimer = null;
|
|
@@ -526,18 +546,20 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
526
546
|
*
|
|
527
547
|
* uses `jose.createLocalJWKSet` so verification is purely local once
|
|
528
548
|
* the JWKS document has been fetched and cached. The cache is preloaded from `KeyValueStore`
|
|
529
|
-
* on cold-start (slot `jwks_cache`) and refreshed on TTL miss
|
|
549
|
+
* on cold-start (slot `jwks_cache`) and refreshed on TTL miss, or once when the token's `kid` is not in
|
|
550
|
+
* the cached set (a rotated key; rate-limited by a cooldown); on persistent network failure
|
|
530
551
|
* the stale-while-error fallback keeps verification working until the cached doc itself expires.
|
|
531
552
|
*/
|
|
532
553
|
async verifyAccessToken(token) {
|
|
533
554
|
try {
|
|
534
555
|
const cache = await this.getOrCreateJwksCache();
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
556
|
+
const result = await cache.withKeyRotationRetry(
|
|
557
|
+
(keySet) => jose.jwtVerify(token, keySet, {
|
|
558
|
+
issuer: this.expectedIssuer,
|
|
559
|
+
audience: this.expectedAudience,
|
|
560
|
+
algorithms: [...sdkCore.ALLOWED_SIGNING_ALGORITHMS]
|
|
561
|
+
})
|
|
562
|
+
);
|
|
541
563
|
return { ok: true, payload: result.payload };
|
|
542
564
|
} catch (err2) {
|
|
543
565
|
return { ok: false, reason: err2 instanceof Error ? err2.message : "verify failed" };
|
|
@@ -565,7 +587,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
565
587
|
* simply a dev pointed at a different host between builds), a slot keyed by tenant alone would
|
|
566
588
|
* silently reuse a PREVIOUS host's persisted document as this instance's "fresh" cache on cold
|
|
567
589
|
* start — cross-host reuse this offline-verification path must never do. The host segment is not
|
|
568
|
-
* secret (it is the caller's own `jwksUri` configuration), so a plain suffix is enough; this only
|
|
590
|
+
* secret (it is the caller's own `jwksUri` or expected-issuer configuration), so a plain suffix is enough; this only
|
|
569
591
|
* needs to be a stable, DIFFERENT key per host, not a cryptographically-opaque one.
|
|
570
592
|
*/
|
|
571
593
|
async resolveJwksKey() {
|
|
@@ -577,11 +599,25 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
577
599
|
}
|
|
578
600
|
jwksHost() {
|
|
579
601
|
try {
|
|
580
|
-
return new URL(this.
|
|
602
|
+
if (this.explicitJwksUri !== void 0) return new URL(this.explicitJwksUri).hostname;
|
|
603
|
+
const issuer = this.expectedIssuer === void 0 ? "" : encodeURIComponent(this.expectedIssuer);
|
|
604
|
+
const origin = this.deploymentBaseUrl === void 0 ? "" : new URL(this.deploymentBaseUrl).hostname;
|
|
605
|
+
return `${origin}.i.${issuer}`;
|
|
581
606
|
} catch {
|
|
582
607
|
return null;
|
|
583
608
|
}
|
|
584
609
|
}
|
|
610
|
+
/** The JWKS document URL: the explicit override, or the expected issuer's discovered `jwks_uri`. */
|
|
611
|
+
async currentJwksUri() {
|
|
612
|
+
if (this.explicitJwksUri !== void 0) return this.explicitJwksUri;
|
|
613
|
+
if (this.expectedIssuer === void 0) {
|
|
614
|
+
throw new Error("no expected issuer to discover signing keys from");
|
|
615
|
+
}
|
|
616
|
+
return this.jwksUriResolver.resolve(
|
|
617
|
+
this.expectedIssuer,
|
|
618
|
+
sdkCore.discoveryOriginFor(this.expectedIssuer, this.deploymentBaseUrl)
|
|
619
|
+
);
|
|
620
|
+
}
|
|
585
621
|
async getOrCreateJwksCache() {
|
|
586
622
|
if (this.jwksCache) return this.jwksCache;
|
|
587
623
|
const key = await this.resolveJwksKey();
|
|
@@ -599,7 +635,10 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
599
635
|
this.jwksCache = sdkCore.createJwksCache({
|
|
600
636
|
ttlMs: this.jwksTtlMs,
|
|
601
637
|
fetchJwks: async () => {
|
|
602
|
-
const response = await this.http.fetch(this.
|
|
638
|
+
const response = await this.http.fetch(await this.currentJwksUri(), {
|
|
639
|
+
method: "GET",
|
|
640
|
+
headers: { Accept: "application/json" }
|
|
641
|
+
});
|
|
603
642
|
if (!response.ok) {
|
|
604
643
|
throw new Error(`JWKS fetch failed: HTTP ${response.status}`);
|
|
605
644
|
}
|
|
@@ -678,10 +717,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
678
717
|
if (!dResult.ok) {
|
|
679
718
|
return this.handleDpopRefreshFailure(dResult.error, opts.duringRestore ?? false);
|
|
680
719
|
}
|
|
681
|
-
this.
|
|
682
|
-
this.refreshAttempts = 0;
|
|
683
|
-
this.offlineStaleCycles = 0;
|
|
684
|
-
await this.setTokens(dResult.tokens, opts.duringRestore ? "restore" : "refresh");
|
|
720
|
+
await this.completeRefresh(dResult.tokens, opts.duringRestore ?? false);
|
|
685
721
|
return true;
|
|
686
722
|
}
|
|
687
723
|
const result = await sdkCore.refreshAccessToken({
|
|
@@ -698,12 +734,26 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
698
734
|
}
|
|
699
735
|
return this.scheduleRetry(result.error, opts.duringRestore ?? false);
|
|
700
736
|
}
|
|
701
|
-
this.
|
|
702
|
-
this.refreshAttempts = 0;
|
|
703
|
-
this.offlineStaleCycles = 0;
|
|
704
|
-
await this.setTokens(result.tokens, opts.duringRestore ? "restore" : "refresh");
|
|
737
|
+
await this.completeRefresh(result.tokens, opts.duringRestore ?? false);
|
|
705
738
|
return true;
|
|
706
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* Commit a successful refresh. A restore that only succeeded on a later retry (the first
|
|
742
|
+
* attempt at cold start hit a transient failure and `restore()` already returned) is completed
|
|
743
|
+
* here: the tokens are applied as a restore and RESTORE_SUCCESS loads the user, instead of a
|
|
744
|
+
* REFRESH_SUCCESS that would mark the machine authenticated with no user.
|
|
745
|
+
*/
|
|
746
|
+
async completeRefresh(tokens, duringRestore) {
|
|
747
|
+
const resumedRestore = this.restorePending;
|
|
748
|
+
this.restorePending = false;
|
|
749
|
+
this.refreshAttempts = 0;
|
|
750
|
+
this.lastRefreshError = null;
|
|
751
|
+
this.emitEvent?.({ type: "refresh_succeeded" });
|
|
752
|
+
await this.setTokens(tokens, duringRestore || resumedRestore ? "restore" : "refresh");
|
|
753
|
+
if (resumedRestore && this.currentUser && this.currentSession) {
|
|
754
|
+
this.dispatch({ type: "RESTORE_SUCCESS", user: this.currentUser, session: this.currentSession });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
707
757
|
isAuthError(error) {
|
|
708
758
|
return error.code === "REFRESH_FAILED" && (error.reason === "expired" || error.reason === "revoked");
|
|
709
759
|
}
|
|
@@ -716,7 +766,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
716
766
|
async handleDpopRefreshFailure(error, duringRestore) {
|
|
717
767
|
this.emitEvent?.({ type: "dpop_refresh_failed", metadata: { code: error.code } });
|
|
718
768
|
if (error.class === "network") {
|
|
719
|
-
return this.scheduleRetry({ code: "REFRESH_FAILED", reason: "network", message: error.message }, duringRestore);
|
|
769
|
+
return this.scheduleRetry(error.cause ?? { code: "REFRESH_FAILED", reason: "network", message: error.message }, duringRestore);
|
|
720
770
|
}
|
|
721
771
|
if (error.class === "dpop_prover_unavailable") {
|
|
722
772
|
this.emitEvent?.({ type: "refresh_failed", metadata: { reason: "dpop_prover_unavailable" } });
|
|
@@ -725,36 +775,53 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
725
775
|
await this.clear({ code: "REFRESH_FAILED", reason: "revoked", message: error.message });
|
|
726
776
|
return false;
|
|
727
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* Back off and retry a transient refresh failure. The session is never cleared here: when the
|
|
780
|
+
* budget is spent the typed transient error is surfaced (state `offline_stale` when signed in)
|
|
781
|
+
* and the stored refresh token waits for the next foreground / reconnect / `getToken()`.
|
|
782
|
+
*/
|
|
728
783
|
scheduleRetry(error, duringRestore) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
784
|
+
const transient = toTransientError(error);
|
|
785
|
+
this.lastRefreshError = transient;
|
|
786
|
+
if (duringRestore) this.restorePending = true;
|
|
787
|
+
const restoring = this.restorePending;
|
|
788
|
+
const delay = sdkCore.refreshRetryDelayMs(this.refreshAttempts, transient.code === "REFRESH_FAILED" ? transient.retryAfterMs : void 0);
|
|
789
|
+
if (delay === void 0) {
|
|
790
|
+
this.refreshAttempts = 0;
|
|
791
|
+
this.emitEvent?.({
|
|
792
|
+
type: "refresh_failed",
|
|
793
|
+
metadata: { reason: "retries_exhausted", ...transient.code === "REFRESH_FAILED" && transient.status !== void 0 && { status: transient.status } }
|
|
794
|
+
});
|
|
795
|
+
if (!restoring) {
|
|
796
|
+
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error: transient });
|
|
738
797
|
this.dispatch({ type: "OFFLINE_STALE" });
|
|
739
|
-
return false;
|
|
740
798
|
}
|
|
741
|
-
void this.clear({ code: "REFRESH_FAILED", reason: "network", message: "Refresh exhausted retries" });
|
|
742
799
|
return false;
|
|
743
800
|
}
|
|
744
|
-
const delay = RETRY_DELAYS_MS[this.refreshAttempts];
|
|
745
801
|
this.refreshAttempts++;
|
|
746
802
|
this.emitEvent?.({ type: "network_retry", metadata: { delayMs: delay, attempt: this.refreshAttempts } });
|
|
747
|
-
if (!
|
|
748
|
-
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error });
|
|
803
|
+
if (!restoring) {
|
|
804
|
+
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error: transient });
|
|
749
805
|
}
|
|
750
806
|
if (this.retryTimer) this.clearTimeoutFn(this.retryTimer);
|
|
751
807
|
this.retryTimer = this.setTimeoutFn(() => {
|
|
752
808
|
this.retryTimer = null;
|
|
753
|
-
void this.performRefresh();
|
|
809
|
+
void this.performRefresh({ duringRestore: this.restorePending });
|
|
754
810
|
}, delay);
|
|
755
811
|
return false;
|
|
756
812
|
}
|
|
757
813
|
};
|
|
814
|
+
function toTransientError(error) {
|
|
815
|
+
if (error.code === "REFRESH_FAILED" && error.reason === "network") return error;
|
|
816
|
+
return {
|
|
817
|
+
code: "REFRESH_FAILED",
|
|
818
|
+
reason: "network",
|
|
819
|
+
message: sdkCore.getErrorMessage(error),
|
|
820
|
+
..."status" in error && error.status !== void 0 && { status: error.status },
|
|
821
|
+
..."oauthError" in error && typeof error.oauthError === "string" && { oauthError: error.oauthError },
|
|
822
|
+
...error.requestId !== void 0 && { requestId: error.requestId }
|
|
823
|
+
};
|
|
824
|
+
}
|
|
758
825
|
|
|
759
826
|
// src/native/expo-adapter.ts
|
|
760
827
|
function createDefaultExpoAdapter(options = {}) {
|
package/dist/index.d.cts
CHANGED
|
@@ -455,7 +455,13 @@ interface UserProfileProps {
|
|
|
455
455
|
}
|
|
456
456
|
declare function UserProfile(props: UserProfileProps): ReactNode;
|
|
457
457
|
|
|
458
|
-
/**
|
|
458
|
+
/**
|
|
459
|
+
* RFC 9449 / three-class client-side refresh failure taxonomy (+ network).
|
|
460
|
+
*
|
|
461
|
+
* `network` is every transient failure — no connection, a 5xx, 429 or 408, an unreadable
|
|
462
|
+
* response — and also any rejection that is not definitive. The session is kept and retried;
|
|
463
|
+
* only `invalid_refresh_token` and `invalid_dpop_proof` end it.
|
|
464
|
+
*/
|
|
459
465
|
type DpopRefreshErrorClass = 'dpop_prover_unavailable' | 'invalid_dpop_proof' | 'invalid_refresh_token' | 'network';
|
|
460
466
|
interface DpopRefreshError {
|
|
461
467
|
/** Stable machine token for the failure class. */
|
|
@@ -463,6 +469,8 @@ interface DpopRefreshError {
|
|
|
463
469
|
/** Full `auth/<class>` code string — parity with `@rakomi/node`'s surfacing. */
|
|
464
470
|
code: `auth/${DpopRefreshErrorClass}`;
|
|
465
471
|
message: string;
|
|
472
|
+
/** The underlying token-endpoint failure (HTTP status, OAuth `error`, `Retry-After`), when there was one. */
|
|
473
|
+
cause?: AuthError;
|
|
466
474
|
}
|
|
467
475
|
type DpopRefreshResult = {
|
|
468
476
|
ok: true;
|
package/dist/index.d.ts
CHANGED
|
@@ -455,7 +455,13 @@ interface UserProfileProps {
|
|
|
455
455
|
}
|
|
456
456
|
declare function UserProfile(props: UserProfileProps): ReactNode;
|
|
457
457
|
|
|
458
|
-
/**
|
|
458
|
+
/**
|
|
459
|
+
* RFC 9449 / three-class client-side refresh failure taxonomy (+ network).
|
|
460
|
+
*
|
|
461
|
+
* `network` is every transient failure — no connection, a 5xx, 429 or 408, an unreadable
|
|
462
|
+
* response — and also any rejection that is not definitive. The session is kept and retried;
|
|
463
|
+
* only `invalid_refresh_token` and `invalid_dpop_proof` end it.
|
|
464
|
+
*/
|
|
459
465
|
type DpopRefreshErrorClass = 'dpop_prover_unavailable' | 'invalid_dpop_proof' | 'invalid_refresh_token' | 'network';
|
|
460
466
|
interface DpopRefreshError {
|
|
461
467
|
/** Stable machine token for the failure class. */
|
|
@@ -463,6 +469,8 @@ interface DpopRefreshError {
|
|
|
463
469
|
/** Full `auth/<class>` code string — parity with `@rakomi/node`'s surfacing. */
|
|
464
470
|
code: `auth/${DpopRefreshErrorClass}`;
|
|
465
471
|
message: string;
|
|
472
|
+
/** The underlying token-endpoint failure (HTTP status, OAuth `error`, `Retry-After`), when there was one. */
|
|
473
|
+
cause?: AuthError;
|
|
466
474
|
}
|
|
467
475
|
type DpopRefreshResult = {
|
|
468
476
|
ok: true;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useMemo, useReducer, useRef, useEffect, useState, useCallback, useContext } from 'react';
|
|
2
|
-
import { createAuthorizationEndpointCache, EventLog, reduce, INITIAL_SNAPSHOT, createTranslator, isSignedIn, hasPermission, hasRole, extractOrgClaims, parseAuthConfigResponse, generatePkce, issueState, buildAuthorizationUrl, consumeState, exchangeAuthCode, parseOAuthCallbackError, scorePassword, isSafeUrl, passkeyError, decodeUser, assertPasskey, stepUpWithPasskey, registerPasskey, listPasskeys, renamePasskey, deletePasskey, verifyMagicLink, requestMagicLink, verifyEmailOtp, requestEmailOtp, register, verifyTotp, MfaStepUpUnavailableError, extractRequestId, decodeSession, deriveTenantStorageKey, createJwksCache, refreshAccessToken, decodeJwtPayload, PasskeyCeremonyError, resolveExpectedIssuer } from '@rakomi/sdk-core';
|
|
2
|
+
import { createAuthorizationEndpointCache, EventLog, reduce, INITIAL_SNAPSHOT, createTranslator, isSignedIn, hasPermission, hasRole, extractOrgClaims, parseAuthConfigResponse, generatePkce, issueState, buildAuthorizationUrl, consumeState, exchangeAuthCode, parseOAuthCallbackError, scorePassword, isSafeUrl, passkeyError, decodeUser, assertPasskey, stepUpWithPasskey, registerPasskey, listPasskeys, renamePasskey, deletePasskey, verifyMagicLink, requestMagicLink, verifyEmailOtp, requestEmailOtp, register, verifyTotp, MfaStepUpUnavailableError, extractRequestId, createIssuerJwksUriResolver, decodeSession, ALLOWED_SIGNING_ALGORITHMS, deriveTenantStorageKey, discoveryOriginFor, createJwksCache, refreshAccessToken, refreshRetryDelayMs, decodeJwtPayload, PasskeyCeremonyError, resolveExpectedIssuer, getErrorMessage } from '@rakomi/sdk-core';
|
|
3
3
|
export { MfaStepUpRequiredError, MfaStepUpUnavailableError, base64url, createAuthorizationEndpointCache, createTranslator, decodeJwtPayload, decodeSession, decodeUser, deriveAuthorizationEndpointFallback, generatePkce, getErrorMessage, hasPermission, hasRole, isSafeUrl, networkError, parseOAuthCallbackError, parseTokenEndpointError, scorePassword, selectPluralForm } from '@rakomi/sdk-core';
|
|
4
4
|
import { jwtVerify } from 'jose';
|
|
5
5
|
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
@@ -211,8 +211,8 @@ function createRnHttpClient(options = {}) {
|
|
|
211
211
|
}
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
|
-
function err(cls, message) {
|
|
215
|
-
return { ok: false, error: { class: cls, code: `auth/${cls}`, message } };
|
|
214
|
+
function err(cls, message, cause) {
|
|
215
|
+
return { ok: false, error: { class: cls, code: `auth/${cls}`, message, ...cause && { cause } } };
|
|
216
216
|
}
|
|
217
217
|
function proverUnavailable(message = "DPoP prover unavailable") {
|
|
218
218
|
return err("dpop_prover_unavailable", message);
|
|
@@ -270,32 +270,30 @@ async function refreshWithDpop(input) {
|
|
|
270
270
|
return mapFailure(result, true);
|
|
271
271
|
}
|
|
272
272
|
function mapFailure(result, attachedProof) {
|
|
273
|
-
const message = result.error.message
|
|
273
|
+
const message = "message" in result.error ? result.error.message : "refresh failed";
|
|
274
274
|
if (attachedProof && (result.dpopProofRejected || result.dpopNonce !== void 0)) {
|
|
275
|
-
return err("invalid_dpop_proof", message);
|
|
275
|
+
return err("invalid_dpop_proof", message, result.error);
|
|
276
276
|
}
|
|
277
|
-
if (result.error.code === "REFRESH_FAILED" && result.error.reason === "
|
|
278
|
-
return err("
|
|
277
|
+
if (result.error.code === "REFRESH_FAILED" && (result.error.reason === "revoked" || result.error.reason === "expired")) {
|
|
278
|
+
return err("invalid_refresh_token", message, result.error);
|
|
279
279
|
}
|
|
280
|
-
return err("
|
|
280
|
+
return err("network", message, result.error);
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
// src/internal/token-runtime.ts
|
|
284
|
-
|
|
284
|
+
function originOf(url) {
|
|
285
|
+
try {
|
|
286
|
+
return new URL(url).origin;
|
|
287
|
+
} catch {
|
|
288
|
+
return void 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
285
291
|
function bytesToBase64Url(bytes) {
|
|
286
292
|
let binary = "";
|
|
287
293
|
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
288
294
|
const std = btoa(binary);
|
|
289
295
|
return std.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
290
296
|
}
|
|
291
|
-
function deriveDefaultJwksUri(tokenEndpoint) {
|
|
292
|
-
try {
|
|
293
|
-
const url = new URL(tokenEndpoint);
|
|
294
|
-
return `${url.origin}/.well-known/jwks.json`;
|
|
295
|
-
} catch {
|
|
296
|
-
return tokenEndpoint.replace(/\/[^/]*$/, "") + "/.well-known/jwks.json";
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
297
|
var RAKOMI_PLATFORM_ISSUER_DEFAULT = "https://api.rakomi.com";
|
|
300
298
|
var RAKOMI_PLATFORM_AUDIENCE_DEFAULT = "https://api.rakomi.com";
|
|
301
299
|
function deriveDefaultIssuer(options) {
|
|
@@ -334,7 +332,9 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
334
332
|
storageKeyResolved = null;
|
|
335
333
|
jwksKeyResolved = null;
|
|
336
334
|
jwksCache = null;
|
|
337
|
-
|
|
335
|
+
explicitJwksUri;
|
|
336
|
+
deploymentBaseUrl;
|
|
337
|
+
jwksUriResolver;
|
|
338
338
|
jwksTtlMs;
|
|
339
339
|
expectedIssuer;
|
|
340
340
|
expectedAudience;
|
|
@@ -346,8 +346,8 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
346
346
|
refreshAttempts = 0;
|
|
347
347
|
retryTimer = null;
|
|
348
348
|
destroyed = false;
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
lastRefreshError = null;
|
|
350
|
+
restorePending = false;
|
|
351
351
|
submitNonces = /* @__PURE__ */ new Map();
|
|
352
352
|
static NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
353
353
|
static MAX_PENDING_NONCES = 100;
|
|
@@ -368,7 +368,18 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
368
368
|
this.now = options.now ?? Date.now;
|
|
369
369
|
this.setTimeoutFn = options.setTimeout ?? globalThis.setTimeout.bind(globalThis);
|
|
370
370
|
this.clearTimeoutFn = options.clearTimeout ?? globalThis.clearTimeout.bind(globalThis);
|
|
371
|
-
this.
|
|
371
|
+
this.explicitJwksUri = options.jwksUri;
|
|
372
|
+
this.deploymentBaseUrl = options.baseUrl ?? originOf(options.tokenEndpoint);
|
|
373
|
+
this.jwksUriResolver = createIssuerJwksUriResolver({
|
|
374
|
+
fetchDiscoveryDocument: async (url) => {
|
|
375
|
+
const response = await this.http.fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
|
376
|
+
if (response.status === 404) return null;
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
throw new Error(`discovery fetch failed: HTTP ${response.status}`);
|
|
379
|
+
}
|
|
380
|
+
return response.json();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
372
383
|
this.jwksTtlMs = options.jwksTtlMs ?? 24 * 60 * 60 * 1e3;
|
|
373
384
|
this.expectedIssuer = options.expectedIssuer ?? deriveDefaultIssuer({ baseUrl: options.baseUrl, tokenEndpoint: options.tokenEndpoint });
|
|
374
385
|
this.expectedAudience = options.expectedAudience ?? RAKOMI_PLATFORM_AUDIENCE_DEFAULT;
|
|
@@ -467,7 +478,10 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
467
478
|
}
|
|
468
479
|
const success = await this.performRefresh();
|
|
469
480
|
if (!success || !this.accessToken || this.expiresAtMs === null) {
|
|
470
|
-
return {
|
|
481
|
+
return {
|
|
482
|
+
ok: false,
|
|
483
|
+
error: this.lastRefreshError ?? { code: "REFRESH_FAILED", reason: "expired", message: "No valid token available" }
|
|
484
|
+
};
|
|
471
485
|
}
|
|
472
486
|
const expiresIn = Math.max(0, Math.floor((this.expiresAtMs - this.now()) / 1e3));
|
|
473
487
|
return { ok: true, token: this.accessToken, tokenType: "Bearer", headers: {}, expiresIn };
|
|
@@ -491,6 +505,10 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
491
505
|
*/
|
|
492
506
|
async refreshOnForeground() {
|
|
493
507
|
if (this.refreshInFlight) return;
|
|
508
|
+
if (this.restorePending) {
|
|
509
|
+
await this.performRefresh({ duringRestore: true });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
494
512
|
if (!this.currentUser) return;
|
|
495
513
|
this.dispatch({ type: "REFRESH_START" });
|
|
496
514
|
await this.performRefresh();
|
|
@@ -505,6 +523,8 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
505
523
|
this.currentSession = null;
|
|
506
524
|
this.refreshInFlight = null;
|
|
507
525
|
this.refreshAttempts = 0;
|
|
526
|
+
this.lastRefreshError = null;
|
|
527
|
+
this.restorePending = false;
|
|
508
528
|
if (this.retryTimer) {
|
|
509
529
|
this.clearTimeoutFn(this.retryTimer);
|
|
510
530
|
this.retryTimer = null;
|
|
@@ -525,18 +545,20 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
525
545
|
*
|
|
526
546
|
* uses `jose.createLocalJWKSet` so verification is purely local once
|
|
527
547
|
* the JWKS document has been fetched and cached. The cache is preloaded from `KeyValueStore`
|
|
528
|
-
* on cold-start (slot `jwks_cache`) and refreshed on TTL miss
|
|
548
|
+
* on cold-start (slot `jwks_cache`) and refreshed on TTL miss, or once when the token's `kid` is not in
|
|
549
|
+
* the cached set (a rotated key; rate-limited by a cooldown); on persistent network failure
|
|
529
550
|
* the stale-while-error fallback keeps verification working until the cached doc itself expires.
|
|
530
551
|
*/
|
|
531
552
|
async verifyAccessToken(token) {
|
|
532
553
|
try {
|
|
533
554
|
const cache = await this.getOrCreateJwksCache();
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
555
|
+
const result = await cache.withKeyRotationRetry(
|
|
556
|
+
(keySet) => jwtVerify(token, keySet, {
|
|
557
|
+
issuer: this.expectedIssuer,
|
|
558
|
+
audience: this.expectedAudience,
|
|
559
|
+
algorithms: [...ALLOWED_SIGNING_ALGORITHMS]
|
|
560
|
+
})
|
|
561
|
+
);
|
|
540
562
|
return { ok: true, payload: result.payload };
|
|
541
563
|
} catch (err2) {
|
|
542
564
|
return { ok: false, reason: err2 instanceof Error ? err2.message : "verify failed" };
|
|
@@ -564,7 +586,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
564
586
|
* simply a dev pointed at a different host between builds), a slot keyed by tenant alone would
|
|
565
587
|
* silently reuse a PREVIOUS host's persisted document as this instance's "fresh" cache on cold
|
|
566
588
|
* start — cross-host reuse this offline-verification path must never do. The host segment is not
|
|
567
|
-
* secret (it is the caller's own `jwksUri` configuration), so a plain suffix is enough; this only
|
|
589
|
+
* secret (it is the caller's own `jwksUri` or expected-issuer configuration), so a plain suffix is enough; this only
|
|
568
590
|
* needs to be a stable, DIFFERENT key per host, not a cryptographically-opaque one.
|
|
569
591
|
*/
|
|
570
592
|
async resolveJwksKey() {
|
|
@@ -576,11 +598,25 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
576
598
|
}
|
|
577
599
|
jwksHost() {
|
|
578
600
|
try {
|
|
579
|
-
return new URL(this.
|
|
601
|
+
if (this.explicitJwksUri !== void 0) return new URL(this.explicitJwksUri).hostname;
|
|
602
|
+
const issuer = this.expectedIssuer === void 0 ? "" : encodeURIComponent(this.expectedIssuer);
|
|
603
|
+
const origin = this.deploymentBaseUrl === void 0 ? "" : new URL(this.deploymentBaseUrl).hostname;
|
|
604
|
+
return `${origin}.i.${issuer}`;
|
|
580
605
|
} catch {
|
|
581
606
|
return null;
|
|
582
607
|
}
|
|
583
608
|
}
|
|
609
|
+
/** The JWKS document URL: the explicit override, or the expected issuer's discovered `jwks_uri`. */
|
|
610
|
+
async currentJwksUri() {
|
|
611
|
+
if (this.explicitJwksUri !== void 0) return this.explicitJwksUri;
|
|
612
|
+
if (this.expectedIssuer === void 0) {
|
|
613
|
+
throw new Error("no expected issuer to discover signing keys from");
|
|
614
|
+
}
|
|
615
|
+
return this.jwksUriResolver.resolve(
|
|
616
|
+
this.expectedIssuer,
|
|
617
|
+
discoveryOriginFor(this.expectedIssuer, this.deploymentBaseUrl)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
584
620
|
async getOrCreateJwksCache() {
|
|
585
621
|
if (this.jwksCache) return this.jwksCache;
|
|
586
622
|
const key = await this.resolveJwksKey();
|
|
@@ -598,7 +634,10 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
598
634
|
this.jwksCache = createJwksCache({
|
|
599
635
|
ttlMs: this.jwksTtlMs,
|
|
600
636
|
fetchJwks: async () => {
|
|
601
|
-
const response = await this.http.fetch(this.
|
|
637
|
+
const response = await this.http.fetch(await this.currentJwksUri(), {
|
|
638
|
+
method: "GET",
|
|
639
|
+
headers: { Accept: "application/json" }
|
|
640
|
+
});
|
|
602
641
|
if (!response.ok) {
|
|
603
642
|
throw new Error(`JWKS fetch failed: HTTP ${response.status}`);
|
|
604
643
|
}
|
|
@@ -677,10 +716,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
677
716
|
if (!dResult.ok) {
|
|
678
717
|
return this.handleDpopRefreshFailure(dResult.error, opts.duringRestore ?? false);
|
|
679
718
|
}
|
|
680
|
-
this.
|
|
681
|
-
this.refreshAttempts = 0;
|
|
682
|
-
this.offlineStaleCycles = 0;
|
|
683
|
-
await this.setTokens(dResult.tokens, opts.duringRestore ? "restore" : "refresh");
|
|
719
|
+
await this.completeRefresh(dResult.tokens, opts.duringRestore ?? false);
|
|
684
720
|
return true;
|
|
685
721
|
}
|
|
686
722
|
const result = await refreshAccessToken({
|
|
@@ -697,12 +733,26 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
697
733
|
}
|
|
698
734
|
return this.scheduleRetry(result.error, opts.duringRestore ?? false);
|
|
699
735
|
}
|
|
700
|
-
this.
|
|
701
|
-
this.refreshAttempts = 0;
|
|
702
|
-
this.offlineStaleCycles = 0;
|
|
703
|
-
await this.setTokens(result.tokens, opts.duringRestore ? "restore" : "refresh");
|
|
736
|
+
await this.completeRefresh(result.tokens, opts.duringRestore ?? false);
|
|
704
737
|
return true;
|
|
705
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* Commit a successful refresh. A restore that only succeeded on a later retry (the first
|
|
741
|
+
* attempt at cold start hit a transient failure and `restore()` already returned) is completed
|
|
742
|
+
* here: the tokens are applied as a restore and RESTORE_SUCCESS loads the user, instead of a
|
|
743
|
+
* REFRESH_SUCCESS that would mark the machine authenticated with no user.
|
|
744
|
+
*/
|
|
745
|
+
async completeRefresh(tokens, duringRestore) {
|
|
746
|
+
const resumedRestore = this.restorePending;
|
|
747
|
+
this.restorePending = false;
|
|
748
|
+
this.refreshAttempts = 0;
|
|
749
|
+
this.lastRefreshError = null;
|
|
750
|
+
this.emitEvent?.({ type: "refresh_succeeded" });
|
|
751
|
+
await this.setTokens(tokens, duringRestore || resumedRestore ? "restore" : "refresh");
|
|
752
|
+
if (resumedRestore && this.currentUser && this.currentSession) {
|
|
753
|
+
this.dispatch({ type: "RESTORE_SUCCESS", user: this.currentUser, session: this.currentSession });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
706
756
|
isAuthError(error) {
|
|
707
757
|
return error.code === "REFRESH_FAILED" && (error.reason === "expired" || error.reason === "revoked");
|
|
708
758
|
}
|
|
@@ -715,7 +765,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
715
765
|
async handleDpopRefreshFailure(error, duringRestore) {
|
|
716
766
|
this.emitEvent?.({ type: "dpop_refresh_failed", metadata: { code: error.code } });
|
|
717
767
|
if (error.class === "network") {
|
|
718
|
-
return this.scheduleRetry({ code: "REFRESH_FAILED", reason: "network", message: error.message }, duringRestore);
|
|
768
|
+
return this.scheduleRetry(error.cause ?? { code: "REFRESH_FAILED", reason: "network", message: error.message }, duringRestore);
|
|
719
769
|
}
|
|
720
770
|
if (error.class === "dpop_prover_unavailable") {
|
|
721
771
|
this.emitEvent?.({ type: "refresh_failed", metadata: { reason: "dpop_prover_unavailable" } });
|
|
@@ -724,36 +774,53 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
724
774
|
await this.clear({ code: "REFRESH_FAILED", reason: "revoked", message: error.message });
|
|
725
775
|
return false;
|
|
726
776
|
}
|
|
777
|
+
/**
|
|
778
|
+
* Back off and retry a transient refresh failure. The session is never cleared here: when the
|
|
779
|
+
* budget is spent the typed transient error is surfaced (state `offline_stale` when signed in)
|
|
780
|
+
* and the stored refresh token waits for the next foreground / reconnect / `getToken()`.
|
|
781
|
+
*/
|
|
727
782
|
scheduleRetry(error, duringRestore) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
783
|
+
const transient = toTransientError(error);
|
|
784
|
+
this.lastRefreshError = transient;
|
|
785
|
+
if (duringRestore) this.restorePending = true;
|
|
786
|
+
const restoring = this.restorePending;
|
|
787
|
+
const delay = refreshRetryDelayMs(this.refreshAttempts, transient.code === "REFRESH_FAILED" ? transient.retryAfterMs : void 0);
|
|
788
|
+
if (delay === void 0) {
|
|
789
|
+
this.refreshAttempts = 0;
|
|
790
|
+
this.emitEvent?.({
|
|
791
|
+
type: "refresh_failed",
|
|
792
|
+
metadata: { reason: "retries_exhausted", ...transient.code === "REFRESH_FAILED" && transient.status !== void 0 && { status: transient.status } }
|
|
793
|
+
});
|
|
794
|
+
if (!restoring) {
|
|
795
|
+
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error: transient });
|
|
737
796
|
this.dispatch({ type: "OFFLINE_STALE" });
|
|
738
|
-
return false;
|
|
739
797
|
}
|
|
740
|
-
void this.clear({ code: "REFRESH_FAILED", reason: "network", message: "Refresh exhausted retries" });
|
|
741
798
|
return false;
|
|
742
799
|
}
|
|
743
|
-
const delay = RETRY_DELAYS_MS[this.refreshAttempts];
|
|
744
800
|
this.refreshAttempts++;
|
|
745
801
|
this.emitEvent?.({ type: "network_retry", metadata: { delayMs: delay, attempt: this.refreshAttempts } });
|
|
746
|
-
if (!
|
|
747
|
-
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error });
|
|
802
|
+
if (!restoring) {
|
|
803
|
+
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error: transient });
|
|
748
804
|
}
|
|
749
805
|
if (this.retryTimer) this.clearTimeoutFn(this.retryTimer);
|
|
750
806
|
this.retryTimer = this.setTimeoutFn(() => {
|
|
751
807
|
this.retryTimer = null;
|
|
752
|
-
void this.performRefresh();
|
|
808
|
+
void this.performRefresh({ duringRestore: this.restorePending });
|
|
753
809
|
}, delay);
|
|
754
810
|
return false;
|
|
755
811
|
}
|
|
756
812
|
};
|
|
813
|
+
function toTransientError(error) {
|
|
814
|
+
if (error.code === "REFRESH_FAILED" && error.reason === "network") return error;
|
|
815
|
+
return {
|
|
816
|
+
code: "REFRESH_FAILED",
|
|
817
|
+
reason: "network",
|
|
818
|
+
message: getErrorMessage(error),
|
|
819
|
+
..."status" in error && error.status !== void 0 && { status: error.status },
|
|
820
|
+
..."oauthError" in error && typeof error.oauthError === "string" && { oauthError: error.oauthError },
|
|
821
|
+
...error.requestId !== void 0 && { requestId: error.requestId }
|
|
822
|
+
};
|
|
823
|
+
}
|
|
757
824
|
|
|
758
825
|
// src/native/expo-adapter.ts
|
|
759
826
|
function createDefaultExpoAdapter(options = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rakomi/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "React Native / Expo SDK for Rakomi authentication.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rakomi",
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@rakomi/react": "0.
|
|
86
|
+
"@rakomi/react": "0.7.0"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
89
|
"jose": "^6.2.3",
|
|
90
|
-
"@rakomi/sdk-core": "^0.
|
|
90
|
+
"@rakomi/sdk-core": "^0.7.0"
|
|
91
91
|
},
|
|
92
92
|
"scripts": {
|
|
93
93
|
"build": "NODE_OPTIONS=--max-old-space-size=4096 tsup",
|
package/sbom.cdx.json
CHANGED
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
},
|
|
27
27
|
"component": {
|
|
28
28
|
"type": "library",
|
|
29
|
-
"bom-ref": "pkg:npm/%40rakomi%2Freact-native@0.
|
|
29
|
+
"bom-ref": "pkg:npm/%40rakomi%2Freact-native@0.7.0",
|
|
30
30
|
"name": "@rakomi/react-native",
|
|
31
|
-
"version": "0.
|
|
32
|
-
"purl": "pkg:npm/%40rakomi%2Freact-native@0.
|
|
31
|
+
"version": "0.7.0",
|
|
32
|
+
"purl": "pkg:npm/%40rakomi%2Freact-native@0.7.0",
|
|
33
33
|
"licenses": [
|
|
34
34
|
{
|
|
35
35
|
"license": {
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"components": [
|
|
43
43
|
{
|
|
44
44
|
"type": "library",
|
|
45
|
-
"bom-ref": "pkg:npm/@rakomi/sdk-core@0.
|
|
45
|
+
"bom-ref": "pkg:npm/@rakomi/sdk-core@0.7.0",
|
|
46
46
|
"name": "@rakomi/sdk-core",
|
|
47
|
-
"version": "0.
|
|
48
|
-
"purl": "pkg:npm/@rakomi/sdk-core@0.
|
|
47
|
+
"version": "0.7.0",
|
|
48
|
+
"purl": "pkg:npm/@rakomi/sdk-core@0.7.0",
|
|
49
49
|
"licenses": [
|
|
50
50
|
{
|
|
51
51
|
"license": {
|