@insureco/bio 0.4.0 → 0.6.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.js CHANGED
@@ -33,6 +33,8 @@ __export(index_exports, {
33
33
  BioAdmin: () => BioAdmin,
34
34
  BioAuth: () => BioAuth,
35
35
  BioError: () => BioError,
36
+ EmbedClient: () => EmbedClient,
37
+ GraphClient: () => GraphClient,
36
38
  decodeToken: () => decodeToken,
37
39
  generatePKCE: () => generatePKCE,
38
40
  isTokenExpired: () => isTokenExpired,
@@ -585,6 +587,257 @@ var BioAdmin = class _BioAdmin {
585
587
  }
586
588
  };
587
589
 
590
+ // src/embed.ts
591
+ var DEFAULT_BIO_URL = "https://bio.tawa.pro";
592
+ var DEFAULT_TIMEOUT_MS3 = 1e4;
593
+ var EmbedClient = class _EmbedClient {
594
+ bioIdUrl;
595
+ clientId;
596
+ clientSecret;
597
+ retries;
598
+ timeoutMs;
599
+ constructor(config) {
600
+ if (!config.clientId) {
601
+ throw new BioError("clientId is required", "config_error");
602
+ }
603
+ if (!config.clientSecret) {
604
+ throw new BioError("clientSecret is required", "config_error");
605
+ }
606
+ this.clientId = config.clientId;
607
+ this.clientSecret = config.clientSecret;
608
+ this.bioIdUrl = (config.bioIdUrl ?? DEFAULT_BIO_URL).replace(/\/$/, "");
609
+ this.retries = config.retries ?? 2;
610
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
611
+ }
612
+ /**
613
+ * Create an EmbedClient from environment variables.
614
+ *
615
+ * Reads: BIO_CLIENT_ID, BIO_CLIENT_SECRET, BIO_ID_URL
616
+ */
617
+ static fromEnv(overrides) {
618
+ const clientId = overrides?.clientId ?? process.env.BIO_CLIENT_ID;
619
+ const clientSecret = overrides?.clientSecret ?? process.env.BIO_CLIENT_SECRET;
620
+ if (!clientId) {
621
+ throw new BioError(
622
+ "BIO_CLIENT_ID environment variable is required",
623
+ "config_error"
624
+ );
625
+ }
626
+ if (!clientSecret) {
627
+ throw new BioError(
628
+ "BIO_CLIENT_SECRET environment variable is required",
629
+ "config_error"
630
+ );
631
+ }
632
+ return new _EmbedClient({
633
+ clientId,
634
+ clientSecret,
635
+ bioIdUrl: overrides?.bioIdUrl ?? process.env.BIO_ID_URL,
636
+ retries: overrides?.retries,
637
+ timeoutMs: overrides?.timeoutMs
638
+ });
639
+ }
640
+ /**
641
+ * Authenticate a user with email and password.
642
+ *
643
+ * @param params - Email and password
644
+ * @returns Access token, refresh token, user profile, and optional branding
645
+ */
646
+ async login(params) {
647
+ if (!params.email) throw new BioError("email is required", "validation_error");
648
+ if (!params.password) throw new BioError("password is required", "validation_error");
649
+ return this.embedRequest("/api/embed/login", {
650
+ email: params.email,
651
+ password: params.password
652
+ });
653
+ }
654
+ /**
655
+ * Create a new user account.
656
+ *
657
+ * @param params - Email, password, name, and optional invite token
658
+ * @returns Access token, refresh token, user profile, and optional branding
659
+ */
660
+ async signup(params) {
661
+ if (!params.email) throw new BioError("email is required", "validation_error");
662
+ if (!params.password) throw new BioError("password is required", "validation_error");
663
+ if (!params.name) throw new BioError("name is required", "validation_error");
664
+ const body = {
665
+ email: params.email,
666
+ password: params.password,
667
+ name: params.name
668
+ };
669
+ if (params.inviteToken) {
670
+ body.inviteToken = params.inviteToken;
671
+ }
672
+ return this.embedRequest("/api/embed/signup", body);
673
+ }
674
+ /**
675
+ * Send a magic link email to the user.
676
+ *
677
+ * The user clicks the link to authenticate without a password.
678
+ * After sending, use `verify()` with the token from the link.
679
+ *
680
+ * @param params - Email address to send the magic link to
681
+ */
682
+ async sendMagicLink(params) {
683
+ if (!params.email) throw new BioError("email is required", "validation_error");
684
+ const response = await this.fetchWithRetry(
685
+ "POST",
686
+ `${this.bioIdUrl}/api/embed/magic-link`,
687
+ JSON.stringify({ email: params.email })
688
+ );
689
+ const json = await parseJsonResponse(response);
690
+ if (!response.ok) {
691
+ throw new BioError(
692
+ extractErrorMessage(json, response.status),
693
+ extractErrorCode(json),
694
+ response.status,
695
+ json
696
+ );
697
+ }
698
+ }
699
+ /**
700
+ * Verify a magic link token and exchange it for auth tokens.
701
+ *
702
+ * @param params - The token from the magic link
703
+ * @returns Access token, refresh token, user profile, and optional branding
704
+ */
705
+ async verify(params) {
706
+ if (!params.token) throw new BioError("token is required", "validation_error");
707
+ return this.embedRequest("/api/embed/verify", {
708
+ token: params.token
709
+ });
710
+ }
711
+ /**
712
+ * Refresh an expired access token using a refresh token.
713
+ *
714
+ * @param params - The refresh token to exchange
715
+ * @returns New access token, rotated refresh token, user profile, and optional branding
716
+ */
717
+ async refresh(params) {
718
+ if (!params.refreshToken) throw new BioError("refreshToken is required", "validation_error");
719
+ return this.embedRequest("/api/embed/refresh", {
720
+ refreshToken: params.refreshToken
721
+ });
722
+ }
723
+ /**
724
+ * Revoke a refresh token (logout).
725
+ *
726
+ * @param params - The refresh token to revoke
727
+ */
728
+ async logout(params) {
729
+ if (!params.refreshToken) throw new BioError("refreshToken is required", "validation_error");
730
+ const response = await this.fetchWithRetry(
731
+ "POST",
732
+ `${this.bioIdUrl}/api/embed/logout`,
733
+ JSON.stringify({ refreshToken: params.refreshToken })
734
+ );
735
+ const json = await parseJsonResponse(response);
736
+ if (!response.ok) {
737
+ throw new BioError(
738
+ extractErrorMessage(json, response.status),
739
+ extractErrorCode(json),
740
+ response.status,
741
+ json
742
+ );
743
+ }
744
+ }
745
+ // ── Private helpers ──────────────────────────────────────────────────────
746
+ async embedRequest(path, body) {
747
+ const response = await this.fetchWithRetry(
748
+ "POST",
749
+ `${this.bioIdUrl}${path}`,
750
+ JSON.stringify(body)
751
+ );
752
+ const json = await parseJsonResponse(response);
753
+ if (!response.ok) {
754
+ throw new BioError(
755
+ extractErrorMessage(json, response.status),
756
+ extractErrorCode(json),
757
+ response.status,
758
+ json
759
+ );
760
+ }
761
+ return mapEmbedResponse(json);
762
+ }
763
+ async fetchWithRetry(method, url, body, attempt = 0) {
764
+ try {
765
+ const response = await fetch(url, {
766
+ method,
767
+ headers: {
768
+ "Content-Type": "application/json",
769
+ "X-Client-Id": this.clientId,
770
+ "X-Client-Secret": this.clientSecret
771
+ },
772
+ body,
773
+ signal: AbortSignal.timeout(this.timeoutMs)
774
+ });
775
+ if (response.status >= 500 && attempt < this.retries) {
776
+ await sleep(retryDelay(attempt));
777
+ return this.fetchWithRetry(method, url, body, attempt + 1);
778
+ }
779
+ return response;
780
+ } catch (err) {
781
+ if (attempt < this.retries) {
782
+ await sleep(retryDelay(attempt));
783
+ return this.fetchWithRetry(method, url, body, attempt + 1);
784
+ }
785
+ const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
786
+ throw new BioError(
787
+ isTimeout ? `Request timed out after ${this.timeoutMs}ms` : err instanceof Error ? err.message : "Network error",
788
+ isTimeout ? "timeout" : "network_error"
789
+ );
790
+ }
791
+ }
792
+ };
793
+ function mapEmbedResponse(raw) {
794
+ const data = raw.data ?? raw;
795
+ const rawUser = data.user ?? {};
796
+ const rawBranding = data.branding;
797
+ const user = {
798
+ bioId: rawUser.bioId,
799
+ email: rawUser.email,
800
+ name: rawUser.name,
801
+ orgSlug: rawUser.orgSlug
802
+ };
803
+ const result = {
804
+ accessToken: data.access_token,
805
+ refreshToken: data.refresh_token,
806
+ tokenType: data.token_type ?? "Bearer",
807
+ expiresIn: data.expires_in,
808
+ user
809
+ };
810
+ if (rawBranding) {
811
+ result.branding = {
812
+ displayName: rawBranding.displayName,
813
+ logoUrl: rawBranding.logoUrl,
814
+ logoMarkUrl: rawBranding.logoMarkUrl,
815
+ primaryColor: rawBranding.primaryColor,
816
+ secondaryColor: rawBranding.secondaryColor,
817
+ verified: rawBranding.verified,
818
+ whiteLabelApproved: rawBranding.whiteLabelApproved
819
+ };
820
+ }
821
+ return result;
822
+ }
823
+ function extractErrorMessage(json, status) {
824
+ const error = json.error;
825
+ if (typeof error === "object" && error !== null) {
826
+ return error.message ?? `Embed API returned ${status}`;
827
+ }
828
+ if (typeof error === "string") {
829
+ return error;
830
+ }
831
+ return `Embed API returned ${status}`;
832
+ }
833
+ function extractErrorCode(json) {
834
+ const error = json.error;
835
+ if (typeof error === "object" && error !== null) {
836
+ return error.code ?? "embed_error";
837
+ }
838
+ return "embed_error";
839
+ }
840
+
588
841
  // src/jwt.ts
589
842
  var import_node_crypto3 = __toESM(require("crypto"));
590
843
  var DEFAULT_ISSUERS = [
@@ -737,11 +990,172 @@ async function verifyTokenJWKS(token, options) {
737
990
  }
738
991
  return payload;
739
992
  }
993
+
994
+ // src/graph.ts
995
+ var DEFAULT_TIMEOUT_MS4 = 1e4;
996
+ var BIO_GRAPH_URL = "https://bio-graph.tawa.pro";
997
+ var GraphClient = class _GraphClient {
998
+ graphUrl;
999
+ accessToken;
1000
+ timeoutMs;
1001
+ constructor(config = {}) {
1002
+ this.graphUrl = (config.graphUrl ?? process.env.BIO_GRAPH_URL ?? BIO_GRAPH_URL).replace(/\/$/, "");
1003
+ this.accessToken = config.accessToken;
1004
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
1005
+ }
1006
+ /**
1007
+ * Create a GraphClient from environment variables.
1008
+ * BIO_GRAPH_URL defaults to https://bio-graph.tawa.pro if not set.
1009
+ */
1010
+ static fromEnv(overrides) {
1011
+ return new _GraphClient({
1012
+ graphUrl: overrides?.graphUrl ?? process.env.BIO_GRAPH_URL,
1013
+ accessToken: overrides?.accessToken,
1014
+ timeoutMs: overrides?.timeoutMs
1015
+ });
1016
+ }
1017
+ // ─── Public Profile Routes ────────────────────────────────────────────────
1018
+ /** Look up an agent by NPN. Returns agent properties + employment chain. */
1019
+ async getAgent(npn) {
1020
+ return this.get(`/api/graph/agent/${encodeURIComponent(npn)}`);
1021
+ }
1022
+ /**
1023
+ * Look up an agency by iecHash, raterspotId, or orgSlug.
1024
+ * Returns agency properties + staff list + accessible programs.
1025
+ */
1026
+ async getAgency(iecHash) {
1027
+ return this.get(`/api/graph/agency/${encodeURIComponent(iecHash)}`);
1028
+ }
1029
+ /**
1030
+ * Look up a carrier by NAIC code, iecHash, raterspotId, or orgSlug.
1031
+ * Returns carrier properties + managed programs.
1032
+ */
1033
+ async getCarrier(naic) {
1034
+ return this.get(`/api/graph/carrier/${encodeURIComponent(naic)}`);
1035
+ }
1036
+ /**
1037
+ * Look up a program by iecHash or raterspotId.
1038
+ * Returns program properties + managing carrier or MGA.
1039
+ */
1040
+ async getProgram(iecHash) {
1041
+ return this.get(`/api/graph/program/${encodeURIComponent(iecHash)}`);
1042
+ }
1043
+ // ─── Authenticated Routes ─────────────────────────────────────────────────
1044
+ /**
1045
+ * Find programs whose appetite covers a given risk.
1046
+ * Requires an accessToken (auth: required).
1047
+ *
1048
+ * @param input - Risk criteria: NAICS code, state, line of business, optional limits
1049
+ */
1050
+ async matchAppetite(input) {
1051
+ return this.post("/api/graph/appetite/match", input, { requiresAuth: true });
1052
+ }
1053
+ // ─── Internal ─────────────────────────────────────────────────────────────
1054
+ async get(path, attempt = 0) {
1055
+ const url = `${this.graphUrl}${path}`;
1056
+ const controller = new AbortController();
1057
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1058
+ let response;
1059
+ try {
1060
+ response = await fetch(url, {
1061
+ headers: this.buildHeaders(),
1062
+ signal: controller.signal
1063
+ });
1064
+ } catch (err) {
1065
+ clearTimeout(timer);
1066
+ const isTimeout = err instanceof Error && err.name === "AbortError";
1067
+ if (!isTimeout && attempt < 2) {
1068
+ await sleep(retryDelay(attempt));
1069
+ return this.get(path, attempt + 1);
1070
+ }
1071
+ throw new BioError(
1072
+ isTimeout ? "bio-graph request timed out" : `bio-graph request failed: ${String(err)}`,
1073
+ isTimeout ? "timeout" : "network_error"
1074
+ );
1075
+ } finally {
1076
+ clearTimeout(timer);
1077
+ }
1078
+ return this.handleResponse(response, path);
1079
+ }
1080
+ async post(path, body, opts = {}, attempt = 0) {
1081
+ if (opts.requiresAuth && !this.accessToken) {
1082
+ throw new BioError(
1083
+ `bio-graph ${path} requires an accessToken \u2014 pass it in the GraphClient constructor`,
1084
+ "config_error"
1085
+ );
1086
+ }
1087
+ const url = `${this.graphUrl}${path}`;
1088
+ const controller = new AbortController();
1089
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1090
+ let response;
1091
+ try {
1092
+ response = await fetch(url, {
1093
+ method: "POST",
1094
+ headers: { ...this.buildHeaders(), "Content-Type": "application/json" },
1095
+ body: JSON.stringify(body),
1096
+ signal: controller.signal
1097
+ });
1098
+ } catch (err) {
1099
+ clearTimeout(timer);
1100
+ const isTimeout = err instanceof Error && err.name === "AbortError";
1101
+ if (!isTimeout && attempt < 2) {
1102
+ await sleep(retryDelay(attempt));
1103
+ return this.post(path, body, opts, attempt + 1);
1104
+ }
1105
+ throw new BioError(
1106
+ isTimeout ? "bio-graph request timed out" : `bio-graph request failed: ${String(err)}`,
1107
+ isTimeout ? "timeout" : "network_error"
1108
+ );
1109
+ } finally {
1110
+ clearTimeout(timer);
1111
+ }
1112
+ return this.handleResponse(response, path);
1113
+ }
1114
+ buildHeaders() {
1115
+ const headers = {};
1116
+ if (this.accessToken) {
1117
+ headers["Authorization"] = `Bearer ${this.accessToken}`;
1118
+ }
1119
+ return headers;
1120
+ }
1121
+ async handleResponse(response, path) {
1122
+ if (response.status === 404) {
1123
+ throw new BioError(`bio-graph entity not found: ${path}`, "not_found", 404);
1124
+ }
1125
+ if (response.status === 401) {
1126
+ throw new BioError(
1127
+ "bio-graph authentication failed \u2014 provide a valid accessToken",
1128
+ "unauthorized",
1129
+ 401
1130
+ );
1131
+ }
1132
+ if (response.status === 402) {
1133
+ throw new BioError(
1134
+ "bio-graph call failed \u2014 insufficient gas tokens. Top up at tawa.insureco.io/wallet",
1135
+ "insufficient_gas",
1136
+ 402
1137
+ );
1138
+ }
1139
+ if (!response.ok) {
1140
+ const body2 = await parseJsonResponse(response).catch(() => ({}));
1141
+ throw new BioError(
1142
+ `bio-graph returned ${response.status} for ${path}`,
1143
+ "api_error",
1144
+ response.status,
1145
+ body2
1146
+ );
1147
+ }
1148
+ const body = await parseJsonResponse(response);
1149
+ return body;
1150
+ }
1151
+ };
740
1152
  // Annotate the CommonJS export names for ESM import in node:
741
1153
  0 && (module.exports = {
742
1154
  BioAdmin,
743
1155
  BioAuth,
744
1156
  BioError,
1157
+ EmbedClient,
1158
+ GraphClient,
745
1159
  decodeToken,
746
1160
  generatePKCE,
747
1161
  isTokenExpired,
package/dist/index.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ import {
2
+ GraphClient
3
+ } from "./chunk-PLN6QPED.mjs";
1
4
  import {
2
5
  BioError,
3
6
  parseJsonResponse,
@@ -512,6 +515,257 @@ var BioAdmin = class _BioAdmin {
512
515
  }
513
516
  };
514
517
 
518
+ // src/embed.ts
519
+ var DEFAULT_BIO_URL = "https://bio.tawa.pro";
520
+ var DEFAULT_TIMEOUT_MS3 = 1e4;
521
+ var EmbedClient = class _EmbedClient {
522
+ bioIdUrl;
523
+ clientId;
524
+ clientSecret;
525
+ retries;
526
+ timeoutMs;
527
+ constructor(config) {
528
+ if (!config.clientId) {
529
+ throw new BioError("clientId is required", "config_error");
530
+ }
531
+ if (!config.clientSecret) {
532
+ throw new BioError("clientSecret is required", "config_error");
533
+ }
534
+ this.clientId = config.clientId;
535
+ this.clientSecret = config.clientSecret;
536
+ this.bioIdUrl = (config.bioIdUrl ?? DEFAULT_BIO_URL).replace(/\/$/, "");
537
+ this.retries = config.retries ?? 2;
538
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
539
+ }
540
+ /**
541
+ * Create an EmbedClient from environment variables.
542
+ *
543
+ * Reads: BIO_CLIENT_ID, BIO_CLIENT_SECRET, BIO_ID_URL
544
+ */
545
+ static fromEnv(overrides) {
546
+ const clientId = overrides?.clientId ?? process.env.BIO_CLIENT_ID;
547
+ const clientSecret = overrides?.clientSecret ?? process.env.BIO_CLIENT_SECRET;
548
+ if (!clientId) {
549
+ throw new BioError(
550
+ "BIO_CLIENT_ID environment variable is required",
551
+ "config_error"
552
+ );
553
+ }
554
+ if (!clientSecret) {
555
+ throw new BioError(
556
+ "BIO_CLIENT_SECRET environment variable is required",
557
+ "config_error"
558
+ );
559
+ }
560
+ return new _EmbedClient({
561
+ clientId,
562
+ clientSecret,
563
+ bioIdUrl: overrides?.bioIdUrl ?? process.env.BIO_ID_URL,
564
+ retries: overrides?.retries,
565
+ timeoutMs: overrides?.timeoutMs
566
+ });
567
+ }
568
+ /**
569
+ * Authenticate a user with email and password.
570
+ *
571
+ * @param params - Email and password
572
+ * @returns Access token, refresh token, user profile, and optional branding
573
+ */
574
+ async login(params) {
575
+ if (!params.email) throw new BioError("email is required", "validation_error");
576
+ if (!params.password) throw new BioError("password is required", "validation_error");
577
+ return this.embedRequest("/api/embed/login", {
578
+ email: params.email,
579
+ password: params.password
580
+ });
581
+ }
582
+ /**
583
+ * Create a new user account.
584
+ *
585
+ * @param params - Email, password, name, and optional invite token
586
+ * @returns Access token, refresh token, user profile, and optional branding
587
+ */
588
+ async signup(params) {
589
+ if (!params.email) throw new BioError("email is required", "validation_error");
590
+ if (!params.password) throw new BioError("password is required", "validation_error");
591
+ if (!params.name) throw new BioError("name is required", "validation_error");
592
+ const body = {
593
+ email: params.email,
594
+ password: params.password,
595
+ name: params.name
596
+ };
597
+ if (params.inviteToken) {
598
+ body.inviteToken = params.inviteToken;
599
+ }
600
+ return this.embedRequest("/api/embed/signup", body);
601
+ }
602
+ /**
603
+ * Send a magic link email to the user.
604
+ *
605
+ * The user clicks the link to authenticate without a password.
606
+ * After sending, use `verify()` with the token from the link.
607
+ *
608
+ * @param params - Email address to send the magic link to
609
+ */
610
+ async sendMagicLink(params) {
611
+ if (!params.email) throw new BioError("email is required", "validation_error");
612
+ const response = await this.fetchWithRetry(
613
+ "POST",
614
+ `${this.bioIdUrl}/api/embed/magic-link`,
615
+ JSON.stringify({ email: params.email })
616
+ );
617
+ const json = await parseJsonResponse(response);
618
+ if (!response.ok) {
619
+ throw new BioError(
620
+ extractErrorMessage(json, response.status),
621
+ extractErrorCode(json),
622
+ response.status,
623
+ json
624
+ );
625
+ }
626
+ }
627
+ /**
628
+ * Verify a magic link token and exchange it for auth tokens.
629
+ *
630
+ * @param params - The token from the magic link
631
+ * @returns Access token, refresh token, user profile, and optional branding
632
+ */
633
+ async verify(params) {
634
+ if (!params.token) throw new BioError("token is required", "validation_error");
635
+ return this.embedRequest("/api/embed/verify", {
636
+ token: params.token
637
+ });
638
+ }
639
+ /**
640
+ * Refresh an expired access token using a refresh token.
641
+ *
642
+ * @param params - The refresh token to exchange
643
+ * @returns New access token, rotated refresh token, user profile, and optional branding
644
+ */
645
+ async refresh(params) {
646
+ if (!params.refreshToken) throw new BioError("refreshToken is required", "validation_error");
647
+ return this.embedRequest("/api/embed/refresh", {
648
+ refreshToken: params.refreshToken
649
+ });
650
+ }
651
+ /**
652
+ * Revoke a refresh token (logout).
653
+ *
654
+ * @param params - The refresh token to revoke
655
+ */
656
+ async logout(params) {
657
+ if (!params.refreshToken) throw new BioError("refreshToken is required", "validation_error");
658
+ const response = await this.fetchWithRetry(
659
+ "POST",
660
+ `${this.bioIdUrl}/api/embed/logout`,
661
+ JSON.stringify({ refreshToken: params.refreshToken })
662
+ );
663
+ const json = await parseJsonResponse(response);
664
+ if (!response.ok) {
665
+ throw new BioError(
666
+ extractErrorMessage(json, response.status),
667
+ extractErrorCode(json),
668
+ response.status,
669
+ json
670
+ );
671
+ }
672
+ }
673
+ // ── Private helpers ──────────────────────────────────────────────────────
674
+ async embedRequest(path, body) {
675
+ const response = await this.fetchWithRetry(
676
+ "POST",
677
+ `${this.bioIdUrl}${path}`,
678
+ JSON.stringify(body)
679
+ );
680
+ const json = await parseJsonResponse(response);
681
+ if (!response.ok) {
682
+ throw new BioError(
683
+ extractErrorMessage(json, response.status),
684
+ extractErrorCode(json),
685
+ response.status,
686
+ json
687
+ );
688
+ }
689
+ return mapEmbedResponse(json);
690
+ }
691
+ async fetchWithRetry(method, url, body, attempt = 0) {
692
+ try {
693
+ const response = await fetch(url, {
694
+ method,
695
+ headers: {
696
+ "Content-Type": "application/json",
697
+ "X-Client-Id": this.clientId,
698
+ "X-Client-Secret": this.clientSecret
699
+ },
700
+ body,
701
+ signal: AbortSignal.timeout(this.timeoutMs)
702
+ });
703
+ if (response.status >= 500 && attempt < this.retries) {
704
+ await sleep(retryDelay(attempt));
705
+ return this.fetchWithRetry(method, url, body, attempt + 1);
706
+ }
707
+ return response;
708
+ } catch (err) {
709
+ if (attempt < this.retries) {
710
+ await sleep(retryDelay(attempt));
711
+ return this.fetchWithRetry(method, url, body, attempt + 1);
712
+ }
713
+ const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
714
+ throw new BioError(
715
+ isTimeout ? `Request timed out after ${this.timeoutMs}ms` : err instanceof Error ? err.message : "Network error",
716
+ isTimeout ? "timeout" : "network_error"
717
+ );
718
+ }
719
+ }
720
+ };
721
+ function mapEmbedResponse(raw) {
722
+ const data = raw.data ?? raw;
723
+ const rawUser = data.user ?? {};
724
+ const rawBranding = data.branding;
725
+ const user = {
726
+ bioId: rawUser.bioId,
727
+ email: rawUser.email,
728
+ name: rawUser.name,
729
+ orgSlug: rawUser.orgSlug
730
+ };
731
+ const result = {
732
+ accessToken: data.access_token,
733
+ refreshToken: data.refresh_token,
734
+ tokenType: data.token_type ?? "Bearer",
735
+ expiresIn: data.expires_in,
736
+ user
737
+ };
738
+ if (rawBranding) {
739
+ result.branding = {
740
+ displayName: rawBranding.displayName,
741
+ logoUrl: rawBranding.logoUrl,
742
+ logoMarkUrl: rawBranding.logoMarkUrl,
743
+ primaryColor: rawBranding.primaryColor,
744
+ secondaryColor: rawBranding.secondaryColor,
745
+ verified: rawBranding.verified,
746
+ whiteLabelApproved: rawBranding.whiteLabelApproved
747
+ };
748
+ }
749
+ return result;
750
+ }
751
+ function extractErrorMessage(json, status) {
752
+ const error = json.error;
753
+ if (typeof error === "object" && error !== null) {
754
+ return error.message ?? `Embed API returned ${status}`;
755
+ }
756
+ if (typeof error === "string") {
757
+ return error;
758
+ }
759
+ return `Embed API returned ${status}`;
760
+ }
761
+ function extractErrorCode(json) {
762
+ const error = json.error;
763
+ if (typeof error === "object" && error !== null) {
764
+ return error.code ?? "embed_error";
765
+ }
766
+ return "embed_error";
767
+ }
768
+
515
769
  // src/jwt.ts
516
770
  import crypto3 from "crypto";
517
771
  var DEFAULT_ISSUERS = [
@@ -668,6 +922,8 @@ export {
668
922
  BioAdmin,
669
923
  BioAuth,
670
924
  BioError,
925
+ EmbedClient,
926
+ GraphClient,
671
927
  decodeToken,
672
928
  generatePKCE,
673
929
  isTokenExpired,