@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 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.87",
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",