@nexalink/carecompass-sdk 0.1.0-beta.8 → 0.2.0-beta.11

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/client.js CHANGED
@@ -1,3 +1,15 @@
1
+ import { validateCareCompassConfig } from "./schema.js";
2
+ import { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, createCareCompassTelemetry, } from "./telemetry.js";
3
+ // Internal error metadata keeps telemetry classification separate from messages,
4
+ // which may include user-visible API details that should not be emitted.
5
+ class CareCompassClientError extends Error {
6
+ constructor(message, telemetryErrorCode, telemetryFailureCategory) {
7
+ super(message);
8
+ this.telemetryErrorCode = telemetryErrorCode;
9
+ this.telemetryFailureCategory = telemetryFailureCategory;
10
+ this.name = "CareCompassClientError";
11
+ }
12
+ }
1
13
  const joinUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}${path}`;
2
14
  const isLocalhostUrl = (value) => {
3
15
  try {
@@ -16,59 +28,186 @@ const isBrowserHostedAwayFromLocalhost = (apiBaseUrl) => {
16
28
  return (window.location.hostname !== "localhost" &&
17
29
  window.location.hostname !== "127.0.0.1");
18
30
  };
19
- export function createCareCompassClient(config, options = {}) {
31
+ // Keep HTTP status mapping centralized so every client command reports failures
32
+ // with the same public telemetry taxonomy.
33
+ const responseError = (status, message) => {
34
+ if (status === 401 || status === 403) {
35
+ return new CareCompassClientError(message, CARECOMPASS_TELEMETRY_ERROR_CODES.AUTHENTICATION_FAILED, "authentication");
36
+ }
37
+ if (status === 429) {
38
+ return new CareCompassClientError(message, CARECOMPASS_TELEMETRY_ERROR_CODES.RATE_LIMITED, "rate_limit");
39
+ }
40
+ if (status >= 500) {
41
+ return new CareCompassClientError(message, CARECOMPASS_TELEMETRY_ERROR_CODES.BACKEND_UNAVAILABLE, "network");
42
+ }
43
+ return new CareCompassClientError(message, CARECOMPASS_TELEMETRY_ERROR_CODES.API_ERROR, "unknown");
44
+ };
45
+ const telemetryFailure = (error) => error instanceof CareCompassClientError
46
+ ? {
47
+ errorCode: error.telemetryErrorCode,
48
+ failureCategory: error.telemetryFailureCategory,
49
+ }
50
+ : {
51
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.INFERENCE_FAILED,
52
+ failureCategory: "unknown",
53
+ };
54
+ const captureInferenceFailure = (telemetry, error, durationMs) => {
55
+ const failure = telemetryFailure(error);
56
+ telemetry.capture(failure.errorCode === CARECOMPASS_TELEMETRY_ERROR_CODES.RATE_LIMITED
57
+ ? CARECOMPASS_TELEMETRY_EVENTS.INFERENCE_RATE_LIMITED
58
+ : CARECOMPASS_TELEMETRY_EVENTS.INFERENCE_FAILED, { ...failure, durationMs });
59
+ };
60
+ export function createCareCompassClient(inputConfig, options = {}) {
61
+ const telemetry = createCareCompassTelemetry(inputConfig);
62
+ const now = options.now ?? Date.now;
63
+ let config;
64
+ try {
65
+ const validation = validateCareCompassConfig(inputConfig);
66
+ if (!validation.ok) {
67
+ const promptInvalid = validation.errors.some((error) => error.startsWith("prompt."));
68
+ if (promptInvalid) {
69
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.PROMPT_CONFIGURATION_FAILED, {
70
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIGURATION_INVALID,
71
+ failureCategory: "configuration",
72
+ });
73
+ }
74
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.SDK_CONFIGURATION_FAILED, {
75
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIGURATION_INVALID,
76
+ failureCategory: "configuration",
77
+ });
78
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.SDK_INITIALIZATION_FAILED, {
79
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIGURATION_INVALID,
80
+ failureCategory: "configuration",
81
+ });
82
+ throw new CareCompassClientError(`Invalid CareCompass config:\n${validation.errors.join("\n")}`, CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIGURATION_INVALID, "configuration");
83
+ }
84
+ config = validation.config;
85
+ }
86
+ catch (error) {
87
+ if (error instanceof CareCompassClientError)
88
+ throw error;
89
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.SDK_CONFIGURATION_FAILED, {
90
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIGURATION_INVALID,
91
+ failureCategory: "configuration",
92
+ });
93
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.SDK_INITIALIZATION_FAILED, {
94
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIGURATION_INVALID,
95
+ failureCategory: "configuration",
96
+ });
97
+ throw new CareCompassClientError("Invalid CareCompass config.", CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIGURATION_INVALID, "configuration");
98
+ }
20
99
  const fetcher = options.fetcher ?? fetch;
100
+ // These headers are the public embed identity; secret API keys stay in the CLI.
21
101
  const headers = {
22
102
  "content-type": "application/json",
23
103
  "x-carecompass-payer-id": config.payerId,
24
104
  "x-carecompass-embed-key": config.embedKey,
25
105
  "x-carecompass-environment": config.environment,
26
106
  };
107
+ const jobStartedAt = new Map();
108
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.SDK_INITIALIZED);
109
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.THEME_LOADED);
110
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.PROMPT_CONFIGURATION_LOADED);
27
111
  async function send(input, init) {
112
+ // Hosted demos and sandboxes cannot call a developer's local API server, so
113
+ // fail early with a setup hint instead of surfacing an opaque fetch error.
28
114
  if (isBrowserHostedAwayFromLocalhost(config.apiBaseUrl)) {
29
- throw new Error(`CareCompass apiBaseUrl is ${config.apiBaseUrl}, but this page is running at ${window.location.origin}. A hosted sandbox or payer site cannot reach your local Next.js server. Use a deployed Nexalink API base URL, or run both the payer page and Nexalink web app locally on the same machine.`);
115
+ throw new CareCompassClientError(`CareCompass apiBaseUrl is ${config.apiBaseUrl}, but this page is running at ${window.location.origin}. A hosted sandbox or remote site cannot reach your local Next.js server. Use a deployed Nexalink API base URL, or run both the integrating page and Nexalink web app locally on the same machine.`, CARECOMPASS_TELEMETRY_ERROR_CODES.NETWORK_ERROR, "network");
30
116
  }
31
117
  try {
32
118
  return await fetcher(input, init);
33
119
  }
34
120
  catch (error) {
121
+ if (error instanceof CareCompassClientError)
122
+ throw error;
35
123
  const message = error instanceof Error ? error.message : "Network request failed";
36
- throw new Error(`CareCompass could not reach ${config.apiBaseUrl}. Confirm apiBaseUrl, CORS, allowedOrigins, and Nexalink hosted API configuration. ${message}`);
124
+ throw new CareCompassClientError(`CareCompass could not reach ${config.apiBaseUrl}. Confirm apiBaseUrl, CORS, allowedOrigins, and Nexalink hosted API configuration. ${message}`, CARECOMPASS_TELEMETRY_ERROR_CODES.NETWORK_ERROR, "network");
37
125
  }
38
126
  }
39
127
  async function parseJson(response) {
40
128
  const body = (await response.json().catch(() => ({})));
41
129
  if (!response.ok) {
42
- throw new Error(body.error ?? "CareCompass request failed.");
130
+ throw responseError(response.status, body.error ?? "CareCompass request failed.");
43
131
  }
44
132
  return body;
45
133
  }
46
134
  return {
47
135
  validateConfig: async () => {
48
- const response = await send(joinUrl(config.apiBaseUrl, "/api/carecompass/embed/config/validate"), {
49
- method: "POST",
50
- headers,
51
- body: JSON.stringify({ config }),
52
- });
53
- return parseJson(response);
136
+ const startedAt = now();
137
+ try {
138
+ const response = await send(joinUrl(config.apiBaseUrl, "/api/carecompass/embed/config/validate"), {
139
+ method: "POST",
140
+ headers,
141
+ body: JSON.stringify({ config }),
142
+ });
143
+ const result = await parseJson(response);
144
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.SDK_CONFIGURATION_VALIDATED, { durationMs: now() - startedAt });
145
+ return result;
146
+ }
147
+ catch (error) {
148
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.SDK_CONFIGURATION_FAILED, { ...telemetryFailure(error), durationMs: now() - startedAt });
149
+ throw error;
150
+ }
54
151
  },
55
152
  ask: async (request) => {
56
- const response = await send(joinUrl(config.apiBaseUrl, "/api/carecompass/embed/ask"), {
57
- method: "POST",
58
- headers,
59
- body: JSON.stringify({
60
- prompt: request.prompt,
61
- sessionId: request.sessionId,
62
- userContext: request.userContext,
63
- promptConfig: config.prompt,
64
- }),
65
- });
66
- return parseJson(response);
153
+ const startedAt = now();
154
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.PROMPT_SUBMITTED);
155
+ try {
156
+ const response = await send(joinUrl(config.apiBaseUrl, "/api/carecompass/embed/ask"), {
157
+ method: "POST",
158
+ headers,
159
+ body: JSON.stringify({
160
+ prompt: request.prompt,
161
+ sessionId: request.sessionId,
162
+ userContext: request.userContext,
163
+ promptConfig: config.prompt,
164
+ }),
165
+ });
166
+ const result = await parseJson(response);
167
+ // Status polling is asynchronous, so retain the submit timestamp until
168
+ // the final job state lets telemetry report end-to-end latency.
169
+ jobStartedAt.set(result.jobId, startedAt);
170
+ return result;
171
+ }
172
+ catch (error) {
173
+ captureInferenceFailure(telemetry, error, now() - startedAt);
174
+ throw error;
175
+ }
67
176
  },
68
177
  status: async (jobId) => {
69
- const response = await send(joinUrl(config.apiBaseUrl, `/api/carecompass/embed/status/${encodeURIComponent(jobId)}`), { headers });
70
- return parseJson(response);
178
+ const startedAt = jobStartedAt.get(jobId) ?? now();
179
+ try {
180
+ const response = await send(joinUrl(config.apiBaseUrl, `/api/carecompass/embed/status/${encodeURIComponent(jobId)}`), { headers });
181
+ const result = await parseJson(response);
182
+ if (result.status === "complete") {
183
+ jobStartedAt.delete(jobId);
184
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.INFERENCE_SUCCEEDED, {
185
+ durationMs: now() - startedAt,
186
+ });
187
+ }
188
+ else if (result.status === "failed") {
189
+ jobStartedAt.delete(jobId);
190
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.INFERENCE_FAILED, {
191
+ durationMs: now() - startedAt,
192
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.INFERENCE_FAILED,
193
+ failureCategory: "inference",
194
+ });
195
+ }
196
+ return result;
197
+ }
198
+ catch (error) {
199
+ jobStartedAt.delete(jobId);
200
+ captureInferenceFailure(telemetry, error, now() - startedAt);
201
+ throw error;
202
+ }
203
+ },
204
+ recordInferenceTimeout: (jobId, durationMs) => {
205
+ jobStartedAt.delete(jobId);
206
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.INFERENCE_FAILED, {
207
+ durationMs,
208
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.INFERENCE_TIMEOUT,
209
+ failureCategory: "timeout",
210
+ });
71
211
  },
72
212
  };
73
213
  }
74
- //# sourceMappingURL=client.js.map
package/dist/component.js CHANGED
@@ -1,7 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { createCareCompassClient } from "./client.js";
4
- export function CareCompassEmbed({ config, pollIntervalMs = 1500, }) {
4
+ import { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, createCareCompassTelemetry, } from "./telemetry.js";
5
+ // The embed should fail closed inside the host page: show the configured
6
+ // fallback text, capture sanitized telemetry, and avoid crashing the integrator.
7
+ class CareCompassErrorBoundary extends React.Component {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.state = { failed: false };
11
+ }
12
+ static getDerivedStateFromError() {
13
+ return { failed: true };
14
+ }
15
+ componentDidCatch() {
16
+ this.props.telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.COMPONENT_ERROR, {
17
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.RENDER_FAILED,
18
+ failureCategory: "rendering",
19
+ });
20
+ }
21
+ render() {
22
+ if (this.state.failed) {
23
+ return _jsx("div", { role: "alert", children: this.props.config.labels.errorMessage });
24
+ }
25
+ return this.props.children;
26
+ }
27
+ }
28
+ function CareCompassEmbedContent({ config, telemetry, pollIntervalMs = 1500, }) {
5
29
  const client = React.useMemo(() => createCareCompassClient(config), [config]);
6
30
  const [messages, setMessages] = React.useState([
7
31
  {
@@ -13,6 +37,13 @@ export function CareCompassEmbed({ config, pollIntervalMs = 1500, }) {
13
37
  const [input, setInput] = React.useState("");
14
38
  const [loading, setLoading] = React.useState(false);
15
39
  const [error, setError] = React.useState(null);
40
+ React.useEffect(() => {
41
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.COMPONENT_RENDERED);
42
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.COMPONENT_OPENED);
43
+ return () => {
44
+ telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.COMPONENT_CLOSED);
45
+ };
46
+ }, [telemetry]);
16
47
  async function sendMessage(event) {
17
48
  event.preventDefault();
18
49
  const prompt = input.trim();
@@ -28,6 +59,8 @@ export function CareCompassEmbed({ config, pollIntervalMs = 1500, }) {
28
59
  try {
29
60
  const { jobId } = await client.ask({ prompt });
30
61
  const startedAt = Date.now();
62
+ // The backend returns a job immediately, so the component polls until it
63
+ // sees a terminal state or reaches the same timeout tracked by telemetry.
31
64
  while (Date.now() - startedAt < 120_000) {
32
65
  const status = await client.status(jobId);
33
66
  if (status.status === "complete" && status.result) {
@@ -46,6 +79,7 @@ export function CareCompassEmbed({ config, pollIntervalMs = 1500, }) {
46
79
  }
47
80
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
48
81
  }
82
+ client.recordInferenceTimeout(jobId, Date.now() - startedAt);
49
83
  throw new Error("CareCompass request timed out.");
50
84
  }
51
85
  catch (err) {
@@ -119,4 +153,9 @@ export function CareCompassEmbed({ config, pollIntervalMs = 1500, }) {
119
153
  padding: "0 14px",
120
154
  }, type: "submit", children: config.labels.sendButton })] }), error ? (_jsx("div", { role: "alert", style: { color: "#b91c1c", fontSize: 12 }, children: error })) : null, _jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: config.labels.disclaimer })] })] }));
121
155
  }
122
- //# sourceMappingURL=component.js.map
156
+ export function CareCompassEmbed({ config, pollIntervalMs = 1500, }) {
157
+ // Telemetry is scoped to the validated public config; no per-message content
158
+ // is passed through this component-level telemetry object.
159
+ const telemetry = React.useMemo(() => createCareCompassTelemetry(config), [config]);
160
+ return (_jsx(CareCompassErrorBoundary, { config: config, telemetry: telemetry, children: _jsx(CareCompassEmbedContent, { config: config, pollIntervalMs: pollIntervalMs, telemetry: telemetry }) }));
161
+ }
@@ -20,6 +20,8 @@ export function readStoredApiKey(path = resolveCredentialsPath()) {
20
20
  : null;
21
21
  }
22
22
  catch {
23
+ // Missing, unreadable, or malformed credential files are treated the same so
24
+ // callers can fall through to the next credential source without leaking paths.
23
25
  return null;
24
26
  }
25
27
  }
@@ -37,6 +39,8 @@ export function writeStoredApiKey(apiKey, path = resolveCredentialsPath()) {
37
39
  }
38
40
  }
39
41
  export function resolveApiKey(argumentApiKey, env = process.env, credentialsPath = resolveCredentialsPath(env)) {
42
+ // Precedence mirrors common CLI behavior: explicit flag, then environment,
43
+ // then the local credential store created by `carecompass login` or `init`.
40
44
  const argKey = normalizeApiKey(argumentApiKey);
41
45
  if (argKey)
42
46
  return { apiKey: argKey, source: "argument" };
@@ -48,4 +52,3 @@ export function resolveApiKey(argumentApiKey, env = process.env, credentialsPath
48
52
  return { apiKey: fileKey, source: "file" };
49
53
  return { apiKey: null, source: "missing" };
50
54
  }
51
- //# sourceMappingURL=credentials.js.map
package/dist/index.d.ts CHANGED
@@ -3,5 +3,8 @@ export type { CareCompassAskRequest, CareCompassAskResponse, CareCompassStatusRe
3
3
  export { CareCompassEmbed } from "./component.js";
4
4
  export { mountCareCompass } from "./mount.js";
5
5
  export { generateCareCompassIframeSnippet, generateCareCompassSnippet, } from "./snippet.js";
6
- export { careCompassConfigSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
7
- export type { CareCompassConfig, CareCompassLabels, CareCompassPromptConfig, CareCompassTheme, CareCompassValidationResult, } from "./schema.js";
6
+ export { careCompassApiBaseUrlSchema, careCompassConfigSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
7
+ export type { CareCompassConfig, CareCompassConfigInput, CareCompassLabels, CareCompassPromptConfig, CareCompassTheme, CareCompassValidationResult, } from "./schema.js";
8
+ export { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, careCompassTelemetryDurationBucket, createCareCompassCliTelemetry, createCareCompassTelemetry, } from "./telemetry.js";
9
+ export type { CareCompassTelemetry, CareCompassTelemetryCaptureOptions, CareCompassTelemetryDurationBucket, CareCompassTelemetryErrorCode, CareCompassTelemetryEvent, CareCompassTelemetryFailureCategory, } from "./telemetry.js";
10
+ export { CARECOMPASS_COMPONENT_VERSION, CARECOMPASS_SDK_VERSION, CARECOMPASS_TELEMETRY_CONTRACT_VERSION, } from "./version.js";
package/dist/index.js CHANGED
@@ -2,5 +2,6 @@ export { createCareCompassClient } from "./client.js";
2
2
  export { CareCompassEmbed } from "./component.js";
3
3
  export { mountCareCompass } from "./mount.js";
4
4
  export { generateCareCompassIframeSnippet, generateCareCompassSnippet, } from "./snippet.js";
5
- export { careCompassConfigSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
6
- //# sourceMappingURL=index.js.map
5
+ export { careCompassApiBaseUrlSchema, careCompassConfigSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
6
+ export { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, careCompassTelemetryDurationBucket, createCareCompassCliTelemetry, createCareCompassTelemetry, } from "./telemetry.js";
7
+ export { CARECOMPASS_COMPONENT_VERSION, CARECOMPASS_SDK_VERSION, CARECOMPASS_TELEMETRY_CONTRACT_VERSION, } from "./version.js";
package/dist/mount.js CHANGED
@@ -1,13 +1,21 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import { CareCompassEmbed } from "./component.js";
4
+ import { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, createCareCompassTelemetry, } from "./telemetry.js";
4
5
  export function mountCareCompass(element, config) {
5
- if (!element)
6
+ if (!element) {
7
+ // Missing mount points usually mean a copied snippet and page markup drifted
8
+ // apart, so capture the setup error before throwing to the caller.
9
+ createCareCompassTelemetry(config).capture(CARECOMPASS_TELEMETRY_EVENTS.COMPONENT_ERROR, {
10
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.MOUNT_ELEMENT_MISSING,
11
+ failureCategory: "rendering",
12
+ });
6
13
  throw new Error("CareCompass mount element was not found.");
14
+ }
7
15
  const root = createRoot(element);
8
16
  root.render(_jsx(CareCompassEmbed, { config: config }));
9
17
  return {
18
+ // Return the small React root facade so non-React hosts can clean up embeds.
10
19
  unmount: () => root.unmount(),
11
20
  };
12
21
  }
13
- //# sourceMappingURL=mount.js.map
package/dist/schema.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  export declare const careCompassOriginSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
3
+ export declare const careCompassApiBaseUrlSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
3
4
  export declare const careCompassThemeSchema: z.ZodObject<{
4
5
  primaryColor: z.ZodString;
5
6
  backgroundColor: z.ZodDefault<z.ZodString>;
@@ -40,7 +41,7 @@ export declare const careCompassConfigSchema: z.ZodObject<{
40
41
  staging: "staging";
41
42
  production: "production";
42
43
  }>;
43
- apiBaseUrl: z.ZodString;
44
+ apiBaseUrl: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
44
45
  embedKey: z.ZodString;
45
46
  allowedOrigins: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
46
47
  branding: z.ZodObject<{
@@ -88,6 +89,7 @@ export type CareCompassTheme = z.infer<typeof careCompassThemeSchema>;
88
89
  export type CareCompassLabels = z.infer<typeof careCompassLabelsSchema>;
89
90
  export type CareCompassPromptConfig = z.infer<typeof careCompassPromptConfigSchema>;
90
91
  export type CareCompassConfig = z.infer<typeof careCompassConfigSchema>;
92
+ export type CareCompassConfigInput = z.input<typeof careCompassConfigSchema>;
91
93
  export type CareCompassValidationResult = {
92
94
  ok: true;
93
95
  config: CareCompassConfig;
package/dist/schema.js CHANGED
@@ -2,13 +2,40 @@ import { z } from "zod";
2
2
  const HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
3
3
  const PAYER_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{2,63}$/;
4
4
  const EMBED_KEY_RE = /^[A-Za-z0-9._-]{16,128}$/;
5
+ const NEXALINK_HOSTNAME = "nexalink.care";
6
+ // These patterns are a local preflight check, not the full policy layer. They
7
+ // catch obvious prompt configurations that conflict with Nexalink guardrails.
5
8
  const unsafePromptPatterns = [
6
9
  /ignore (all )?(previous|prior) (instructions|rules)/i,
7
10
  /bypass (safety|guardrails|compliance)/i,
8
11
  /disable (safety|guardrails|compliance)/i,
9
12
  /provide (a )?(diagnosis|treatment plan)/i,
13
+ /reveal (the )?(system|developer) (prompt|instructions)/i,
14
+ /override (nexalink|carecompass) (safety|guardrails|policy)/i,
10
15
  /return raw (phi|credentials|tokens|secrets)/i,
11
16
  ];
17
+ const isLocalhostHostname = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
18
+ const isNexalinkHostname = (hostname) => hostname === NEXALINK_HOSTNAME || hostname.endsWith(`.${NEXALINK_HOSTNAME}`);
19
+ const parseUrl = (value) => {
20
+ try {
21
+ return new URL(value);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ };
27
+ const isLocalhostUrl = (value) => {
28
+ const parsed = parseUrl(value);
29
+ return parsed ? isLocalhostHostname(parsed.hostname) : false;
30
+ };
31
+ const isNexalinkHostedApiUrl = (value) => {
32
+ const parsed = parseUrl(value);
33
+ if (!parsed)
34
+ return false;
35
+ if (isLocalhostHostname(parsed.hostname))
36
+ return true;
37
+ return parsed.protocol === "https:" && isNexalinkHostname(parsed.hostname);
38
+ };
12
39
  export const careCompassOriginSchema = z
13
40
  .string()
14
41
  .url("Allowed origins must be valid URLs.")
@@ -17,6 +44,13 @@ export const careCompassOriginSchema = z
17
44
  const parsed = new URL(value);
18
45
  return parsed.protocol === "https:" || parsed.hostname === "localhost";
19
46
  }, "Allowed origins must use HTTPS, except localhost for development.");
47
+ export const careCompassApiBaseUrlSchema = z
48
+ .string()
49
+ .url("API base URL must be a valid URL.")
50
+ .transform((value) => value.replace(/\/+$/, ""))
51
+ // Runtime traffic should stay on Nexalink-hosted HTTPS endpoints; localhost is
52
+ // kept only for development because integrators need to test local web apps.
53
+ .refine(isNexalinkHostedApiUrl, "API base URL must be a Nexalink-hosted HTTPS URL, except localhost for development.");
20
54
  export const careCompassThemeSchema = z.object({
21
55
  primaryColor: z.string().regex(HEX_COLOR_RE, "Use a hex color like #2563eb."),
22
56
  backgroundColor: z
@@ -72,17 +106,32 @@ export const careCompassPromptConfigSchema = z
72
106
  });
73
107
  export const careCompassBrandingSchema = z.object({
74
108
  displayName: z.string().min(1).max(80),
75
- logoUrl: z.string().url().optional(),
76
- supportUrl: z.string().url().optional(),
109
+ logoUrl: z
110
+ .string()
111
+ .url()
112
+ .refine((value) => {
113
+ const parsed = new URL(value);
114
+ return (parsed.protocol === "https:" || isLocalhostHostname(parsed.hostname));
115
+ }, "Logo URL must use HTTPS, except localhost for development.")
116
+ .optional(),
117
+ supportUrl: z
118
+ .string()
119
+ .url()
120
+ .refine((value) => {
121
+ const parsed = new URL(value);
122
+ return (parsed.protocol === "https:" || isLocalhostHostname(parsed.hostname));
123
+ }, "Support URL must use HTTPS, except localhost for development.")
124
+ .optional(),
77
125
  });
78
126
  export const careCompassPlacementSchema = z.object({
79
127
  mode: z.enum(["inline", "floating", "sidebar"]).default("inline"),
80
128
  selector: z.string().min(1).max(120).optional(),
81
129
  });
82
- export const careCompassConfigSchema = z.object({
130
+ export const careCompassConfigSchema = z
131
+ .object({
83
132
  payerId: z.string().regex(PAYER_ID_RE, "Invalid payer ID."),
84
133
  environment: z.enum(["development", "staging", "production"]),
85
- apiBaseUrl: z.string().url("API base URL must be a valid URL."),
134
+ apiBaseUrl: careCompassApiBaseUrlSchema,
86
135
  embedKey: z.string().regex(EMBED_KEY_RE, "Invalid public embed key."),
87
136
  allowedOrigins: z
88
137
  .array(careCompassOriginSchema)
@@ -104,6 +153,28 @@ export const careCompassConfigSchema = z.object({
104
153
  deploymentTarget: z
105
154
  .enum(["react", "nextjs", "static", "web", "other"])
106
155
  .default("web"),
156
+ })
157
+ .superRefine((value, ctx) => {
158
+ // Development can point both sides at localhost. Staging and production must
159
+ // use deployable origins so embeds cannot ship with machine-local endpoints.
160
+ if (value.environment === "development")
161
+ return;
162
+ if (isLocalhostUrl(value.apiBaseUrl)) {
163
+ ctx.addIssue({
164
+ code: "custom",
165
+ path: ["apiBaseUrl"],
166
+ message: "Staging and production API base URLs must use a Nexalink-hosted HTTPS URL.",
167
+ });
168
+ }
169
+ for (const [index, origin] of value.allowedOrigins.entries()) {
170
+ if (isLocalhostUrl(origin)) {
171
+ ctx.addIssue({
172
+ code: "custom",
173
+ path: ["allowedOrigins", index],
174
+ message: "Staging and production allowed origins must not use localhost.",
175
+ });
176
+ }
177
+ }
107
178
  });
108
179
  export function validateCareCompassConfig(value) {
109
180
  const result = careCompassConfigSchema.safeParse(value);
@@ -111,6 +182,8 @@ export function validateCareCompassConfig(value) {
111
182
  return { ok: true, config: result.data };
112
183
  return {
113
184
  ok: false,
185
+ // Flatten Zod issue paths into CLI-friendly strings while preserving enough
186
+ // context for users to find the bad field quickly.
114
187
  errors: result.error.issues.map((issue) => {
115
188
  const path = issue.path.length ? `${issue.path.join(".")}: ` : "";
116
189
  return `${path}${issue.message}`;
@@ -118,6 +191,8 @@ export function validateCareCompassConfig(value) {
118
191
  };
119
192
  }
120
193
  export function createDefaultCareCompassConfig(overrides = {}) {
194
+ // Defaults are intentionally valid staging examples so tests and generated
195
+ // setup kits can start from a complete config and override only local details.
121
196
  return careCompassConfigSchema.parse({
122
197
  payerId: "payer-demo",
123
198
  environment: "staging",
@@ -142,4 +217,3 @@ export function createDefaultCareCompassConfig(overrides = {}) {
142
217
  ...overrides,
143
218
  });
144
219
  }
145
- //# sourceMappingURL=schema.js.map
package/dist/snippet.js CHANGED
@@ -1,9 +1,26 @@
1
+ const SCRIPT_JSON_ESCAPES = {
2
+ "<": "\\u003c",
3
+ ">": "\\u003e",
4
+ "&": "\\u0026",
5
+ "\u2028": "\\u2028",
6
+ "\u2029": "\\u2029",
7
+ };
8
+ const escapeHtmlAttribute = (value) => value
9
+ .replaceAll("&", "&amp;")
10
+ .replaceAll('"', "&quot;")
11
+ .replaceAll("<", "&lt;")
12
+ .replaceAll(">", "&gt;");
13
+ // Script JSON needs stronger escaping than attributes because `</script>` and
14
+ // Unicode line separators can terminate or break the generated module.
15
+ const serializeScriptJson = (value) => JSON.stringify(value, null, 2).replace(/[<>&\u2028\u2029]/g, (character) => SCRIPT_JSON_ESCAPES[character]);
1
16
  export function generateCareCompassIframeSnippet(config, options = {}) {
2
17
  const src = options.src ?? "/carecompass/carecompass-embed.html";
3
18
  const title = `${config.branding.displayName} CareCompass`;
19
+ // The iframe form is intentionally static so it can be pasted into CMS pages
20
+ // or server-rendered apps without needing a bundler.
4
21
  return `<iframe
