@sendoracloud/sdk-react-native 0.5.0 → 0.8.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 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.5.0",
3
+ "version": "0.8.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",