@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 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
- ### 3. Existing Embed Compatibility
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 declare function createCareCompassClient(inputConfig: CareCompassConfig, options?: {
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)}`), { headers });
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
+ };