5
- title="${title.replaceAll('"', "&quot;")}"
6
- src="${src.replaceAll('"', "&quot;")}"
22
+ title="${escapeHtmlAttribute(title)}"
23
+ src="${escapeHtmlAttribute(src)}"
7
24
  style="width: 100%; min-height: 640px; border: 0; border-radius: ${config.theme.borderRadius};"
8
25
  loading="lazy"
9
26
  ></iframe>`;
@@ -13,12 +30,13 @@ export function generateCareCompassSnippet(config, options = {}) {
13
30
  const moduleUrl = options.mode === "cdn"
14
31
  ? "https://esm.sh/@nexalink/carecompass-sdk"
15
32
  : "@nexalink/carecompass-sdk";
33
+ // The config is embedded beside the mount call so the generated page is
34
+ // deployable as a single static asset.
16
35
  return `<div id="${targetId}"></div>
17
36
  <script type="module">
18
37
  import { mountCareCompass } from "${moduleUrl}";
19
38
 
20
- const config = ${JSON.stringify(config, null, 2)};
39
+ const config = ${serializeScriptJson(config)};
21
40
  mountCareCompass(document.getElementById("${targetId}"), config);
22
41
  </script>`;
23
42
  }
24
- //# sourceMappingURL=snippet.js.map
@@ -0,0 +1,71 @@
1
+ import type { CareCompassConfig } from "./schema.js";
2
+ export declare const CARECOMPASS_TELEMETRY_EVENTS: {
3
+ readonly SDK_INITIALIZED: "carecompass_sdk_initialized";
4
+ readonly SDK_INITIALIZATION_FAILED: "carecompass_sdk_initialization_failed";
5
+ readonly SDK_CONFIGURATION_VALIDATED: "carecompass_sdk_configuration_validated";
6
+ readonly SDK_CONFIGURATION_FAILED: "carecompass_sdk_configuration_failed";
7
+ readonly COMPONENT_RENDERED: "carecompass_component_rendered";
8
+ readonly COMPONENT_OPENED: "carecompass_component_opened";
9
+ readonly COMPONENT_CLOSED: "carecompass_component_closed";
10
+ readonly COMPONENT_ERROR: "carecompass_component_error";
11
+ readonly PROMPT_SUBMITTED: "carecompass_prompt_submitted";
12
+ readonly INFERENCE_SUCCEEDED: "carecompass_inference_succeeded";
13
+ readonly INFERENCE_FAILED: "carecompass_inference_failed";
14
+ readonly INFERENCE_RATE_LIMITED: "carecompass_inference_rate_limited";
15
+ readonly THEME_LOADED: "carecompass_theme_loaded";
16
+ readonly PROMPT_CONFIGURATION_LOADED: "carecompass_prompt_configuration_loaded";
17
+ readonly PROMPT_CONFIGURATION_FAILED: "carecompass_prompt_configuration_failed";
18
+ readonly CLI_STARTED: "carecompass_cli_started";
19
+ readonly CLI_LOGIN_STARTED: "carecompass_cli_login_started";
20
+ readonly CLI_LOGIN_SUCCEEDED: "carecompass_cli_login_succeeded";
21
+ readonly CLI_LOGIN_FAILED: "carecompass_cli_login_failed";
22
+ readonly CLI_INIT_STARTED: "carecompass_cli_init_started";
23
+ readonly CLI_INIT_FAILED: "carecompass_cli_init_failed";
24
+ readonly CLI_SETUP_KIT_GENERATED: "carecompass_cli_setup_kit_generated";
25
+ readonly CLI_VALIDATION_SUCCEEDED: "carecompass_cli_validation_succeeded";
26
+ readonly CLI_VALIDATION_FAILED: "carecompass_cli_validation_failed";
27
+ readonly CLI_SNIPPET_GENERATED: "carecompass_cli_snippet_generated";
28
+ readonly CLI_DOCTOR_COMPLETED: "carecompass_cli_doctor_completed";
29
+ readonly CLI_COMMAND_FAILED: "carecompass_cli_command_failed";
30
+ };
31
+ export type CareCompassTelemetryEvent = (typeof CARECOMPASS_TELEMETRY_EVENTS)[keyof typeof CARECOMPASS_TELEMETRY_EVENTS];
32
+ export declare const CARECOMPASS_TELEMETRY_ERROR_CODES: {
33
+ readonly API_ERROR: "api_error";
34
+ readonly AUTHENTICATION_FAILED: "authentication_failed";
35
+ readonly BACKEND_UNAVAILABLE: "backend_unavailable";
36
+ readonly CONFIGURATION_INVALID: "configuration_invalid";
37
+ readonly INFERENCE_FAILED: "inference_failed";
38
+ readonly INFERENCE_TIMEOUT: "inference_timeout";
39
+ readonly MOUNT_ELEMENT_MISSING: "mount_element_missing";
40
+ readonly NETWORK_ERROR: "network_error";
41
+ readonly RATE_LIMITED: "rate_limited";
42
+ readonly RENDER_FAILED: "render_failed";
43
+ readonly CLI_COMMAND_FAILED: "cli_command_failed";
44
+ };
45
+ export type CareCompassTelemetryErrorCode = (typeof CARECOMPASS_TELEMETRY_ERROR_CODES)[keyof typeof CARECOMPASS_TELEMETRY_ERROR_CODES];
46
+ export type CareCompassTelemetryFailureCategory = "authentication" | "configuration" | "inference" | "network" | "rate_limit" | "rendering" | "timeout" | "unknown";
47
+ export type CareCompassTelemetryDurationBucket = "under_250_ms" | "250_to_999_ms" | "1_to_4_999_ms" | "5_to_29_999_ms" | "30_s_or_more";
48
+ export type CareCompassTelemetryCaptureOptions = {
49
+ durationMs?: number;
50
+ errorCode?: CareCompassTelemetryErrorCode;
51
+ failureCategory?: CareCompassTelemetryFailureCategory;
52
+ };
53
+ type Fetcher = typeof fetch;
54
+ export type CareCompassTelemetry = {
55
+ enabled: boolean;
56
+ capture: (event: CareCompassTelemetryEvent, options?: CareCompassTelemetryCaptureOptions) => void;
57
+ captureAsync: (event: CareCompassTelemetryEvent, options?: CareCompassTelemetryCaptureOptions) => Promise<void>;
58
+ };
59
+ export declare const careCompassTelemetryDurationBucket: (durationMs: number) => CareCompassTelemetryDurationBucket;
60
+ export declare function createCareCompassTelemetry(config: Pick<CareCompassConfig, "environment" | "payerId">, options?: {
61
+ fetcher?: Fetcher;
62
+ now?: () => Date;
63
+ }): CareCompassTelemetry;
64
+ export declare function createCareCompassCliTelemetry(sessionId: string, context?: {
65
+ environment?: CareCompassConfig["environment"];
66
+ payerId?: string;
67
+ }, options?: {
68
+ fetcher?: Fetcher;
69
+ now?: () => Date;
70
+ }): CareCompassTelemetry;
71
+ export {};