@sendoracloud/sdk-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/dist/index.cjs +90 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +90 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -447,6 +447,83 @@ var Auth = class {
|
|
|
447
447
|
this.bearerHeaders()
|
|
448
448
|
);
|
|
449
449
|
}
|
|
450
|
+
// ============================================================
|
|
451
|
+
// OIDC SSO
|
|
452
|
+
// ============================================================
|
|
453
|
+
/**
|
|
454
|
+
* Start OIDC sign-in. Returns the IdP's authorization URL — caller
|
|
455
|
+
* opens it via React Native's `Linking.openURL()` (or
|
|
456
|
+
* `WebBrowser.openAuthSessionAsync` from `expo-web-browser` when
|
|
457
|
+
* inside Expo). The IdP redirects through Sendora's callback to the
|
|
458
|
+
* `returnTo` URL with `#sendora_oidc_token=...` in the fragment.
|
|
459
|
+
*
|
|
460
|
+
* On the customer's app side, `returnTo` MUST be a custom URL scheme
|
|
461
|
+
* registered in Info.plist (iOS) / AndroidManifest.xml intent-filter
|
|
462
|
+
* (Android), e.g. `myapp://oidc-callback`. When the deep link fires,
|
|
463
|
+
* call `consumeSsoFragment(url)` on the captured URL.
|
|
464
|
+
*/
|
|
465
|
+
async startSso(returnTo) {
|
|
466
|
+
const res = await post(
|
|
467
|
+
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
468
|
+
"/api/v1/auth-service/sso/oidc/start",
|
|
469
|
+
{ returnTo }
|
|
470
|
+
);
|
|
471
|
+
if (!res || !res.ok) throw new AuthError("SSO_START_FAILED", `HTTP ${res?.status ?? "network"}`);
|
|
472
|
+
const parsed = await res.json();
|
|
473
|
+
if (!parsed.success || !parsed.data?.authorizationUrl) {
|
|
474
|
+
throw new AuthError("SSO_START_FAILED", "Missing authorizationUrl");
|
|
475
|
+
}
|
|
476
|
+
return { authorizationUrl: parsed.data.authorizationUrl };
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Consume an OIDC callback URL captured via React Native's
|
|
480
|
+
* `Linking.addEventListener('url', ...)`. Reads the fragment for
|
|
481
|
+
* `sendora_oidc_token`, exchanges it for a session, and persists.
|
|
482
|
+
* Throws AuthError on `sendora_oidc_error=...` fragment.
|
|
483
|
+
*/
|
|
484
|
+
consumeSsoFragment(callbackUrl) {
|
|
485
|
+
return this.serialize(async () => {
|
|
486
|
+
if (this.user !== null) await this.wipeLocalIdentity();
|
|
487
|
+
const hashIdx = callbackUrl.indexOf("#");
|
|
488
|
+
const fragment = hashIdx >= 0 ? callbackUrl.slice(hashIdx + 1) : "";
|
|
489
|
+
const params = new URLSearchParams(fragment);
|
|
490
|
+
const err = params.get("sendora_oidc_error");
|
|
491
|
+
if (err) throw new AuthError("SSO_FAILED", decodeURIComponent(err));
|
|
492
|
+
const refresh = params.get("sendora_oidc_token");
|
|
493
|
+
if (!refresh) throw new AuthError("SSO_NO_TOKEN", "Callback URL has no sendora_oidc_token");
|
|
494
|
+
const res = await post(
|
|
495
|
+
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
496
|
+
"/api/v1/auth-service/token/refresh",
|
|
497
|
+
{ refreshToken: refresh }
|
|
498
|
+
);
|
|
499
|
+
if (!res || !res.ok) throw new AuthError("SSO_EXCHANGE_FAILED", `HTTP ${res?.status ?? "network"}`);
|
|
500
|
+
const parsed = await res.json();
|
|
501
|
+
const access = parsed.data?.accessToken;
|
|
502
|
+
const newRefresh = parsed.data?.refreshToken;
|
|
503
|
+
const expiresIn = parsed.data?.expiresIn;
|
|
504
|
+
if (!parsed.success || !access || !newRefresh || !expiresIn || expiresIn <= 0) {
|
|
505
|
+
throw new AuthError("SSO_EXCHANGE_FAILED", "Could not exchange OIDC token");
|
|
506
|
+
}
|
|
507
|
+
const claims = decodeJwtPayload(access);
|
|
508
|
+
const user = {
|
|
509
|
+
id: claims?.sub ?? "",
|
|
510
|
+
email: claims?.email ?? null,
|
|
511
|
+
emailVerified: claims?.email_verified ?? true,
|
|
512
|
+
name: claims?.name ?? null,
|
|
513
|
+
isAnonymous: false
|
|
514
|
+
};
|
|
515
|
+
this.persist({
|
|
516
|
+
user,
|
|
517
|
+
tokens: {
|
|
518
|
+
accessToken: access,
|
|
519
|
+
refreshToken: newRefresh,
|
|
520
|
+
expiresIn,
|
|
521
|
+
tokenType: "Bearer"
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
return user;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
450
527
|
bearerHeaders() {
|
|
451
528
|
if (!this.accessToken) {
|
|
452
529
|
throw new AuthError("NOT_SIGNED_IN", "Not signed in");
|
|
@@ -551,6 +628,19 @@ var Auth = class {
|
|
|
551
628
|
await this.hooks.onAnonymousWipe();
|
|
552
629
|
}
|
|
553
630
|
};
|
|
631
|
+
function decodeJwtPayload(token) {
|
|
632
|
+
try {
|
|
633
|
+
const parts = token.split(".");
|
|
634
|
+
if (parts.length !== 3) return null;
|
|
635
|
+
const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
636
|
+
const pad = (4 - padded.length % 4) % 4;
|
|
637
|
+
if (typeof atob !== "function") return null;
|
|
638
|
+
const json = atob(padded + "===".slice(0, pad));
|
|
639
|
+
return JSON.parse(json);
|
|
640
|
+
} catch {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
554
644
|
|
|
555
645
|
// src/index.ts
|
|
556
646
|
var SDK_VERSION = "0.4.0";
|
package/dist/index.d.cts
CHANGED
|
@@ -160,6 +160,28 @@ declare class Auth {
|
|
|
160
160
|
}>>;
|
|
161
161
|
revokeSession(sessionId: string): Promise<void>;
|
|
162
162
|
revokeAllSessions(): Promise<void>;
|
|
163
|
+
/**
|
|
164
|
+
* Start OIDC sign-in. Returns the IdP's authorization URL — caller
|
|
165
|
+
* opens it via React Native's `Linking.openURL()` (or
|
|
166
|
+
* `WebBrowser.openAuthSessionAsync` from `expo-web-browser` when
|
|
167
|
+
* inside Expo). The IdP redirects through Sendora's callback to the
|
|
168
|
+
* `returnTo` URL with `#sendora_oidc_token=...` in the fragment.
|
|
169
|
+
*
|
|
170
|
+
* On the customer's app side, `returnTo` MUST be a custom URL scheme
|
|
171
|
+
* registered in Info.plist (iOS) / AndroidManifest.xml intent-filter
|
|
172
|
+
* (Android), e.g. `myapp://oidc-callback`. When the deep link fires,
|
|
173
|
+
* call `consumeSsoFragment(url)` on the captured URL.
|
|
174
|
+
*/
|
|
175
|
+
startSso(returnTo: string): Promise<{
|
|
176
|
+
authorizationUrl: string;
|
|
177
|
+
}>;
|
|
178
|
+
/**
|
|
179
|
+
* Consume an OIDC callback URL captured via React Native's
|
|
180
|
+
* `Linking.addEventListener('url', ...)`. Reads the fragment for
|
|
181
|
+
* `sendora_oidc_token`, exchanges it for a session, and persists.
|
|
182
|
+
* Throws AuthError on `sendora_oidc_error=...` fragment.
|
|
183
|
+
*/
|
|
184
|
+
consumeSsoFragment(callbackUrl: string): Promise<AuthUser>;
|
|
163
185
|
private bearerHeaders;
|
|
164
186
|
private unwrap;
|
|
165
187
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -160,6 +160,28 @@ declare class Auth {
|
|
|
160
160
|
}>>;
|
|
161
161
|
revokeSession(sessionId: string): Promise<void>;
|
|
162
162
|
revokeAllSessions(): Promise<void>;
|
|
163
|
+
/**
|
|
164
|
+
* Start OIDC sign-in. Returns the IdP's authorization URL — caller
|
|
165
|
+
* opens it via React Native's `Linking.openURL()` (or
|
|
166
|
+
* `WebBrowser.openAuthSessionAsync` from `expo-web-browser` when
|
|
167
|
+
* inside Expo). The IdP redirects through Sendora's callback to the
|
|
168
|
+
* `returnTo` URL with `#sendora_oidc_token=...` in the fragment.
|
|
169
|
+
*
|
|
170
|
+
* On the customer's app side, `returnTo` MUST be a custom URL scheme
|
|
171
|
+
* registered in Info.plist (iOS) / AndroidManifest.xml intent-filter
|
|
172
|
+
* (Android), e.g. `myapp://oidc-callback`. When the deep link fires,
|
|
173
|
+
* call `consumeSsoFragment(url)` on the captured URL.
|
|
174
|
+
*/
|
|
175
|
+
startSso(returnTo: string): Promise<{
|
|
176
|
+
authorizationUrl: string;
|
|
177
|
+
}>;
|
|
178
|
+
/**
|
|
179
|
+
* Consume an OIDC callback URL captured via React Native's
|
|
180
|
+
* `Linking.addEventListener('url', ...)`. Reads the fragment for
|
|
181
|
+
* `sendora_oidc_token`, exchanges it for a session, and persists.
|
|
182
|
+
* Throws AuthError on `sendora_oidc_error=...` fragment.
|
|
183
|
+
*/
|
|
184
|
+
consumeSsoFragment(callbackUrl: string): Promise<AuthUser>;
|
|
163
185
|
private bearerHeaders;
|
|
164
186
|
private unwrap;
|
|
165
187
|
/**
|
package/dist/index.js
CHANGED
|
@@ -407,6 +407,83 @@ var Auth = class {
|
|
|
407
407
|
this.bearerHeaders()
|
|
408
408
|
);
|
|
409
409
|
}
|
|
410
|
+
// ============================================================
|
|
411
|
+
// OIDC SSO
|
|
412
|
+
// ============================================================
|
|
413
|
+
/**
|
|
414
|
+
* Start OIDC sign-in. Returns the IdP's authorization URL — caller
|
|
415
|
+
* opens it via React Native's `Linking.openURL()` (or
|
|
416
|
+
* `WebBrowser.openAuthSessionAsync` from `expo-web-browser` when
|
|
417
|
+
* inside Expo). The IdP redirects through Sendora's callback to the
|
|
418
|
+
* `returnTo` URL with `#sendora_oidc_token=...` in the fragment.
|
|
419
|
+
*
|
|
420
|
+
* On the customer's app side, `returnTo` MUST be a custom URL scheme
|
|
421
|
+
* registered in Info.plist (iOS) / AndroidManifest.xml intent-filter
|
|
422
|
+
* (Android), e.g. `myapp://oidc-callback`. When the deep link fires,
|
|
423
|
+
* call `consumeSsoFragment(url)` on the captured URL.
|
|
424
|
+
*/
|
|
425
|
+
async startSso(returnTo) {
|
|
426
|
+
const res = await post(
|
|
427
|
+
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
428
|
+
"/api/v1/auth-service/sso/oidc/start",
|
|
429
|
+
{ returnTo }
|
|
430
|
+
);
|
|
431
|
+
if (!res || !res.ok) throw new AuthError("SSO_START_FAILED", `HTTP ${res?.status ?? "network"}`);
|
|
432
|
+
const parsed = await res.json();
|
|
433
|
+
if (!parsed.success || !parsed.data?.authorizationUrl) {
|
|
434
|
+
throw new AuthError("SSO_START_FAILED", "Missing authorizationUrl");
|
|
435
|
+
}
|
|
436
|
+
return { authorizationUrl: parsed.data.authorizationUrl };
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Consume an OIDC callback URL captured via React Native's
|
|
440
|
+
* `Linking.addEventListener('url', ...)`. Reads the fragment for
|
|
441
|
+
* `sendora_oidc_token`, exchanges it for a session, and persists.
|
|
442
|
+
* Throws AuthError on `sendora_oidc_error=...` fragment.
|
|
443
|
+
*/
|
|
444
|
+
consumeSsoFragment(callbackUrl) {
|
|
445
|
+
return this.serialize(async () => {
|
|
446
|
+
if (this.user !== null) await this.wipeLocalIdentity();
|
|
447
|
+
const hashIdx = callbackUrl.indexOf("#");
|
|
448
|
+
const fragment = hashIdx >= 0 ? callbackUrl.slice(hashIdx + 1) : "";
|
|
449
|
+
const params = new URLSearchParams(fragment);
|
|
450
|
+
const err = params.get("sendora_oidc_error");
|
|
451
|
+
if (err) throw new AuthError("SSO_FAILED", decodeURIComponent(err));
|
|
452
|
+
const refresh = params.get("sendora_oidc_token");
|
|
453
|
+
if (!refresh) throw new AuthError("SSO_NO_TOKEN", "Callback URL has no sendora_oidc_token");
|
|
454
|
+
const res = await post(
|
|
455
|
+
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
456
|
+
"/api/v1/auth-service/token/refresh",
|
|
457
|
+
{ refreshToken: refresh }
|
|
458
|
+
);
|
|
459
|
+
if (!res || !res.ok) throw new AuthError("SSO_EXCHANGE_FAILED", `HTTP ${res?.status ?? "network"}`);
|
|
460
|
+
const parsed = await res.json();
|
|
461
|
+
const access = parsed.data?.accessToken;
|
|
462
|
+
const newRefresh = parsed.data?.refreshToken;
|
|
463
|
+
const expiresIn = parsed.data?.expiresIn;
|
|
464
|
+
if (!parsed.success || !access || !newRefresh || !expiresIn || expiresIn <= 0) {
|
|
465
|
+
throw new AuthError("SSO_EXCHANGE_FAILED", "Could not exchange OIDC token");
|
|
466
|
+
}
|
|
467
|
+
const claims = decodeJwtPayload(access);
|
|
468
|
+
const user = {
|
|
469
|
+
id: claims?.sub ?? "",
|
|
470
|
+
email: claims?.email ?? null,
|
|
471
|
+
emailVerified: claims?.email_verified ?? true,
|
|
472
|
+
name: claims?.name ?? null,
|
|
473
|
+
isAnonymous: false
|
|
474
|
+
};
|
|
475
|
+
this.persist({
|
|
476
|
+
user,
|
|
477
|
+
tokens: {
|
|
478
|
+
accessToken: access,
|
|
479
|
+
refreshToken: newRefresh,
|
|
480
|
+
expiresIn,
|
|
481
|
+
tokenType: "Bearer"
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
return user;
|
|
485
|
+
});
|
|
486
|
+
}
|
|
410
487
|
bearerHeaders() {
|
|
411
488
|
if (!this.accessToken) {
|
|
412
489
|
throw new AuthError("NOT_SIGNED_IN", "Not signed in");
|
|
@@ -511,6 +588,19 @@ var Auth = class {
|
|
|
511
588
|
await this.hooks.onAnonymousWipe();
|
|
512
589
|
}
|
|
513
590
|
};
|
|
591
|
+
function decodeJwtPayload(token) {
|
|
592
|
+
try {
|
|
593
|
+
const parts = token.split(".");
|
|
594
|
+
if (parts.length !== 3) return null;
|
|
595
|
+
const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
596
|
+
const pad = (4 - padded.length % 4) % 4;
|
|
597
|
+
if (typeof atob !== "function") return null;
|
|
598
|
+
const json = atob(padded + "===".slice(0, pad));
|
|
599
|
+
return JSON.parse(json);
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
514
604
|
|
|
515
605
|
// src/index.ts
|
|
516
606
|
var SDK_VERSION = "0.4.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth. Expo Go compatible, no native modules beyond AsyncStorage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|