@metaboliccode-dev/widget 0.2.87 → 0.2.88
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/README.md +29 -0
- package/dist/entry-server.d.ts +58 -0
- package/dist/entry-server.test.d.ts +1 -0
- package/dist/server.js +98 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -37,6 +37,35 @@ npm install @metaboliccode-dev/widget
|
|
|
37
37
|
|
|
38
38
|
## Component usage
|
|
39
39
|
|
|
40
|
+
New integrations obtain dashboard tokens on their server using OAuth credentials
|
|
41
|
+
registered for their tenant and environment:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { createMetabolicOAuthClient } from "@metaboliccode-dev/widget/server";
|
|
45
|
+
|
|
46
|
+
const oauth = createMetabolicOAuthClient({
|
|
47
|
+
baseUrl: process.env.METABOLIC_CODE_BACKEND_BASE_URL!,
|
|
48
|
+
clientId: process.env.METABOLIC_CODE_OAUTH_CLIENT_ID!,
|
|
49
|
+
clientSecret: process.env.METABOLIC_CODE_OAUTH_CLIENT_SECRET!,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Validate the host user session and authorize these selectors before delegation.
|
|
53
|
+
const { accessToken, expiresIn } = await oauth.getDelegatedToken({
|
|
54
|
+
sub: authorizedUserId,
|
|
55
|
+
role: "patient",
|
|
56
|
+
patient_id: authorizedPatientId,
|
|
57
|
+
report_id: authorizedReportId,
|
|
58
|
+
scope: "reports:read patient:read",
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Pass only the delegated token to the dashboard. Client secrets and service tokens
|
|
63
|
+
must stay on the server. The helper caches service tokens and renews them before
|
|
64
|
+
expiry; delegated tokens expire after 300 seconds. Hosts request a fresh delegated
|
|
65
|
+
token when opening or renewing a dashboard session. OAuth failures never fall back
|
|
66
|
+
to legacy signing. Existing HS256 dashboard tokens and embed integrations remain
|
|
67
|
+
supported during migration.
|
|
68
|
+
|
|
40
69
|
```tsx
|
|
41
70
|
import { MetabolicDashboard } from "@metaboliccode-dev/widget";
|
|
42
71
|
import "@metaboliccode-dev/widget/styles.css";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface MetabolicOAuthClientOptions {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
fetch?: typeof globalThis.fetch;
|
|
6
|
+
}
|
|
7
|
+
export interface MetabolicAccessToken {
|
|
8
|
+
accessToken: string;
|
|
9
|
+
expiresIn: number;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
scope: string;
|
|
12
|
+
}
|
|
13
|
+
export interface MetabolicDelegatedIdentity {
|
|
14
|
+
sub: string;
|
|
15
|
+
role: string;
|
|
16
|
+
scope: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
patient_id?: string;
|
|
19
|
+
report_id?: string;
|
|
20
|
+
questionnaire_response_id?: string;
|
|
21
|
+
white_label_domain?: string;
|
|
22
|
+
firebase_project_id?: string;
|
|
23
|
+
actor?: {
|
|
24
|
+
uid: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
emailVerified?: boolean;
|
|
27
|
+
role?: string;
|
|
28
|
+
impersonatedBy?: {
|
|
29
|
+
uid: string;
|
|
30
|
+
email?: string;
|
|
31
|
+
role?: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface MetabolicEmbedLaunch {
|
|
36
|
+
subject: string;
|
|
37
|
+
role: string;
|
|
38
|
+
origin: string;
|
|
39
|
+
scope: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
patient_id?: string;
|
|
42
|
+
report_id?: string;
|
|
43
|
+
questionnaire_response_id?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare class MetabolicOAuthError extends Error {
|
|
46
|
+
readonly status: number;
|
|
47
|
+
readonly operation: string;
|
|
48
|
+
constructor(status: number, operation: string);
|
|
49
|
+
}
|
|
50
|
+
/** Server-only client. Never put its credentials or service tokens in browser props. */
|
|
51
|
+
export declare function createMetabolicOAuthClient(options: MetabolicOAuthClientOptions): {
|
|
52
|
+
getAccessToken: (scopes: readonly string[]) => Promise<MetabolicAccessToken>;
|
|
53
|
+
getDelegatedToken: (identity: MetabolicDelegatedIdentity) => Promise<MetabolicAccessToken>;
|
|
54
|
+
createEmbedLaunch: (identity: MetabolicEmbedLaunch) => Promise<{
|
|
55
|
+
embedToken: string;
|
|
56
|
+
expiresIn: number;
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
class l extends Error {
|
|
2
|
+
constructor(n, s) {
|
|
3
|
+
super(`Metabolic Code ${s} failed with HTTP ${n}.`), this.status = n, this.operation = s, this.name = "MetabolicOAuthError";
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
function A(e) {
|
|
7
|
+
if (typeof window < "u")
|
|
8
|
+
throw new Error("The Metabolic Code OAuth client must run on a server.");
|
|
9
|
+
const n = y(e.baseUrl);
|
|
10
|
+
if (!e.clientId || !e.clientSecret)
|
|
11
|
+
throw new Error("Metabolic Code OAuth client credentials are required.");
|
|
12
|
+
const s = e.fetch ?? globalThis.fetch, d = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map();
|
|
13
|
+
async function g(o) {
|
|
14
|
+
const t = Date.now(), r = btoa(
|
|
15
|
+
`${encodeURIComponent(e.clientId)}:${encodeURIComponent(e.clientSecret)}`
|
|
16
|
+
), i = await s(`${n}/oauth/token`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Basic ${r}`,
|
|
20
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
21
|
+
Accept: "application/json"
|
|
22
|
+
},
|
|
23
|
+
body: new URLSearchParams({ grant_type: "client_credentials", scope: o }),
|
|
24
|
+
cache: "no-store",
|
|
25
|
+
signal: AbortSignal.timeout(15e3)
|
|
26
|
+
});
|
|
27
|
+
if (!i.ok) throw new l(i.status, "token issuance");
|
|
28
|
+
return w(await i.json(), t);
|
|
29
|
+
}
|
|
30
|
+
async function u(o) {
|
|
31
|
+
const t = [...new Set(o)].sort().join(" ");
|
|
32
|
+
if (!t) throw new Error("At least one Metabolic Code OAuth scope is required.");
|
|
33
|
+
const r = d.get(t);
|
|
34
|
+
if (r && r.expiresAt > Date.now() + 3e4) return r;
|
|
35
|
+
const i = a.get(t);
|
|
36
|
+
if (i) return i;
|
|
37
|
+
const c = g(t).then((p) => (d.set(t, p), p)).finally(() => a.delete(t));
|
|
38
|
+
return a.set(t, c), c;
|
|
39
|
+
}
|
|
40
|
+
async function h(o, t, r) {
|
|
41
|
+
const i = await u([t]), c = await s(`${n}${o}`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
Authorization: `Bearer ${i.accessToken}`,
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
Accept: "application/json"
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify(r),
|
|
49
|
+
cache: "no-store",
|
|
50
|
+
signal: AbortSignal.timeout(15e3)
|
|
51
|
+
});
|
|
52
|
+
if (!c.ok) throw new l(c.status, "delegated authorization");
|
|
53
|
+
return c.json();
|
|
54
|
+
}
|
|
55
|
+
async function k(o) {
|
|
56
|
+
const t = Date.now(), r = await h(
|
|
57
|
+
"/api/v1/auth/delegated_tokens",
|
|
58
|
+
"tokens:delegate",
|
|
59
|
+
o
|
|
60
|
+
);
|
|
61
|
+
return w(r, t);
|
|
62
|
+
}
|
|
63
|
+
async function m(o) {
|
|
64
|
+
const t = await h("/api/v1/embed/launches", "embed:launch", {
|
|
65
|
+
embed: o
|
|
66
|
+
});
|
|
67
|
+
if (!f(t) || typeof t.embed_token != "string" || !b(t.expires_in))
|
|
68
|
+
throw new Error("Metabolic Code returned an invalid embed launch.");
|
|
69
|
+
return { embedToken: t.embed_token, expiresIn: t.expires_in };
|
|
70
|
+
}
|
|
71
|
+
return { getAccessToken: u, getDelegatedToken: k, createEmbedLaunch: m };
|
|
72
|
+
}
|
|
73
|
+
function y(e) {
|
|
74
|
+
const n = new URL(e), s = ["localhost", "127.0.0.1", "[::1]"].includes(n.hostname);
|
|
75
|
+
if (n.protocol !== "https:" && !(n.protocol === "http:" && s) || n.username || n.password || n.search || n.hash || n.pathname !== "/")
|
|
76
|
+
throw new Error("Metabolic Code OAuth requires an HTTPS origin or loopback HTTP origin.");
|
|
77
|
+
return n.origin;
|
|
78
|
+
}
|
|
79
|
+
function f(e) {
|
|
80
|
+
return typeof e == "object" && e !== null;
|
|
81
|
+
}
|
|
82
|
+
function b(e) {
|
|
83
|
+
return typeof e == "number" && Number.isInteger(e) && e > 0 && e <= 300;
|
|
84
|
+
}
|
|
85
|
+
function w(e, n) {
|
|
86
|
+
if (!f(e) || typeof e.access_token != "string" || !e.access_token || e.token_type !== "Bearer" || !b(e.expires_in) || typeof e.scope != "string")
|
|
87
|
+
throw new Error("Metabolic Code returned an invalid OAuth token response.");
|
|
88
|
+
return {
|
|
89
|
+
accessToken: e.access_token,
|
|
90
|
+
expiresIn: e.expires_in,
|
|
91
|
+
expiresAt: n + e.expires_in * 1e3,
|
|
92
|
+
scope: e.scope
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
l as MetabolicOAuthError,
|
|
97
|
+
A as createMetabolicOAuthClient
|
|
98
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaboliccode-dev/widget",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.88",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"import": "./dist/api.js",
|
|
22
22
|
"default": "./dist/api.js"
|
|
23
23
|
},
|
|
24
|
+
"./server": {
|
|
25
|
+
"types": "./dist/entry-server.d.ts",
|
|
26
|
+
"import": "./dist/server.js",
|
|
27
|
+
"default": "./dist/server.js"
|
|
28
|
+
},
|
|
24
29
|
"./ag-charts": {
|
|
25
30
|
"types": "./dist/entry-ag-charts.d.ts",
|
|
26
31
|
"import": "./dist/ag-charts.js",
|