@nexalink/carecompass-sdk 0.4.0-beta.15 → 0.5.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/README.md +282 -3
- package/dist/cli.js +0 -0
- package/dist/client.d.ts +32 -2
- package/dist/client.js +42 -3
- package/dist/compliance.d.ts +69 -0
- package/dist/compliance.js +121 -0
- package/dist/component.d.ts +24 -2
- package/dist/component.js +63 -4
- package/dist/core.d.ts +19 -2
- package/dist/core.js +140 -8
- package/dist/index.d.ts +11 -5
- package/dist/index.js +5 -2
- package/dist/markdown.d.ts +2 -1
- package/dist/mount.d.ts +4 -1
- package/dist/mount.js +2 -2
- package/dist/rcm.d.ts +93 -0
- package/dist/rcm.js +284 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +1 -0
- package/dist/schema.d.ts +49 -0
- package/dist/schema.js +36 -0
- package/dist/telemetry.js +114 -82
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/dist/web-component.d.ts +13 -0
- package/dist/web-component.js +236 -0
- package/package.json +27 -12
- package/qa-fixtures/DEMO-ENC-1042-rcm-policy.md +27 -0
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ Nexalink-hosted CareCompass APIs
|
|
|
79
79
|
CareCompass AI runtime
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
The browser receives only public embed configuration: tenant identifier, environment, public embed key, allowed origins, branding, labels, theme, placement, and approved prompt settings.
|
|
82
|
+
The browser receives only public embed configuration: tenant identifier, environment, public embed key, allowed origins, endpoint, model preferences, feature flags, branding, labels, theme, placement, compliance, and approved prompt settings.
|
|
83
83
|
|
|
84
84
|
Secret API keys stay in CLI, server-side, or trusted setup workflows. The browser never receives model credentials or Nexalink API keys.
|
|
85
85
|
|
|
@@ -97,6 +97,7 @@ The core manages:
|
|
|
97
97
|
- loading, error, timeout, and retry state
|
|
98
98
|
- context injection
|
|
99
99
|
- subscriptions for custom renderers
|
|
100
|
+
- PHI preflight, safety callbacks, and metadata-only audit trails
|
|
100
101
|
- endpoint calls and telemetry integration
|
|
101
102
|
|
|
102
103
|
```ts
|
|
@@ -172,7 +173,174 @@ Custom message rendering:
|
|
|
172
173
|
/>
|
|
173
174
|
```
|
|
174
175
|
|
|
175
|
-
###
|
|
176
|
+
### RCM coding-assistance pilot workflow
|
|
177
|
+
|
|
178
|
+
The RCM kit is a narrow, structured coding-review workflow built on the same
|
|
179
|
+
headless core, compliance preflight, and metadata-only audit events. It returns
|
|
180
|
+
candidate codes for qualified human review; it never makes a final coding,
|
|
181
|
+
billing, claim-submission, diagnosis, or treatment decision.
|
|
182
|
+
|
|
183
|
+
The SDK sends `workflow: "rcm-coding-assistance"` as an explicit top-level ask
|
|
184
|
+
field. The hosted runtime authorizes the workflow from the issued embed key and
|
|
185
|
+
server-side tenant entitlement. Values in user context do not authorize a
|
|
186
|
+
workflow.
|
|
187
|
+
|
|
188
|
+
For the hosted RCM path, configure the tenant's RCM policy corpus in the
|
|
189
|
+
CareCompass tenant portal before embedding: an authorized administrator must
|
|
190
|
+
have an executed BAA, enable the RCM entitlement, set up the corpus, upload a
|
|
191
|
+
customer-owned policy version, explicitly approve it, and wait for ingestion to
|
|
192
|
+
become active. The browser sends structured encounter context only; it never
|
|
193
|
+
sends a corpus ID, policy text, or policy references.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
import {
|
|
197
|
+
CARECOMPASS_RCM_SYNTHETIC_DEMO,
|
|
198
|
+
RcmCodingAssistant,
|
|
199
|
+
} from "@nexalink/carecompass-sdk/rcm";
|
|
200
|
+
|
|
201
|
+
<RcmCodingAssistant
|
|
202
|
+
config={config}
|
|
203
|
+
initialInput={CARECOMPASS_RCM_SYNTHETIC_DEMO}
|
|
204
|
+
workflowOptions={{
|
|
205
|
+
customerRules: { requirePolicySupport: true },
|
|
206
|
+
retrieval: "hosted",
|
|
207
|
+
}}
|
|
208
|
+
onAuditEvent={(event) => hostAudit(event)}
|
|
209
|
+
onSafetyEvent={(event) => hostSafetyMonitor(event)}
|
|
210
|
+
/>;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The included demo input is synthetic and contains no patient data. Use it only
|
|
214
|
+
with synthetic, approved policy material during development. The package
|
|
215
|
+
includes [`DEMO-ENC-1042-rcm-policy.md`](qa-fixtures/DEMO-ENC-1042-rcm-policy.md)
|
|
216
|
+
for the matching demo. Upload that file through **RCM policy corpus**, give it
|
|
217
|
+
the `2026.1-demo` version, explicitly approve it, and wait for ingestion before
|
|
218
|
+
submitting `CARECOMPASS_RCM_SYNTHETIC_DEMO`. It is deliberately not a real payer
|
|
219
|
+
policy or coding manual.
|
|
220
|
+
|
|
221
|
+
A production tenant must have an executed BAA, an RCM entitlement, and a
|
|
222
|
+
healthy active corpus; otherwise the hosted workflow fails closed with a
|
|
223
|
+
structured human-review escalation.
|
|
224
|
+
|
|
225
|
+
In hosted mode, the result's `policyReferences` are the sanitized citations
|
|
226
|
+
resolved by the runtime. Render those citations alongside a coder-review
|
|
227
|
+
suggestion. A `review_required` result requires policy support; empty retrieval,
|
|
228
|
+
invalid citations, guardrail intervention, or invalid JSON becomes an
|
|
229
|
+
`abstain`/`escalate` result with no recommendations. Hosts receive the normal
|
|
230
|
+
metadata-only audit and safety events, including authorization, retrieval,
|
|
231
|
+
validation, and escalation outcomes.
|
|
232
|
+
|
|
233
|
+
For a customer-managed trusted retrieval boundary, pass an `RcmPolicyRetriever`
|
|
234
|
+
instead of `"hosted"`. Its `approvedCorpusIds` allowlist is enforced by the SDK;
|
|
235
|
+
unapproved or missing references produce a human-review escalation.
|
|
236
|
+
|
|
237
|
+
To evaluate the pilot, compare **manual research minutes per case** before and
|
|
238
|
+
after assisted, human-reviewed use. The target is to measure a 25% or greater
|
|
239
|
+
reduction, not to make or submit a coding decision.
|
|
240
|
+
|
|
241
|
+
### 3. React Hook
|
|
242
|
+
|
|
243
|
+
Use `useCareCompass` when your React app needs its own visual design but does
|
|
244
|
+
not want to manage subscriptions, retries, or teardown by hand.
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
import { useCareCompass } from "@nexalink/carecompass-sdk/react";
|
|
248
|
+
import config from "./carecompass/carecompass.config.json";
|
|
249
|
+
|
|
250
|
+
export function CareCompassPanel() {
|
|
251
|
+
const { snapshot, sendMessage, retryLast } = useCareCompass(config, {
|
|
252
|
+
initialContext: { workflow: "member-portal" },
|
|
253
|
+
onSafetyEvent: (event) => monitorSafety(event),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<section>
|
|
258
|
+
{snapshot.messages.map((message) => (
|
|
259
|
+
<p key={message.id}>{message.text}</p>
|
|
260
|
+
))}
|
|
261
|
+
<button
|
|
262
|
+
disabled={snapshot.loading}
|
|
263
|
+
onClick={() => void sendMessage("Find an in-network doctor")}
|
|
264
|
+
>
|
|
265
|
+
Find care
|
|
266
|
+
</button>
|
|
267
|
+
{snapshot.canRetry && (
|
|
268
|
+
<button onClick={() => void retryLast()}>Retry</button>
|
|
269
|
+
)}
|
|
270
|
+
</section>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The hook returns the underlying `core` for audit export, safety/audit
|
|
276
|
+
subscriptions, tool/widget events, and the lifecycle `streamMessage` API.
|
|
277
|
+
|
|
278
|
+
### 4. Web Component (Vue-compatible)
|
|
279
|
+
|
|
280
|
+
The Web Component is rendered directly from the headless core and exposes a
|
|
281
|
+
standard custom-element property, making it a good fit for Vue, Angular, CMS
|
|
282
|
+
shells, and mixed-framework pages without requiring a React runtime. Register
|
|
283
|
+
it only in the browser.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import { defineCareCompassElement } from "@nexalink/carecompass-sdk/web-component";
|
|
287
|
+
|
|
288
|
+
defineCareCompassElement();
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```vue
|
|
292
|
+
<script setup lang="ts">
|
|
293
|
+
import { onMounted, ref } from "vue";
|
|
294
|
+
import { defineCareCompassElement } from "@nexalink/carecompass-sdk/web-component";
|
|
295
|
+
import config from "./carecompass/carecompass.config.json";
|
|
296
|
+
|
|
297
|
+
const chat = ref<HTMLElement & { config?: typeof config }>();
|
|
298
|
+
onMounted(() => {
|
|
299
|
+
defineCareCompassElement();
|
|
300
|
+
if (chat.value) chat.value.config = config;
|
|
301
|
+
});
|
|
302
|
+
</script>
|
|
303
|
+
|
|
304
|
+
<template><carecompass-chat ref="chat" /></template>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
For static hosts, the element also accepts a JSON `config` attribute. Prefer the
|
|
308
|
+
property assignment above in application code: it avoids HTML escaping and keeps
|
|
309
|
+
the configuration in your typed build pipeline.
|
|
310
|
+
|
|
311
|
+
### 5. Script-tag Quickstart
|
|
312
|
+
|
|
313
|
+
The existing `mountCareCompass` path remains supported for quick static pages
|
|
314
|
+
and CMS integrations. Generate a script snippet from the same config:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { generateCareCompassSnippet } from "@nexalink/carecompass-sdk";
|
|
318
|
+
|
|
319
|
+
const html = generateCareCompassSnippet(config, { mode: "cdn" });
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The generated module calls `mountCareCompass` and does not require a React app.
|
|
323
|
+
|
|
324
|
+
### 6. Iframe Fallback
|
|
325
|
+
|
|
326
|
+
Use an iframe when the host page needs strong CSS/JavaScript isolation or cannot
|
|
327
|
+
run a module script. The CLI `init` flow generates an iframe page, or you can
|
|
328
|
+
create the host markup with `generateCareCompassIframeSnippet(config)`.
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import { generateCareCompassIframeSnippet } from "@nexalink/carecompass-sdk";
|
|
332
|
+
|
|
333
|
+
const html = generateCareCompassIframeSnippet(config, {
|
|
334
|
+
src: "/carecompass/carecompass-embed.html",
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
An iframe isolates page styles and scripts, but it adds a separate document for
|
|
339
|
+
focus management, analytics, responsive sizing, and host-to-widget context.
|
|
340
|
+
Choose the React hook or Web Component when the assistant needs rich in-page
|
|
341
|
+
coordination; choose the iframe for constrained or high-isolation environments.
|
|
342
|
+
|
|
343
|
+
### 7. Existing Embed Compatibility
|
|
176
344
|
|
|
177
345
|
Existing embed integrations can keep using the root package exports.
|
|
178
346
|
|
|
@@ -192,6 +360,40 @@ import config from "./carecompass/carecompass.config.json";
|
|
|
192
360
|
mountCareCompass(document.getElementById("carecompass-root"), config);
|
|
193
361
|
```
|
|
194
362
|
|
|
363
|
+
### Production runtime tokens
|
|
364
|
+
|
|
365
|
+
Production embeds require a fresh runtime token from an integrator-controlled
|
|
366
|
+
backend-for-frontend (BFF). Do not place a Nexalink API key, runtime-token
|
|
367
|
+
secret, or a reusable token in `carecompass.config.json` or browser code.
|
|
368
|
+
|
|
369
|
+
Provide `runtimeTokenProvider`; the SDK calls it before validation, every
|
|
370
|
+
inference request, and every status poll. The provider should call your BFF,
|
|
371
|
+
which holds the Nexalink API key and obtains a short-lived token for the
|
|
372
|
+
current browser origin and workflow.
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
const runtimeTokenProvider = async ({ workflow }: { workflow?: string }) => {
|
|
376
|
+
const response = await fetch("/api/carecompass/runtime-token", {
|
|
377
|
+
method: "POST",
|
|
378
|
+
credentials: "same-origin",
|
|
379
|
+
headers: { "content-type": "application/json" },
|
|
380
|
+
body: JSON.stringify({ workflow }),
|
|
381
|
+
});
|
|
382
|
+
if (!response.ok) throw new Error("Could not authorize CareCompass.");
|
|
383
|
+
return ((await response.json()) as { token: string }).token;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
<CareCompassEmbed
|
|
387
|
+
config={config}
|
|
388
|
+
runtimeTokenProvider={runtimeTokenProvider}
|
|
389
|
+
/>;
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
For static hosts, pass the provider as the third argument to
|
|
393
|
+
`mountCareCompass`. For Web Components, assign it to the
|
|
394
|
+
`runtimeTokenProvider` property before setting `config`. Production requests
|
|
395
|
+
fail closed when the provider is absent, fails, or returns an empty token.
|
|
396
|
+
|
|
195
397
|
## Quick Start
|
|
196
398
|
|
|
197
399
|
Live regulated use requires Nexalink onboarding, issued credentials, and BAA coverage where PHI may be involved. The package is publicly installable for developer convenience, but runtime access is credentialed.
|
|
@@ -249,6 +451,17 @@ npx @nexalink/carecompass-sdk doctor --config carecompass/carecompass.config.jso
|
|
|
249
451
|
"welcomeMessage": "How can I help?",
|
|
250
452
|
"allowUserContext": false
|
|
251
453
|
},
|
|
454
|
+
"compliance": {
|
|
455
|
+
"preset": "healthcare"
|
|
456
|
+
},
|
|
457
|
+
"model": {
|
|
458
|
+
"id": "care-navigation-v2",
|
|
459
|
+
"version": "2026-07"
|
|
460
|
+
},
|
|
461
|
+
"featureFlags": {
|
|
462
|
+
"care-plan-summary": true,
|
|
463
|
+
"provider-search": true
|
|
464
|
+
},
|
|
252
465
|
"placement": {
|
|
253
466
|
"mode": "inline"
|
|
254
467
|
},
|
|
@@ -256,6 +469,72 @@ npx @nexalink/carecompass-sdk doctor --config carecompass/carecompass.config.jso
|
|
|
256
469
|
}
|
|
257
470
|
```
|
|
258
471
|
|
|
472
|
+
## Compliance Controls And Audit Trails
|
|
473
|
+
|
|
474
|
+
Compliance controls are local SDK preflight checks, not a substitute for Nexalink runtime guardrails, a BAA, or the host application's own HIPAA program. They detect common direct identifiers (email addresses, US phone numbers, SSNs, DOBs, member/policy IDs, and MRNs) in messages, responses, and string-valued context before the SDK stores or sends them.
|
|
475
|
+
|
|
476
|
+
Choose a preset in public SDK configuration, then override only the behavior you need:
|
|
477
|
+
|
|
478
|
+
| Preset | PHI behavior | Audit retention | Tradeoff |
|
|
479
|
+
| ------------ | --------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
480
|
+
| `standard` | Detects and emits a `policy_sensitive` safety callback; does not modify requests. | Off | Preserves existing SDK behavior; the host must opt into audit retention. |
|
|
481
|
+
| `healthcare` | Replaces detected values with `[REDACTED]` before requests and UI state. | On | Recommended default for PHI-oriented browser integrations; local detection cannot identify every form of PHI. |
|
|
482
|
+
| `strict` | Blocks a request or response with detectable PHI. | On | Stronger browser-side control, but legitimate identifier-bearing workflows need a host-approved alternative. |
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
import { createCareCompassCore } from "@nexalink/carecompass-sdk/core";
|
|
486
|
+
|
|
487
|
+
const core = createCareCompassCore({
|
|
488
|
+
...config,
|
|
489
|
+
compliance: {
|
|
490
|
+
preset: "healthcare",
|
|
491
|
+
phi: { replacement: "[PHI REMOVED]" },
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const unsubscribeAudit = core.subscribeAudit((event) => {
|
|
496
|
+
// Persist to your audit system. Events contain type, outcome, timestamp,
|
|
497
|
+
// source, PHI category/count, and a generated ID—never message text,
|
|
498
|
+
// context values, identifiers, or raw errors.
|
|
499
|
+
persistAuditEvent(event);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const unsubscribeSafety = core.subscribeSafety((event) => {
|
|
503
|
+
// `blocked`, `redacted`, `escalated`, or `policy_sensitive` with only
|
|
504
|
+
// source plus PHI category/count for safe monitoring.
|
|
505
|
+
monitorSafetyEvent(event);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Export a copy of the in-memory, metadata-only audit trail on demand.
|
|
509
|
+
const auditTrail = core.exportAuditTrail();
|
|
510
|
+
|
|
511
|
+
unsubscribeAudit();
|
|
512
|
+
unsubscribeSafety();
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
For host-owned tools and UI actions that happen outside the core request lifecycle, add their metadata-only events explicitly:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
core.recordToolCall("completed");
|
|
519
|
+
core.recordWidgetAction();
|
|
520
|
+
core.recordSafetyDecision("escalated");
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
If your organization has an approved detector or redaction format, pass it through `createCareCompassCore` options. These hooks receive the raw value only inside your host application; the SDK still emits only PHI categories and counts in audit and safety events.
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
const core = createCareCompassCore(config, {
|
|
527
|
+
compliance: {
|
|
528
|
+
phi: {
|
|
529
|
+
detect: (value) => approvedPhiDetector(value),
|
|
530
|
+
redact: (value, matches) => approvedRedactor(value, matches),
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The React UI kit accepts `onAuditEvent` and `onSafetyEvent` with the same safe payloads. Its submit and retry actions are automatically represented as widget audit events when auditing is enabled.
|
|
537
|
+
|
|
259
538
|
## Credential Model
|
|
260
539
|
|
|
261
540
|
CareCompass uses two key types:
|
|
@@ -316,7 +595,7 @@ The SDK is designed to keep sensitive operational details out of the browser:
|
|
|
316
595
|
- no secret API keys in frontend config
|
|
317
596
|
- no model endpoint credentials in browser code
|
|
318
597
|
- no customer-managed telemetry switches in runtime config
|
|
319
|
-
- no prompt, response, chat history, user context, PHI/PII, credentials, raw errors, stack traces, CLI arguments, terminal input, or file paths in SDK telemetry
|
|
598
|
+
- no prompt, response, chat history, user context, PHI/PII, payer, tenant, deployment, session, or user identifiers, credentials, raw errors, stack traces, CLI arguments, terminal input, or file paths in SDK telemetry
|
|
320
599
|
- generated snippets escape serialized config
|
|
321
600
|
- default React rendering escapes raw HTML
|
|
322
601
|
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/client.d.ts
CHANGED
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
import { type CareCompassConfig } from "./schema.js";
|
|
2
2
|
export type CareCompassAskRequest = {
|
|
3
3
|
prompt: string;
|
|
4
|
+
/** Stable client-generated id used to make a submit/retry idempotent. */
|
|
5
|
+
requestId?: string;
|
|
6
|
+
workflow?: CareCompassWorkflowId;
|
|
4
7
|
sessionId?: string;
|
|
5
8
|
userContext?: Record<string, unknown>;
|
|
6
9
|
};
|
|
10
|
+
export type CareCompassWorkflowId = "rcm-coding-assistance";
|
|
7
11
|
export type CareCompassAskResponse = {
|
|
8
12
|
jobId: string;
|
|
9
13
|
};
|
|
14
|
+
export type CareCompassRetrievalReference = {
|
|
15
|
+
referenceId: string;
|
|
16
|
+
documentId: string;
|
|
17
|
+
title: string;
|
|
18
|
+
version: string;
|
|
19
|
+
excerpt: string;
|
|
20
|
+
};
|
|
21
|
+
export type CareCompassRetrievalMetadata = {
|
|
22
|
+
corpusId: string;
|
|
23
|
+
corpusVersion: string;
|
|
24
|
+
outcome: string;
|
|
25
|
+
latencyMs: number;
|
|
26
|
+
references: CareCompassRetrievalReference[];
|
|
27
|
+
};
|
|
10
28
|
export type CareCompassStatusResponse = {
|
|
11
29
|
status: "queued" | "running" | "complete" | "failed";
|
|
12
30
|
result?: string;
|
|
31
|
+
retrieval?: CareCompassRetrievalMetadata;
|
|
13
32
|
};
|
|
14
33
|
type Fetcher = typeof fetch;
|
|
15
|
-
export
|
|
34
|
+
export type CareCompassRuntimeTokenRequest = {
|
|
35
|
+
payerId: string;
|
|
36
|
+
environment: CareCompassConfig["environment"];
|
|
37
|
+
embedKey: string;
|
|
38
|
+
workflow?: CareCompassWorkflowId;
|
|
39
|
+
jobId?: string;
|
|
40
|
+
};
|
|
41
|
+
/** Obtains a short-lived production token from the integrator's BFF. */
|
|
42
|
+
export type CareCompassRuntimeTokenProvider = (request: CareCompassRuntimeTokenRequest) => Promise<string> | string;
|
|
43
|
+
export type CareCompassClientOptions = {
|
|
16
44
|
fetcher?: Fetcher;
|
|
17
45
|
now?: () => number;
|
|
18
|
-
|
|
46
|
+
runtimeTokenProvider?: CareCompassRuntimeTokenProvider;
|
|
47
|
+
};
|
|
48
|
+
export declare function createCareCompassClient(inputConfig: CareCompassConfig, options?: CareCompassClientOptions): {
|
|
19
49
|
validateConfig: () => Promise<{
|
|
20
50
|
ok: true;
|
|
21
51
|
}>;
|
package/dist/client.js
CHANGED
|
@@ -105,6 +105,30 @@ export function createCareCompassClient(inputConfig, options = {}) {
|
|
|
105
105
|
"x-carecompass-environment": config.environment,
|
|
106
106
|
};
|
|
107
107
|
const jobStartedAt = new Map();
|
|
108
|
+
const jobWorkflows = new Map();
|
|
109
|
+
async function requestHeaders(request = {}) {
|
|
110
|
+
if (config.environment !== "production")
|
|
111
|
+
return headers;
|
|
112
|
+
if (!options.runtimeTokenProvider) {
|
|
113
|
+
throw new CareCompassClientError("Production CareCompass embeds require a runtimeTokenProvider backed by your server.", CARECOMPASS_TELEMETRY_ERROR_CODES.AUTHENTICATION_FAILED, "authentication");
|
|
114
|
+
}
|
|
115
|
+
let token;
|
|
116
|
+
try {
|
|
117
|
+
token = await options.runtimeTokenProvider({
|
|
118
|
+
payerId: config.payerId,
|
|
119
|
+
environment: config.environment,
|
|
120
|
+
embedKey: config.embedKey,
|
|
121
|
+
...request,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new CareCompassClientError("CareCompass runtime authorization could not be obtained.", CARECOMPASS_TELEMETRY_ERROR_CODES.AUTHENTICATION_FAILED, "authentication");
|
|
126
|
+
}
|
|
127
|
+
if (typeof token !== "string" || !token.trim()) {
|
|
128
|
+
throw new CareCompassClientError("CareCompass runtime authorization could not be obtained.", CARECOMPASS_TELEMETRY_ERROR_CODES.AUTHENTICATION_FAILED, "authentication");
|
|
129
|
+
}
|
|
130
|
+
return { ...headers, "x-carecompass-runtime-token": token.trim() };
|
|
131
|
+
}
|
|
108
132
|
telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.SDK_INITIALIZED);
|
|
109
133
|
telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.THEME_LOADED);
|
|
110
134
|
telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.PROMPT_CONFIGURATION_LOADED);
|
|
@@ -137,7 +161,7 @@ export function createCareCompassClient(inputConfig, options = {}) {
|
|
|
137
161
|
try {
|
|
138
162
|
const response = await send(joinUrl(config.apiBaseUrl, "/api/carecompass/embed/config/validate"), {
|
|
139
163
|
method: "POST",
|
|
140
|
-
headers,
|
|
164
|
+
headers: await requestHeaders(),
|
|
141
165
|
body: JSON.stringify({ config }),
|
|
142
166
|
});
|
|
143
167
|
const result = await parseJson(response);
|
|
@@ -155,18 +179,25 @@ export function createCareCompassClient(inputConfig, options = {}) {
|
|
|
155
179
|
try {
|
|
156
180
|
const response = await send(joinUrl(config.apiBaseUrl, "/api/carecompass/embed/ask"), {
|
|
157
181
|
method: "POST",
|
|
158
|
-
headers,
|
|
182
|
+
headers: await requestHeaders({ workflow: request.workflow }),
|
|
159
183
|
body: JSON.stringify({
|
|
160
184
|
prompt: request.prompt,
|
|
185
|
+
requestId: request.requestId,
|
|
186
|
+
workflow: request.workflow,
|
|
161
187
|
sessionId: request.sessionId,
|
|
162
188
|
userContext: request.userContext,
|
|
163
189
|
promptConfig: config.prompt,
|
|
190
|
+
// Public routing preferences; the hosted runtime validates model
|
|
191
|
+
// availability and may ignore disabled feature flags.
|
|
192
|
+
model: config.model,
|
|
193
|
+
featureFlags: config.featureFlags,
|
|
164
194
|
}),
|
|
165
195
|
});
|
|
166
196
|
const result = await parseJson(response);
|
|
167
197
|
// Status polling is asynchronous, so retain the submit timestamp until
|
|
168
198
|
// the final job state lets telemetry report end-to-end latency.
|
|
169
199
|
jobStartedAt.set(result.jobId, startedAt);
|
|
200
|
+
jobWorkflows.set(result.jobId, request.workflow);
|
|
170
201
|
return result;
|
|
171
202
|
}
|
|
172
203
|
catch (error) {
|
|
@@ -177,16 +208,23 @@ export function createCareCompassClient(inputConfig, options = {}) {
|
|
|
177
208
|
status: async (jobId) => {
|
|
178
209
|
const startedAt = jobStartedAt.get(jobId) ?? now();
|
|
179
210
|
try {
|
|
180
|
-
const response = await send(joinUrl(config.apiBaseUrl, `/api/carecompass/embed/status/${encodeURIComponent(jobId)}`), {
|
|
211
|
+
const response = await send(joinUrl(config.apiBaseUrl, `/api/carecompass/embed/status/${encodeURIComponent(jobId)}`), {
|
|
212
|
+
headers: await requestHeaders({
|
|
213
|
+
jobId,
|
|
214
|
+
workflow: jobWorkflows.get(jobId),
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
181
217
|
const result = await parseJson(response);
|
|
182
218
|
if (result.status === "complete") {
|
|
183
219
|
jobStartedAt.delete(jobId);
|
|
220
|
+
jobWorkflows.delete(jobId);
|
|
184
221
|
telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.INFERENCE_SUCCEEDED, {
|
|
185
222
|
durationMs: now() - startedAt,
|
|
186
223
|
});
|
|
187
224
|
}
|
|
188
225
|
else if (result.status === "failed") {
|
|
189
226
|
jobStartedAt.delete(jobId);
|
|
227
|
+
jobWorkflows.delete(jobId);
|
|
190
228
|
telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.INFERENCE_FAILED, {
|
|
191
229
|
durationMs: now() - startedAt,
|
|
192
230
|
errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.INFERENCE_FAILED,
|
|
@@ -197,6 +235,7 @@ export function createCareCompassClient(inputConfig, options = {}) {
|
|
|
197
235
|
}
|
|
198
236
|
catch (error) {
|
|
199
237
|
jobStartedAt.delete(jobId);
|
|
238
|
+
jobWorkflows.delete(jobId);
|
|
200
239
|
captureInferenceFailure(telemetry, error, now() - startedAt);
|
|
201
240
|
throw error;
|
|
202
241
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CareCompassComplianceConfig } from "./schema.js";
|
|
2
|
+
/** The identifier categories detected by the SDK's local PHI preflight. */
|
|
3
|
+
export type CareCompassPhiCategory = "date_of_birth" | "email" | "medical_record_number" | "member_id" | "phone" | "ssn";
|
|
4
|
+
export type CareCompassPhiMatch = {
|
|
5
|
+
category: CareCompassPhiCategory;
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
};
|
|
9
|
+
export type CareCompassPhiSource = "message" | "response" | "context";
|
|
10
|
+
export type CareCompassPhiDetectionContext = {
|
|
11
|
+
source: CareCompassPhiSource;
|
|
12
|
+
};
|
|
13
|
+
export type CareCompassPhiDetector = (value: string, context: CareCompassPhiDetectionContext) => CareCompassPhiMatch[];
|
|
14
|
+
export type CareCompassPhiRedactor = (value: string, matches: CareCompassPhiMatch[], replacement: string) => string;
|
|
15
|
+
export type CareCompassPhiHooks = {
|
|
16
|
+
/** Replaces the built-in detector. The hook receives raw content; never emit it. */
|
|
17
|
+
detect?: CareCompassPhiDetector;
|
|
18
|
+
/** Replaces the built-in redactor. The hook receives raw content; never emit it. */
|
|
19
|
+
redact?: CareCompassPhiRedactor;
|
|
20
|
+
};
|
|
21
|
+
export type CareCompassComplianceRuntimeOptions = {
|
|
22
|
+
phi?: CareCompassPhiHooks;
|
|
23
|
+
};
|
|
24
|
+
export type CareCompassCompliancePreset = "standard" | "healthcare" | "strict";
|
|
25
|
+
export type CareCompassPhiAction = "allow" | "redact" | "block";
|
|
26
|
+
export type CareCompassCompliancePolicy = {
|
|
27
|
+
auditEnabled: boolean;
|
|
28
|
+
phiAction: CareCompassPhiAction;
|
|
29
|
+
preset: CareCompassCompliancePreset;
|
|
30
|
+
replacement: string;
|
|
31
|
+
};
|
|
32
|
+
export declare const CARECOMPASS_COMPLIANCE_PRESETS: Readonly<Record<CareCompassCompliancePreset, Omit<CareCompassCompliancePolicy, "preset">>>;
|
|
33
|
+
export type CareCompassAuditEventType = "message" | "tool_call" | "widget_action" | "error" | "safety_decision";
|
|
34
|
+
export type CareCompassAuditOutcome = "submitted" | "completed" | "failed" | "blocked" | "redacted" | "escalated" | "recorded";
|
|
35
|
+
export type CareCompassAuditSource = CareCompassPhiSource | "tool_call" | "widget_action";
|
|
36
|
+
export type CareCompassAuditEvent = {
|
|
37
|
+
id: string;
|
|
38
|
+
occurredAt: number;
|
|
39
|
+
outcome: CareCompassAuditOutcome;
|
|
40
|
+
phi: {
|
|
41
|
+
categories: CareCompassPhiCategory[];
|
|
42
|
+
count: number;
|
|
43
|
+
};
|
|
44
|
+
source?: CareCompassAuditSource;
|
|
45
|
+
type: CareCompassAuditEventType;
|
|
46
|
+
};
|
|
47
|
+
export type CareCompassSafetyDecision = "blocked" | "escalated" | "redacted" | "policy_sensitive";
|
|
48
|
+
export type CareCompassSafetyEvent = {
|
|
49
|
+
decision: CareCompassSafetyDecision;
|
|
50
|
+
occurredAt: number;
|
|
51
|
+
phi: {
|
|
52
|
+
categories: CareCompassPhiCategory[];
|
|
53
|
+
count: number;
|
|
54
|
+
};
|
|
55
|
+
source: CareCompassPhiSource | "tool_call" | "widget_action";
|
|
56
|
+
};
|
|
57
|
+
export type CareCompassAuditListener = (event: CareCompassAuditEvent) => void;
|
|
58
|
+
export type CareCompassSafetyListener = (event: CareCompassSafetyEvent) => void;
|
|
59
|
+
export declare const detectCareCompassPhi: CareCompassPhiDetector;
|
|
60
|
+
export declare const redactCareCompassPhi: CareCompassPhiRedactor;
|
|
61
|
+
export declare const resolveCareCompassCompliancePolicy: (config: CareCompassComplianceConfig | undefined) => CareCompassCompliancePolicy;
|
|
62
|
+
export declare const phiSummary: (matches: CareCompassPhiMatch[]) => {
|
|
63
|
+
categories: CareCompassPhiCategory[];
|
|
64
|
+
count: number;
|
|
65
|
+
};
|
|
66
|
+
export declare const sanitizeCareCompassValue: (value: unknown, source: CareCompassPhiSource, policy: CareCompassCompliancePolicy, hooks: CareCompassPhiHooks | undefined) => {
|
|
67
|
+
matches: CareCompassPhiMatch[];
|
|
68
|
+
value: unknown;
|
|
69
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const CARECOMPASS_COMPLIANCE_PRESETS = {
|
|
2
|
+
// Standard is intentionally non-mutating for existing integrations. Detection
|
|
3
|
+
// can still drive a safety callback when the host opts into one.
|
|
4
|
+
standard: {
|
|
5
|
+
auditEnabled: false,
|
|
6
|
+
phiAction: "allow",
|
|
7
|
+
replacement: "[REDACTED]",
|
|
8
|
+
},
|
|
9
|
+
// Healthcare protects browser-visible requests and retains metadata-only audit
|
|
10
|
+
// records. It is the recommended starting point for PHI-oriented deployments.
|
|
11
|
+
healthcare: {
|
|
12
|
+
auditEnabled: true,
|
|
13
|
+
phiAction: "redact",
|
|
14
|
+
replacement: "[REDACTED]",
|
|
15
|
+
},
|
|
16
|
+
// Strict is appropriate when the host must stop detectable PHI before it leaves
|
|
17
|
+
// the browser. It may reject legitimate identifier-bearing workflow requests.
|
|
18
|
+
strict: {
|
|
19
|
+
auditEnabled: true,
|
|
20
|
+
phiAction: "block",
|
|
21
|
+
replacement: "[REDACTED]",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const PHI_PATTERNS = [
|
|
25
|
+
{ category: "email", pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
|
|
26
|
+
{ category: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
27
|
+
{
|
|
28
|
+
category: "phone",
|
|
29
|
+
pattern: /(?:\+1[ .-]?)?(?:\(?\d{3}\)?[ .-]?)\d{3}[ .-]\d{4}\b/g,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
category: "date_of_birth",
|
|
33
|
+
pattern: /\b(?:dob|date of birth|birth date)\s*[:#-]?\s*(?:\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2})\b/gi,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
category: "medical_record_number",
|
|
37
|
+
pattern: /\b(?:mrn|medical record(?: number)?)\s*[:#-]?\s*[A-Z0-9-]{3,}\b/gi,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
category: "member_id",
|
|
41
|
+
pattern: /\b(?:member|patient|policy|subscriber)(?:\s+(?:id|number))?\s*[:#-]?\s*[A-Z0-9-]{3,}\b/gi,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
const PHI_CATEGORIES = new Set([
|
|
45
|
+
"date_of_birth",
|
|
46
|
+
"email",
|
|
47
|
+
"medical_record_number",
|
|
48
|
+
"member_id",
|
|
49
|
+
"phone",
|
|
50
|
+
"ssn",
|
|
51
|
+
]);
|
|
52
|
+
const validMatches = (value, matches) => matches
|
|
53
|
+
.filter((match) => PHI_CATEGORIES.has(match.category) &&
|
|
54
|
+
Number.isInteger(match.start) &&
|
|
55
|
+
Number.isInteger(match.end) &&
|
|
56
|
+
match.start >= 0 &&
|
|
57
|
+
match.end > match.start &&
|
|
58
|
+
match.end <= value.length)
|
|
59
|
+
.sort((a, b) => a.start - b.start || b.end - a.end)
|
|
60
|
+
.filter((match, index, sorted) => index === 0 || match.start >= sorted[index - 1].end);
|
|
61
|
+
export const detectCareCompassPhi = (value) => {
|
|
62
|
+
const matches = [];
|
|
63
|
+
for (const { category, pattern } of PHI_PATTERNS) {
|
|
64
|
+
pattern.lastIndex = 0;
|
|
65
|
+
for (const match of value.matchAll(pattern)) {
|
|
66
|
+
const start = match.index;
|
|
67
|
+
if (start === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
matches.push({ category, start, end: start + match[0].length });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return validMatches(value, matches);
|
|
73
|
+
};
|
|
74
|
+
export const redactCareCompassPhi = (value, matches, replacement) => {
|
|
75
|
+
let redacted = value;
|
|
76
|
+
for (const match of [...matches].sort((a, b) => b.start - a.start)) {
|
|
77
|
+
redacted = `${redacted.slice(0, match.start)}${replacement}${redacted.slice(match.end)}`;
|
|
78
|
+
}
|
|
79
|
+
return redacted;
|
|
80
|
+
};
|
|
81
|
+
export const resolveCareCompassCompliancePolicy = (config) => {
|
|
82
|
+
const preset = config?.preset ?? "standard";
|
|
83
|
+
const defaults = CARECOMPASS_COMPLIANCE_PRESETS[preset];
|
|
84
|
+
return {
|
|
85
|
+
preset,
|
|
86
|
+
auditEnabled: config?.audit?.enabled ?? defaults.auditEnabled,
|
|
87
|
+
phiAction: config?.phi?.action ?? defaults.phiAction,
|
|
88
|
+
replacement: config?.phi?.replacement ?? defaults.replacement,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export const phiSummary = (matches) => ({
|
|
92
|
+
categories: [...new Set(matches.map((match) => match.category))].sort(),
|
|
93
|
+
count: matches.length,
|
|
94
|
+
});
|
|
95
|
+
export const sanitizeCareCompassValue = (value, source, policy, hooks) => {
|
|
96
|
+
if (typeof value === "string") {
|
|
97
|
+
const matches = validMatches(value, (hooks?.detect ?? detectCareCompassPhi)(value, { source }));
|
|
98
|
+
if (policy.phiAction !== "redact" || matches.length === 0) {
|
|
99
|
+
return { matches, value };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
matches,
|
|
103
|
+
value: (hooks?.redact ?? redactCareCompassPhi)(value, matches, policy.replacement),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
const entries = value.map((entry) => sanitizeCareCompassValue(entry, source, policy, hooks));
|
|
108
|
+
return {
|
|
109
|
+
matches: entries.flatMap((entry) => entry.matches),
|
|
110
|
+
value: entries.map((entry) => entry.value),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (value && typeof value === "object") {
|
|
114
|
+
const entries = Object.entries(value).map(([key, entry]) => [key, sanitizeCareCompassValue(entry, source, policy, hooks)]);
|
|
115
|
+
return {
|
|
116
|
+
matches: entries.flatMap(([, entry]) => entry.matches),
|
|
117
|
+
value: Object.fromEntries(entries.map(([key, entry]) => [key, entry.value])),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { matches: [], value };
|
|
121
|
+
};
|