@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/chunk-PLN6QPED.mjs +169 -0
- package/dist/graph.d.mts +204 -0
- package/dist/graph.d.ts +204 -0
- package/dist/graph.js +225 -0
- package/dist/graph.mjs +7 -0
- package/dist/index.d.mts +89 -3
- package/dist/index.d.ts +89 -3
- package/dist/index.js +414 -0
- package/dist/index.mjs +256 -0
- package/dist/{types-Dkb-drHZ.d.mts → types-CJe1FP61.d.mts} +95 -1
- package/dist/{types-Dkb-drHZ.d.ts → types-CJe1FP61.d.ts} +95 -1
- package/dist/users.d.mts +1 -1
- package/dist/users.d.ts +1 -1
- package/package.json +14 -8
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,
|