@insureco/bio 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/{chunk-PLN6QPED.mjs → chunk-CKHMGUDP.mjs} +41 -0
- package/dist/chunk-UBURAGWI.mjs +154 -0
- package/dist/graph.d.mts +73 -1
- package/dist/graph.d.ts +73 -1
- package/dist/graph.js +41 -0
- package/dist/graph.mjs +1 -1
- package/dist/index.d.mts +99 -4
- package/dist/index.d.ts +99 -4
- package/dist/index.js +466 -2
- package/dist/index.mjs +274 -1
- package/dist/passport-react.d.mts +27 -0
- package/dist/passport-react.d.ts +27 -0
- package/dist/passport-react.js +115 -0
- package/dist/passport-react.mjs +90 -0
- package/dist/passport-types-bPgjNxv-.d.mts +79 -0
- package/dist/passport-types-bPgjNxv-.d.ts +79 -0
- package/dist/passport.d.mts +70 -0
- package/dist/passport.d.ts +70 -0
- package/dist/passport.js +180 -0
- package/dist/passport.mjs +6 -0
- package/dist/{types-Dkb-drHZ.d.mts → types-DOpXwdF2.d.mts} +117 -1
- package/dist/{types-Dkb-drHZ.d.ts → types-DOpXwdF2.d.ts} +117 -1
- package/dist/users.d.mts +1 -1
- package/dist/users.d.ts +1 -1
- package/dist/users.js +1 -0
- package/dist/users.mjs +1 -0
- package/package.json +24 -2
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GraphClient
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CKHMGUDP.mjs";
|
|
4
|
+
import {
|
|
5
|
+
PassportClient
|
|
6
|
+
} from "./chunk-UBURAGWI.mjs";
|
|
4
7
|
import {
|
|
5
8
|
BioError,
|
|
6
9
|
parseJsonResponse,
|
|
@@ -100,6 +103,23 @@ var BioAuth = class _BioAuth {
|
|
|
100
103
|
codeChallenge
|
|
101
104
|
};
|
|
102
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Build an authorization URL with prompt=none for silent authentication.
|
|
108
|
+
*
|
|
109
|
+
* Useful for checking if the user has an existing session without showing
|
|
110
|
+
* a login screen. If the user is not authenticated, Bio-ID redirects back
|
|
111
|
+
* with an `error=login_required` query parameter instead of showing UI.
|
|
112
|
+
*/
|
|
113
|
+
silentAuth(opts) {
|
|
114
|
+
const result = this.getAuthorizationUrl({
|
|
115
|
+
redirectUri: opts.redirectUri,
|
|
116
|
+
scopes: opts.scopes,
|
|
117
|
+
state: opts.state
|
|
118
|
+
});
|
|
119
|
+
const url = new URL(result.url);
|
|
120
|
+
url.searchParams.set("prompt", "none");
|
|
121
|
+
return { ...result, url: url.toString() };
|
|
122
|
+
}
|
|
103
123
|
/**
|
|
104
124
|
* Exchange an authorization code for tokens.
|
|
105
125
|
*
|
|
@@ -515,6 +535,257 @@ var BioAdmin = class _BioAdmin {
|
|
|
515
535
|
}
|
|
516
536
|
};
|
|
517
537
|
|
|
538
|
+
// src/embed.ts
|
|
539
|
+
var DEFAULT_BIO_URL = "https://bio.tawa.pro";
|
|
540
|
+
var DEFAULT_TIMEOUT_MS3 = 1e4;
|
|
541
|
+
var EmbedClient = class _EmbedClient {
|
|
542
|
+
bioIdUrl;
|
|
543
|
+
clientId;
|
|
544
|
+
clientSecret;
|
|
545
|
+
retries;
|
|
546
|
+
timeoutMs;
|
|
547
|
+
constructor(config) {
|
|
548
|
+
if (!config.clientId) {
|
|
549
|
+
throw new BioError("clientId is required", "config_error");
|
|
550
|
+
}
|
|
551
|
+
if (!config.clientSecret) {
|
|
552
|
+
throw new BioError("clientSecret is required", "config_error");
|
|
553
|
+
}
|
|
554
|
+
this.clientId = config.clientId;
|
|
555
|
+
this.clientSecret = config.clientSecret;
|
|
556
|
+
this.bioIdUrl = (config.bioIdUrl ?? DEFAULT_BIO_URL).replace(/\/$/, "");
|
|
557
|
+
this.retries = config.retries ?? 2;
|
|
558
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Create an EmbedClient from environment variables.
|
|
562
|
+
*
|
|
563
|
+
* Reads: BIO_CLIENT_ID, BIO_CLIENT_SECRET, BIO_ID_URL
|
|
564
|
+
*/
|
|
565
|
+
static fromEnv(overrides) {
|
|
566
|
+
const clientId = overrides?.clientId ?? process.env.BIO_CLIENT_ID;
|
|
567
|
+
const clientSecret = overrides?.clientSecret ?? process.env.BIO_CLIENT_SECRET;
|
|
568
|
+
if (!clientId) {
|
|
569
|
+
throw new BioError(
|
|
570
|
+
"BIO_CLIENT_ID environment variable is required",
|
|
571
|
+
"config_error"
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (!clientSecret) {
|
|
575
|
+
throw new BioError(
|
|
576
|
+
"BIO_CLIENT_SECRET environment variable is required",
|
|
577
|
+
"config_error"
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
return new _EmbedClient({
|
|
581
|
+
clientId,
|
|
582
|
+
clientSecret,
|
|
583
|
+
bioIdUrl: overrides?.bioIdUrl ?? process.env.BIO_ID_URL,
|
|
584
|
+
retries: overrides?.retries,
|
|
585
|
+
timeoutMs: overrides?.timeoutMs
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Authenticate a user with email and password.
|
|
590
|
+
*
|
|
591
|
+
* @param params - Email and password
|
|
592
|
+
* @returns Access token, refresh token, user profile, and optional branding
|
|
593
|
+
*/
|
|
594
|
+
async login(params) {
|
|
595
|
+
if (!params.email) throw new BioError("email is required", "validation_error");
|
|
596
|
+
if (!params.password) throw new BioError("password is required", "validation_error");
|
|
597
|
+
return this.embedRequest("/api/embed/login", {
|
|
598
|
+
email: params.email,
|
|
599
|
+
password: params.password
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Create a new user account.
|
|
604
|
+
*
|
|
605
|
+
* @param params - Email, password, name, and optional invite token
|
|
606
|
+
* @returns Access token, refresh token, user profile, and optional branding
|
|
607
|
+
*/
|
|
608
|
+
async signup(params) {
|
|
609
|
+
if (!params.email) throw new BioError("email is required", "validation_error");
|
|
610
|
+
if (!params.password) throw new BioError("password is required", "validation_error");
|
|
611
|
+
if (!params.name) throw new BioError("name is required", "validation_error");
|
|
612
|
+
const body = {
|
|
613
|
+
email: params.email,
|
|
614
|
+
password: params.password,
|
|
615
|
+
name: params.name
|
|
616
|
+
};
|
|
617
|
+
if (params.inviteToken) {
|
|
618
|
+
body.inviteToken = params.inviteToken;
|
|
619
|
+
}
|
|
620
|
+
return this.embedRequest("/api/embed/signup", body);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Send a magic link email to the user.
|
|
624
|
+
*
|
|
625
|
+
* The user clicks the link to authenticate without a password.
|
|
626
|
+
* After sending, use `verify()` with the token from the link.
|
|
627
|
+
*
|
|
628
|
+
* @param params - Email address to send the magic link to
|
|
629
|
+
*/
|
|
630
|
+
async sendMagicLink(params) {
|
|
631
|
+
if (!params.email) throw new BioError("email is required", "validation_error");
|
|
632
|
+
const response = await this.fetchWithRetry(
|
|
633
|
+
"POST",
|
|
634
|
+
`${this.bioIdUrl}/api/embed/magic-link`,
|
|
635
|
+
JSON.stringify({ email: params.email })
|
|
636
|
+
);
|
|
637
|
+
const json = await parseJsonResponse(response);
|
|
638
|
+
if (!response.ok) {
|
|
639
|
+
throw new BioError(
|
|
640
|
+
extractErrorMessage(json, response.status),
|
|
641
|
+
extractErrorCode(json),
|
|
642
|
+
response.status,
|
|
643
|
+
json
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Verify a magic link token and exchange it for auth tokens.
|
|
649
|
+
*
|
|
650
|
+
* @param params - The token from the magic link
|
|
651
|
+
* @returns Access token, refresh token, user profile, and optional branding
|
|
652
|
+
*/
|
|
653
|
+
async verify(params) {
|
|
654
|
+
if (!params.token) throw new BioError("token is required", "validation_error");
|
|
655
|
+
return this.embedRequest("/api/embed/verify", {
|
|
656
|
+
token: params.token
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Refresh an expired access token using a refresh token.
|
|
661
|
+
*
|
|
662
|
+
* @param params - The refresh token to exchange
|
|
663
|
+
* @returns New access token, rotated refresh token, user profile, and optional branding
|
|
664
|
+
*/
|
|
665
|
+
async refresh(params) {
|
|
666
|
+
if (!params.refreshToken) throw new BioError("refreshToken is required", "validation_error");
|
|
667
|
+
return this.embedRequest("/api/embed/refresh", {
|
|
668
|
+
refreshToken: params.refreshToken
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Revoke a refresh token (logout).
|
|
673
|
+
*
|
|
674
|
+
* @param params - The refresh token to revoke
|
|
675
|
+
*/
|
|
676
|
+
async logout(params) {
|
|
677
|
+
if (!params.refreshToken) throw new BioError("refreshToken is required", "validation_error");
|
|
678
|
+
const response = await this.fetchWithRetry(
|
|
679
|
+
"POST",
|
|
680
|
+
`${this.bioIdUrl}/api/embed/logout`,
|
|
681
|
+
JSON.stringify({ refreshToken: params.refreshToken })
|
|
682
|
+
);
|
|
683
|
+
const json = await parseJsonResponse(response);
|
|
684
|
+
if (!response.ok) {
|
|
685
|
+
throw new BioError(
|
|
686
|
+
extractErrorMessage(json, response.status),
|
|
687
|
+
extractErrorCode(json),
|
|
688
|
+
response.status,
|
|
689
|
+
json
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
694
|
+
async embedRequest(path, body) {
|
|
695
|
+
const response = await this.fetchWithRetry(
|
|
696
|
+
"POST",
|
|
697
|
+
`${this.bioIdUrl}${path}`,
|
|
698
|
+
JSON.stringify(body)
|
|
699
|
+
);
|
|
700
|
+
const json = await parseJsonResponse(response);
|
|
701
|
+
if (!response.ok) {
|
|
702
|
+
throw new BioError(
|
|
703
|
+
extractErrorMessage(json, response.status),
|
|
704
|
+
extractErrorCode(json),
|
|
705
|
+
response.status,
|
|
706
|
+
json
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
return mapEmbedResponse(json);
|
|
710
|
+
}
|
|
711
|
+
async fetchWithRetry(method, url, body, attempt = 0) {
|
|
712
|
+
try {
|
|
713
|
+
const response = await fetch(url, {
|
|
714
|
+
method,
|
|
715
|
+
headers: {
|
|
716
|
+
"Content-Type": "application/json",
|
|
717
|
+
"X-Client-Id": this.clientId,
|
|
718
|
+
"X-Client-Secret": this.clientSecret
|
|
719
|
+
},
|
|
720
|
+
body,
|
|
721
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
722
|
+
});
|
|
723
|
+
if (response.status >= 500 && attempt < this.retries) {
|
|
724
|
+
await sleep(retryDelay(attempt));
|
|
725
|
+
return this.fetchWithRetry(method, url, body, attempt + 1);
|
|
726
|
+
}
|
|
727
|
+
return response;
|
|
728
|
+
} catch (err) {
|
|
729
|
+
if (attempt < this.retries) {
|
|
730
|
+
await sleep(retryDelay(attempt));
|
|
731
|
+
return this.fetchWithRetry(method, url, body, attempt + 1);
|
|
732
|
+
}
|
|
733
|
+
const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
|
|
734
|
+
throw new BioError(
|
|
735
|
+
isTimeout ? `Request timed out after ${this.timeoutMs}ms` : err instanceof Error ? err.message : "Network error",
|
|
736
|
+
isTimeout ? "timeout" : "network_error"
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
function mapEmbedResponse(raw) {
|
|
742
|
+
const data = raw.data ?? raw;
|
|
743
|
+
const rawUser = data.user ?? {};
|
|
744
|
+
const rawBranding = data.branding;
|
|
745
|
+
const user = {
|
|
746
|
+
bioId: rawUser.bioId,
|
|
747
|
+
email: rawUser.email,
|
|
748
|
+
name: rawUser.name,
|
|
749
|
+
orgSlug: rawUser.orgSlug
|
|
750
|
+
};
|
|
751
|
+
const result = {
|
|
752
|
+
accessToken: data.access_token,
|
|
753
|
+
refreshToken: data.refresh_token,
|
|
754
|
+
tokenType: data.token_type ?? "Bearer",
|
|
755
|
+
expiresIn: data.expires_in,
|
|
756
|
+
user
|
|
757
|
+
};
|
|
758
|
+
if (rawBranding) {
|
|
759
|
+
result.branding = {
|
|
760
|
+
displayName: rawBranding.displayName,
|
|
761
|
+
logoUrl: rawBranding.logoUrl,
|
|
762
|
+
logoMarkUrl: rawBranding.logoMarkUrl,
|
|
763
|
+
primaryColor: rawBranding.primaryColor,
|
|
764
|
+
secondaryColor: rawBranding.secondaryColor,
|
|
765
|
+
verified: rawBranding.verified,
|
|
766
|
+
whiteLabelApproved: rawBranding.whiteLabelApproved
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
function extractErrorMessage(json, status) {
|
|
772
|
+
const error = json.error;
|
|
773
|
+
if (typeof error === "object" && error !== null) {
|
|
774
|
+
return error.message ?? `Embed API returned ${status}`;
|
|
775
|
+
}
|
|
776
|
+
if (typeof error === "string") {
|
|
777
|
+
return error;
|
|
778
|
+
}
|
|
779
|
+
return `Embed API returned ${status}`;
|
|
780
|
+
}
|
|
781
|
+
function extractErrorCode(json) {
|
|
782
|
+
const error = json.error;
|
|
783
|
+
if (typeof error === "object" && error !== null) {
|
|
784
|
+
return error.code ?? "embed_error";
|
|
785
|
+
}
|
|
786
|
+
return "embed_error";
|
|
787
|
+
}
|
|
788
|
+
|
|
518
789
|
// src/jwt.ts
|
|
519
790
|
import crypto3 from "crypto";
|
|
520
791
|
var DEFAULT_ISSUERS = [
|
|
@@ -671,7 +942,9 @@ export {
|
|
|
671
942
|
BioAdmin,
|
|
672
943
|
BioAuth,
|
|
673
944
|
BioError,
|
|
945
|
+
EmbedClient,
|
|
674
946
|
GraphClient,
|
|
947
|
+
PassportClient,
|
|
675
948
|
decodeToken,
|
|
676
949
|
generatePKCE,
|
|
677
950
|
isTokenExpired,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { P as Passport, a as PassportStatus } from './passport-types-bPgjNxv-.mjs';
|
|
2
|
+
|
|
3
|
+
interface UsePassportOptions {
|
|
4
|
+
/** Bio-ID base URL */
|
|
5
|
+
bioIdUrl: string;
|
|
6
|
+
/** Access token for authentication */
|
|
7
|
+
accessToken: string | null;
|
|
8
|
+
/** Service name for tracking */
|
|
9
|
+
service?: string;
|
|
10
|
+
/** Auto-reconnect (default: true) */
|
|
11
|
+
autoReconnect?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface UsePassportResult {
|
|
14
|
+
/** Current passport object (null until identity received) */
|
|
15
|
+
passport: Passport | null;
|
|
16
|
+
/** Connection status */
|
|
17
|
+
status: PassportStatus;
|
|
18
|
+
/** Last error (null if none) */
|
|
19
|
+
error: Error | null;
|
|
20
|
+
/** Manually disconnect */
|
|
21
|
+
disconnect: () => void;
|
|
22
|
+
/** Manually reconnect */
|
|
23
|
+
reconnect: () => void;
|
|
24
|
+
}
|
|
25
|
+
declare function usePassport(options: UsePassportOptions): UsePassportResult;
|
|
26
|
+
|
|
27
|
+
export { type UsePassportOptions, type UsePassportResult, usePassport };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { P as Passport, a as PassportStatus } from './passport-types-bPgjNxv-.js';
|
|
2
|
+
|
|
3
|
+
interface UsePassportOptions {
|
|
4
|
+
/** Bio-ID base URL */
|
|
5
|
+
bioIdUrl: string;
|
|
6
|
+
/** Access token for authentication */
|
|
7
|
+
accessToken: string | null;
|
|
8
|
+
/** Service name for tracking */
|
|
9
|
+
service?: string;
|
|
10
|
+
/** Auto-reconnect (default: true) */
|
|
11
|
+
autoReconnect?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface UsePassportResult {
|
|
14
|
+
/** Current passport object (null until identity received) */
|
|
15
|
+
passport: Passport | null;
|
|
16
|
+
/** Connection status */
|
|
17
|
+
status: PassportStatus;
|
|
18
|
+
/** Last error (null if none) */
|
|
19
|
+
error: Error | null;
|
|
20
|
+
/** Manually disconnect */
|
|
21
|
+
disconnect: () => void;
|
|
22
|
+
/** Manually reconnect */
|
|
23
|
+
reconnect: () => void;
|
|
24
|
+
}
|
|
25
|
+
declare function usePassport(options: UsePassportOptions): UsePassportResult;
|
|
26
|
+
|
|
27
|
+
export { type UsePassportOptions, type UsePassportResult, usePassport };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/passport-react.ts
|
|
21
|
+
var passport_react_exports = {};
|
|
22
|
+
__export(passport_react_exports, {
|
|
23
|
+
usePassport: () => usePassport
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(passport_react_exports);
|
|
26
|
+
var import_react = require("react");
|
|
27
|
+
function usePassport(options) {
|
|
28
|
+
const { bioIdUrl, accessToken, service, autoReconnect = true } = options;
|
|
29
|
+
const [passport, setPassport] = (0, import_react.useState)(null);
|
|
30
|
+
const [status, setStatus] = (0, import_react.useState)("disconnected");
|
|
31
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
32
|
+
const wsRef = (0, import_react.useRef)(null);
|
|
33
|
+
const reconnectTimerRef = (0, import_react.useRef)(null);
|
|
34
|
+
const reconnectAttemptRef = (0, import_react.useRef)(0);
|
|
35
|
+
const closedRef = (0, import_react.useRef)(false);
|
|
36
|
+
const cleanup = (0, import_react.useCallback)(() => {
|
|
37
|
+
if (reconnectTimerRef.current) {
|
|
38
|
+
clearTimeout(reconnectTimerRef.current);
|
|
39
|
+
reconnectTimerRef.current = null;
|
|
40
|
+
}
|
|
41
|
+
if (wsRef.current) {
|
|
42
|
+
wsRef.current.onclose = null;
|
|
43
|
+
wsRef.current.onerror = null;
|
|
44
|
+
wsRef.current.onmessage = null;
|
|
45
|
+
wsRef.current.close();
|
|
46
|
+
wsRef.current = null;
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
const connect = (0, import_react.useCallback)(() => {
|
|
50
|
+
if (!accessToken || !bioIdUrl || closedRef.current) return;
|
|
51
|
+
cleanup();
|
|
52
|
+
setStatus("connecting");
|
|
53
|
+
setError(null);
|
|
54
|
+
const url = new URL("/passport", bioIdUrl.replace(/^http/, "ws"));
|
|
55
|
+
url.searchParams.set("token", accessToken);
|
|
56
|
+
url.searchParams.set("version", "1");
|
|
57
|
+
if (service) url.searchParams.set("service", service);
|
|
58
|
+
const ws = new WebSocket(url.toString());
|
|
59
|
+
wsRef.current = ws;
|
|
60
|
+
ws.onopen = () => {
|
|
61
|
+
reconnectAttemptRef.current = 0;
|
|
62
|
+
setStatus("connected");
|
|
63
|
+
};
|
|
64
|
+
ws.onmessage = (event) => {
|
|
65
|
+
try {
|
|
66
|
+
const data = JSON.parse(event.data);
|
|
67
|
+
if (data.type === "identity" || data.type === "passport_updated") {
|
|
68
|
+
setPassport(data.passport ?? null);
|
|
69
|
+
} else if (data.type === "revoked") {
|
|
70
|
+
setPassport(null);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
ws.onclose = () => {
|
|
76
|
+
setStatus("disconnected");
|
|
77
|
+
if (!closedRef.current && autoReconnect) {
|
|
78
|
+
const delay = Math.min(1e3 * Math.pow(2, reconnectAttemptRef.current), 3e4);
|
|
79
|
+
reconnectAttemptRef.current++;
|
|
80
|
+
reconnectTimerRef.current = setTimeout(() => {
|
|
81
|
+
reconnectTimerRef.current = null;
|
|
82
|
+
connect();
|
|
83
|
+
}, delay);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
ws.onerror = () => {
|
|
87
|
+
setError(new Error("Passport WebSocket error"));
|
|
88
|
+
setStatus("error");
|
|
89
|
+
};
|
|
90
|
+
}, [bioIdUrl, accessToken, service, autoReconnect, cleanup]);
|
|
91
|
+
(0, import_react.useEffect)(() => {
|
|
92
|
+
closedRef.current = false;
|
|
93
|
+
connect();
|
|
94
|
+
return () => {
|
|
95
|
+
closedRef.current = true;
|
|
96
|
+
cleanup();
|
|
97
|
+
setStatus("disconnected");
|
|
98
|
+
};
|
|
99
|
+
}, [connect, cleanup]);
|
|
100
|
+
const disconnect = (0, import_react.useCallback)(() => {
|
|
101
|
+
closedRef.current = true;
|
|
102
|
+
cleanup();
|
|
103
|
+
setStatus("disconnected");
|
|
104
|
+
}, [cleanup]);
|
|
105
|
+
const reconnect = (0, import_react.useCallback)(() => {
|
|
106
|
+
closedRef.current = false;
|
|
107
|
+
reconnectAttemptRef.current = 0;
|
|
108
|
+
connect();
|
|
109
|
+
}, [connect]);
|
|
110
|
+
return { passport, status, error, disconnect, reconnect };
|
|
111
|
+
}
|
|
112
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
113
|
+
0 && (module.exports = {
|
|
114
|
+
usePassport
|
|
115
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/passport-react.ts
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
3
|
+
function usePassport(options) {
|
|
4
|
+
const { bioIdUrl, accessToken, service, autoReconnect = true } = options;
|
|
5
|
+
const [passport, setPassport] = useState(null);
|
|
6
|
+
const [status, setStatus] = useState("disconnected");
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const wsRef = useRef(null);
|
|
9
|
+
const reconnectTimerRef = useRef(null);
|
|
10
|
+
const reconnectAttemptRef = useRef(0);
|
|
11
|
+
const closedRef = useRef(false);
|
|
12
|
+
const cleanup = useCallback(() => {
|
|
13
|
+
if (reconnectTimerRef.current) {
|
|
14
|
+
clearTimeout(reconnectTimerRef.current);
|
|
15
|
+
reconnectTimerRef.current = null;
|
|
16
|
+
}
|
|
17
|
+
if (wsRef.current) {
|
|
18
|
+
wsRef.current.onclose = null;
|
|
19
|
+
wsRef.current.onerror = null;
|
|
20
|
+
wsRef.current.onmessage = null;
|
|
21
|
+
wsRef.current.close();
|
|
22
|
+
wsRef.current = null;
|
|
23
|
+
}
|
|
24
|
+
}, []);
|
|
25
|
+
const connect = useCallback(() => {
|
|
26
|
+
if (!accessToken || !bioIdUrl || closedRef.current) return;
|
|
27
|
+
cleanup();
|
|
28
|
+
setStatus("connecting");
|
|
29
|
+
setError(null);
|
|
30
|
+
const url = new URL("/passport", bioIdUrl.replace(/^http/, "ws"));
|
|
31
|
+
url.searchParams.set("token", accessToken);
|
|
32
|
+
url.searchParams.set("version", "1");
|
|
33
|
+
if (service) url.searchParams.set("service", service);
|
|
34
|
+
const ws = new WebSocket(url.toString());
|
|
35
|
+
wsRef.current = ws;
|
|
36
|
+
ws.onopen = () => {
|
|
37
|
+
reconnectAttemptRef.current = 0;
|
|
38
|
+
setStatus("connected");
|
|
39
|
+
};
|
|
40
|
+
ws.onmessage = (event) => {
|
|
41
|
+
try {
|
|
42
|
+
const data = JSON.parse(event.data);
|
|
43
|
+
if (data.type === "identity" || data.type === "passport_updated") {
|
|
44
|
+
setPassport(data.passport ?? null);
|
|
45
|
+
} else if (data.type === "revoked") {
|
|
46
|
+
setPassport(null);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
ws.onclose = () => {
|
|
52
|
+
setStatus("disconnected");
|
|
53
|
+
if (!closedRef.current && autoReconnect) {
|
|
54
|
+
const delay = Math.min(1e3 * Math.pow(2, reconnectAttemptRef.current), 3e4);
|
|
55
|
+
reconnectAttemptRef.current++;
|
|
56
|
+
reconnectTimerRef.current = setTimeout(() => {
|
|
57
|
+
reconnectTimerRef.current = null;
|
|
58
|
+
connect();
|
|
59
|
+
}, delay);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
ws.onerror = () => {
|
|
63
|
+
setError(new Error("Passport WebSocket error"));
|
|
64
|
+
setStatus("error");
|
|
65
|
+
};
|
|
66
|
+
}, [bioIdUrl, accessToken, service, autoReconnect, cleanup]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
closedRef.current = false;
|
|
69
|
+
connect();
|
|
70
|
+
return () => {
|
|
71
|
+
closedRef.current = true;
|
|
72
|
+
cleanup();
|
|
73
|
+
setStatus("disconnected");
|
|
74
|
+
};
|
|
75
|
+
}, [connect, cleanup]);
|
|
76
|
+
const disconnect = useCallback(() => {
|
|
77
|
+
closedRef.current = true;
|
|
78
|
+
cleanup();
|
|
79
|
+
setStatus("disconnected");
|
|
80
|
+
}, [cleanup]);
|
|
81
|
+
const reconnect = useCallback(() => {
|
|
82
|
+
closedRef.current = false;
|
|
83
|
+
reconnectAttemptRef.current = 0;
|
|
84
|
+
connect();
|
|
85
|
+
}, [connect]);
|
|
86
|
+
return { passport, status, error, disconnect, reconnect };
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
usePassport
|
|
90
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** The full Passport identity object pushed via WebSocket */
|
|
2
|
+
interface Passport {
|
|
3
|
+
bioId: string;
|
|
4
|
+
email: string;
|
|
5
|
+
firstName: string;
|
|
6
|
+
lastName: string;
|
|
7
|
+
orgSlug: string;
|
|
8
|
+
orgId: string;
|
|
9
|
+
roles: string[];
|
|
10
|
+
villages: PassportVillage[];
|
|
11
|
+
serviceGrants: PassportServiceGrant[];
|
|
12
|
+
crossOrgPermissions: PassportCrossOrgPermission[];
|
|
13
|
+
session: PassportSession;
|
|
14
|
+
}
|
|
15
|
+
interface PassportVillage {
|
|
16
|
+
slug: string;
|
|
17
|
+
name: string;
|
|
18
|
+
role: 'member' | 'admin' | 'owner';
|
|
19
|
+
}
|
|
20
|
+
interface PassportServiceGrant {
|
|
21
|
+
service: string;
|
|
22
|
+
scopes: string[];
|
|
23
|
+
grantedAt: string;
|
|
24
|
+
}
|
|
25
|
+
interface PassportCrossOrgPermission {
|
|
26
|
+
targetOrgSlug: string;
|
|
27
|
+
modules: string[];
|
|
28
|
+
}
|
|
29
|
+
interface PassportSession {
|
|
30
|
+
issuedAt: string;
|
|
31
|
+
lastSeen: string;
|
|
32
|
+
connectedServices: string[];
|
|
33
|
+
}
|
|
34
|
+
/** Interface for token refresh — accepts any object with a refreshToken method */
|
|
35
|
+
interface PassportTokenRefresher {
|
|
36
|
+
refreshToken: (refreshToken: string) => Promise<{
|
|
37
|
+
access_token: string;
|
|
38
|
+
refresh_token?: string;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
/** Configuration for PassportClient */
|
|
42
|
+
interface PassportClientConfig {
|
|
43
|
+
/** Bio-ID base URL (e.g. https://bio.insureco.io) */
|
|
44
|
+
bioIdUrl: string;
|
|
45
|
+
/** Access token for authentication */
|
|
46
|
+
accessToken: string;
|
|
47
|
+
/** Service name sent as ?service= query param */
|
|
48
|
+
service?: string;
|
|
49
|
+
/** Auto-reconnect on disconnect (default: true) */
|
|
50
|
+
autoReconnect?: boolean;
|
|
51
|
+
/** Max reconnect delay in ms (default: 30000) */
|
|
52
|
+
maxReconnectDelay?: number;
|
|
53
|
+
/** Optional refresh token for auto-refresh on 4003 close */
|
|
54
|
+
refreshToken?: string;
|
|
55
|
+
/** Callback when tokens are refreshed (so app can persist new tokens) */
|
|
56
|
+
onTokenRefresh?: (tokens: {
|
|
57
|
+
access_token: string;
|
|
58
|
+
refresh_token?: string;
|
|
59
|
+
}) => void;
|
|
60
|
+
/** Token refresher (e.g. BioAuth instance) — required if refreshToken is set */
|
|
61
|
+
bioAuth?: PassportTokenRefresher;
|
|
62
|
+
}
|
|
63
|
+
/** Events emitted by the passport socket */
|
|
64
|
+
type PassportEventType = 'identity' | 'passport_updated' | 'revoked';
|
|
65
|
+
/** Message received from the passport WebSocket */
|
|
66
|
+
interface PassportMessage {
|
|
67
|
+
type: PassportEventType;
|
|
68
|
+
passport?: Passport;
|
|
69
|
+
}
|
|
70
|
+
/** Status of the passport connection */
|
|
71
|
+
type PassportStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
72
|
+
/** Branding config from passport (optional) */
|
|
73
|
+
interface PassportBranding {
|
|
74
|
+
logoUrl?: string;
|
|
75
|
+
primaryColor?: string;
|
|
76
|
+
appName?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type { Passport as P, PassportStatus as a, PassportBranding as b, PassportClientConfig as c, PassportCrossOrgPermission as d, PassportEventType as e, PassportMessage as f, PassportServiceGrant as g, PassportSession as h, PassportTokenRefresher as i, PassportVillage as j };
|