@rakomi/react-native 0.6.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/THIRD-PARTY-NOTICES +1 -1
- package/dist/index.cjs +114 -49
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +115 -50
- package/package.json +3 -3
- package/sbom.cdx.json +6 -6
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;
|
|
@@ -537,7 +557,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
537
557
|
(keySet) => jose.jwtVerify(token, keySet, {
|
|
538
558
|
issuer: this.expectedIssuer,
|
|
539
559
|
audience: this.expectedAudience,
|
|
540
|
-
algorithms: [
|
|
560
|
+
algorithms: [...sdkCore.ALLOWED_SIGNING_ALGORITHMS]
|
|
541
561
|
})
|
|
542
562
|
);
|
|
543
563
|
return { ok: true, payload: result.payload };
|
|
@@ -567,7 +587,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
567
587
|
* simply a dev pointed at a different host between builds), a slot keyed by tenant alone would
|
|
568
588
|
* silently reuse a PREVIOUS host's persisted document as this instance's "fresh" cache on cold
|
|
569
589
|
* start — cross-host reuse this offline-verification path must never do. The host segment is not
|
|
570
|
-
* 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
|
|
571
591
|
* needs to be a stable, DIFFERENT key per host, not a cryptographically-opaque one.
|
|
572
592
|
*/
|
|
573
593
|
async resolveJwksKey() {
|
|
@@ -579,11 +599,25 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
579
599
|
}
|
|
580
600
|
jwksHost() {
|
|
581
601
|
try {
|
|
582
|
-
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}`;
|
|
583
606
|
} catch {
|
|
584
607
|
return null;
|
|
585
608
|
}
|
|
586
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
|
+
}
|
|
587
621
|
async getOrCreateJwksCache() {
|
|
588
622
|
if (this.jwksCache) return this.jwksCache;
|
|
589
623
|
const key = await this.resolveJwksKey();
|
|
@@ -601,7 +635,10 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
601
635
|
this.jwksCache = sdkCore.createJwksCache({
|
|
602
636
|
ttlMs: this.jwksTtlMs,
|
|
603
637
|
fetchJwks: async () => {
|
|
604
|
-
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
|
+
});
|
|
605
642
|
if (!response.ok) {
|
|
606
643
|
throw new Error(`JWKS fetch failed: HTTP ${response.status}`);
|
|
607
644
|
}
|
|
@@ -680,10 +717,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
680
717
|
if (!dResult.ok) {
|
|
681
718
|
return this.handleDpopRefreshFailure(dResult.error, opts.duringRestore ?? false);
|
|
682
719
|
}
|
|
683
|
-
this.
|
|
684
|
-
this.refreshAttempts = 0;
|
|
685
|
-
this.offlineStaleCycles = 0;
|
|
686
|
-
await this.setTokens(dResult.tokens, opts.duringRestore ? "restore" : "refresh");
|
|
720
|
+
await this.completeRefresh(dResult.tokens, opts.duringRestore ?? false);
|
|
687
721
|
return true;
|
|
688
722
|
}
|
|
689
723
|
const result = await sdkCore.refreshAccessToken({
|
|
@@ -700,12 +734,26 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
700
734
|
}
|
|
701
735
|
return this.scheduleRetry(result.error, opts.duringRestore ?? false);
|
|
702
736
|
}
|
|
703
|
-
this.
|
|
704
|
-
this.refreshAttempts = 0;
|
|
705
|
-
this.offlineStaleCycles = 0;
|
|
706
|
-
await this.setTokens(result.tokens, opts.duringRestore ? "restore" : "refresh");
|
|
737
|
+
await this.completeRefresh(result.tokens, opts.duringRestore ?? false);
|
|
707
738
|
return true;
|
|
708
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
|
+
}
|
|
709
757
|
isAuthError(error) {
|
|
710
758
|
return error.code === "REFRESH_FAILED" && (error.reason === "expired" || error.reason === "revoked");
|
|
711
759
|
}
|
|
@@ -718,7 +766,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
718
766
|
async handleDpopRefreshFailure(error, duringRestore) {
|
|
719
767
|
this.emitEvent?.({ type: "dpop_refresh_failed", metadata: { code: error.code } });
|
|
720
768
|
if (error.class === "network") {
|
|
721
|
-
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);
|
|
722
770
|
}
|
|
723
771
|
if (error.class === "dpop_prover_unavailable") {
|
|
724
772
|
this.emitEvent?.({ type: "refresh_failed", metadata: { reason: "dpop_prover_unavailable" } });
|
|
@@ -727,36 +775,53 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
727
775
|
await this.clear({ code: "REFRESH_FAILED", reason: "revoked", message: error.message });
|
|
728
776
|
return false;
|
|
729
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
|
+
*/
|
|
730
783
|
scheduleRetry(error, duringRestore) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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 });
|
|
740
797
|
this.dispatch({ type: "OFFLINE_STALE" });
|
|
741
|
-
return false;
|
|
742
798
|
}
|
|
743
|
-
void this.clear({ code: "REFRESH_FAILED", reason: "network", message: "Refresh exhausted retries" });
|
|
744
799
|
return false;
|
|
745
800
|
}
|
|
746
|
-
const delay = RETRY_DELAYS_MS[this.refreshAttempts];
|
|
747
801
|
this.refreshAttempts++;
|
|
748
802
|
this.emitEvent?.({ type: "network_retry", metadata: { delayMs: delay, attempt: this.refreshAttempts } });
|
|
749
|
-
if (!
|
|
750
|
-
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error });
|
|
803
|
+
if (!restoring) {
|
|
804
|
+
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error: transient });
|
|
751
805
|
}
|
|
752
806
|
if (this.retryTimer) this.clearTimeoutFn(this.retryTimer);
|
|
753
807
|
this.retryTimer = this.setTimeoutFn(() => {
|
|
754
808
|
this.retryTimer = null;
|
|
755
|
-
void this.performRefresh();
|
|
809
|
+
void this.performRefresh({ duringRestore: this.restorePending });
|
|
756
810
|
}, delay);
|
|
757
811
|
return false;
|
|
758
812
|
}
|
|
759
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
|
+
}
|
|
760
825
|
|
|
761
826
|
// src/native/expo-adapter.ts
|
|
762
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;
|
|
@@ -536,7 +556,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
536
556
|
(keySet) => jwtVerify(token, keySet, {
|
|
537
557
|
issuer: this.expectedIssuer,
|
|
538
558
|
audience: this.expectedAudience,
|
|
539
|
-
algorithms: [
|
|
559
|
+
algorithms: [...ALLOWED_SIGNING_ALGORITHMS]
|
|
540
560
|
})
|
|
541
561
|
);
|
|
542
562
|
return { ok: true, payload: result.payload };
|
|
@@ -566,7 +586,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
566
586
|
* simply a dev pointed at a different host between builds), a slot keyed by tenant alone would
|
|
567
587
|
* silently reuse a PREVIOUS host's persisted document as this instance's "fresh" cache on cold
|
|
568
588
|
* start — cross-host reuse this offline-verification path must never do. The host segment is not
|
|
569
|
-
* 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
|
|
570
590
|
* needs to be a stable, DIFFERENT key per host, not a cryptographically-opaque one.
|
|
571
591
|
*/
|
|
572
592
|
async resolveJwksKey() {
|
|
@@ -578,11 +598,25 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
578
598
|
}
|
|
579
599
|
jwksHost() {
|
|
580
600
|
try {
|
|
581
|
-
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}`;
|
|
582
605
|
} catch {
|
|
583
606
|
return null;
|
|
584
607
|
}
|
|
585
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
|
+
}
|
|
586
620
|
async getOrCreateJwksCache() {
|
|
587
621
|
if (this.jwksCache) return this.jwksCache;
|
|
588
622
|
const key = await this.resolveJwksKey();
|
|
@@ -600,7 +634,10 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
600
634
|
this.jwksCache = createJwksCache({
|
|
601
635
|
ttlMs: this.jwksTtlMs,
|
|
602
636
|
fetchJwks: async () => {
|
|
603
|
-
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
|
+
});
|
|
604
641
|
if (!response.ok) {
|
|
605
642
|
throw new Error(`JWKS fetch failed: HTTP ${response.status}`);
|
|
606
643
|
}
|
|
@@ -679,10 +716,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
679
716
|
if (!dResult.ok) {
|
|
680
717
|
return this.handleDpopRefreshFailure(dResult.error, opts.duringRestore ?? false);
|
|
681
718
|
}
|
|
682
|
-
this.
|
|
683
|
-
this.refreshAttempts = 0;
|
|
684
|
-
this.offlineStaleCycles = 0;
|
|
685
|
-
await this.setTokens(dResult.tokens, opts.duringRestore ? "restore" : "refresh");
|
|
719
|
+
await this.completeRefresh(dResult.tokens, opts.duringRestore ?? false);
|
|
686
720
|
return true;
|
|
687
721
|
}
|
|
688
722
|
const result = await refreshAccessToken({
|
|
@@ -699,12 +733,26 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
699
733
|
}
|
|
700
734
|
return this.scheduleRetry(result.error, opts.duringRestore ?? false);
|
|
701
735
|
}
|
|
702
|
-
this.
|
|
703
|
-
this.refreshAttempts = 0;
|
|
704
|
-
this.offlineStaleCycles = 0;
|
|
705
|
-
await this.setTokens(result.tokens, opts.duringRestore ? "restore" : "refresh");
|
|
736
|
+
await this.completeRefresh(result.tokens, opts.duringRestore ?? false);
|
|
706
737
|
return true;
|
|
707
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
|
+
}
|
|
708
756
|
isAuthError(error) {
|
|
709
757
|
return error.code === "REFRESH_FAILED" && (error.reason === "expired" || error.reason === "revoked");
|
|
710
758
|
}
|
|
@@ -717,7 +765,7 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
717
765
|
async handleDpopRefreshFailure(error, duringRestore) {
|
|
718
766
|
this.emitEvent?.({ type: "dpop_refresh_failed", metadata: { code: error.code } });
|
|
719
767
|
if (error.class === "network") {
|
|
720
|
-
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);
|
|
721
769
|
}
|
|
722
770
|
if (error.class === "dpop_prover_unavailable") {
|
|
723
771
|
this.emitEvent?.({ type: "refresh_failed", metadata: { reason: "dpop_prover_unavailable" } });
|
|
@@ -726,36 +774,53 @@ var TokenRuntime = class _TokenRuntime {
|
|
|
726
774
|
await this.clear({ code: "REFRESH_FAILED", reason: "revoked", message: error.message });
|
|
727
775
|
return false;
|
|
728
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
|
+
*/
|
|
729
782
|
scheduleRetry(error, duringRestore) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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 });
|
|
739
796
|
this.dispatch({ type: "OFFLINE_STALE" });
|
|
740
|
-
return false;
|
|
741
797
|
}
|
|
742
|
-
void this.clear({ code: "REFRESH_FAILED", reason: "network", message: "Refresh exhausted retries" });
|
|
743
798
|
return false;
|
|
744
799
|
}
|
|
745
|
-
const delay = RETRY_DELAYS_MS[this.refreshAttempts];
|
|
746
800
|
this.refreshAttempts++;
|
|
747
801
|
this.emitEvent?.({ type: "network_retry", metadata: { delayMs: delay, attempt: this.refreshAttempts } });
|
|
748
|
-
if (!
|
|
749
|
-
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error });
|
|
802
|
+
if (!restoring) {
|
|
803
|
+
this.dispatch({ type: "REFRESH_NETWORK_ERROR", error: transient });
|
|
750
804
|
}
|
|
751
805
|
if (this.retryTimer) this.clearTimeoutFn(this.retryTimer);
|
|
752
806
|
this.retryTimer = this.setTimeoutFn(() => {
|
|
753
807
|
this.retryTimer = null;
|
|
754
|
-
void this.performRefresh();
|
|
808
|
+
void this.performRefresh({ duringRestore: this.restorePending });
|
|
755
809
|
}, delay);
|
|
756
810
|
return false;
|
|
757
811
|
}
|
|
758
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
|
+
}
|
|
759
824
|
|
|
760
825
|
// src/native/expo-adapter.ts
|
|
761
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": {
|