@nexalink/carecompass-sdk 0.3.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/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { randomUUID } from "node:crypto";
3
- import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { careCompassApiBaseUrlSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
@@ -32,13 +32,26 @@ planning, emergency triage, legal advice, and coverage guarantees. When a member
32
32
  needs clinical, emergency, plan-specific, or account-specific help that requires
33
33
  a human, route them to the customer's approved support resources.`;
34
34
  const CLI_COMMAND_ALLOWLIST = new Set([
35
+ "auth",
36
+ "completion",
37
+ "config",
38
+ "configure",
35
39
  "doctor",
40
+ "embed",
41
+ "health",
36
42
  "help",
37
43
  "init",
38
44
  "login",
45
+ "logout",
46
+ "query",
47
+ "setup",
39
48
  "snippet",
40
49
  "validate",
50
+ "version",
41
51
  ]);
52
+ const UNAUTHENTICATED_AUTH_ATTEMPT_LIMIT = 2;
53
+ const UNAUTHENTICATED_AUTH_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
54
+ const UNAUTHENTICATED_AUTH_RATE_LIMIT_FILE = "auth-rate-limit.json";
42
55
  class CliTelemetryError extends Error {
43
56
  constructor(message, telemetry) {
44
57
  super(message);
@@ -47,6 +60,8 @@ class CliTelemetryError extends Error {
47
60
  }
48
61
  const sanitizeCommandName = (command) => {
49
62
  const normalized = command === "--help" || command === "-h" ? "help" : command;
63
+ if (command === "--version" || command === "-v")
64
+ return "version";
50
65
  return CLI_COMMAND_ALLOWLIST.has(normalized) ? normalized : "unknown_command";
51
66
  };
52
67
  const authTelemetryForSource = (source) => {
@@ -58,6 +73,96 @@ const authTelemetryForSource = (source) => {
58
73
  }
59
74
  return { authMethod: "api_key", keySource: "cli_flag" };
60
75
  };
76
+ const unauthenticatedRateLimitPath = (io) => io.env.CARECOMPASS_AUTH_RATE_LIMIT_PATH ??
77
+ join(dirname(resolveCredentialsPath(io.env)), UNAUTHENTICATED_AUTH_RATE_LIMIT_FILE);
78
+ const unauthenticatedRateLimitKey = (io) => {
79
+ const keyMaterial = io.env.CARECOMPASS_AUTH_RATE_LIMIT_KEY
80
+ ? `explicit:${io.env.CARECOMPASS_AUTH_RATE_LIMIT_KEY}`
81
+ : io.env.CARECOMPASS_CLIENT_IP
82
+ ? `ip:${io.env.CARECOMPASS_CLIENT_IP}`
83
+ : `credentials:${resolveCredentialsPath(io.env)}`;
84
+ return createHash("sha256")
85
+ .update("carecompass unauthenticated auth rate limit")
86
+ .update(keyMaterial)
87
+ .digest("hex");
88
+ };
89
+ const emptyUnauthenticatedAuthLedger = () => ({
90
+ version: 1,
91
+ entries: {},
92
+ });
93
+ const readUnauthenticatedAuthLedger = (path) => {
94
+ try {
95
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
96
+ if (parsed.version !== 1 || typeof parsed.entries !== "object") {
97
+ return emptyUnauthenticatedAuthLedger();
98
+ }
99
+ const entries = {};
100
+ for (const [key, value] of Object.entries(parsed.entries)) {
101
+ if (typeof value === "object" &&
102
+ value !== null &&
103
+ typeof value.failures ===
104
+ "number" &&
105
+ typeof value.firstAttemptAt ===
106
+ "number" &&
107
+ typeof value.lastAttemptAt ===
108
+ "number") {
109
+ entries[key] = value;
110
+ }
111
+ }
112
+ return { version: 1, entries };
113
+ }
114
+ catch {
115
+ return emptyUnauthenticatedAuthLedger();
116
+ }
117
+ };
118
+ const writeUnauthenticatedAuthLedger = (path, ledger) => {
119
+ mkdirSync(dirname(path), { recursive: true });
120
+ writeFileSync(path, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
121
+ try {
122
+ chmodSync(path, 0o600);
123
+ }
124
+ catch {
125
+ // Some filesystems do not support chmod; the ledger contains only hashed
126
+ // limiter keys and counters.
127
+ }
128
+ };
129
+ const pruneUnauthenticatedAuthLedger = (ledger, now) => {
130
+ for (const [key, entry] of Object.entries(ledger.entries)) {
131
+ if (now - entry.firstAttemptAt >
132
+ UNAUTHENTICATED_AUTH_RATE_LIMIT_WINDOW_MS) {
133
+ delete ledger.entries[key];
134
+ }
135
+ }
136
+ };
137
+ const recordUnauthenticatedAuthFailure = (io) => {
138
+ const path = unauthenticatedRateLimitPath(io);
139
+ const key = unauthenticatedRateLimitKey(io);
140
+ const now = Date.now();
141
+ const ledger = readUnauthenticatedAuthLedger(path);
142
+ pruneUnauthenticatedAuthLedger(ledger, now);
143
+ const existing = ledger.entries[key];
144
+ const entry = existing &&
145
+ now - existing.firstAttemptAt <= UNAUTHENTICATED_AUTH_RATE_LIMIT_WINDOW_MS
146
+ ? existing
147
+ : { failures: 0, firstAttemptAt: now, lastAttemptAt: now };
148
+ entry.failures += 1;
149
+ entry.lastAttemptAt = now;
150
+ ledger.entries[key] = entry;
151
+ writeUnauthenticatedAuthLedger(path, ledger);
152
+ const failures = entry.failures;
153
+ if (failures < UNAUTHENTICATED_AUTH_ATTEMPT_LIMIT)
154
+ return null;
155
+ return new CliTelemetryError("Too many failed authentication attempts. Wait before trying again or verify the API key in the Nexalink console.", {
156
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.RATE_LIMIT_EXCEEDED,
157
+ failureCategory: "rate_limit_error",
158
+ });
159
+ };
160
+ const resetUnauthenticatedAuthFailures = (io) => {
161
+ const path = unauthenticatedRateLimitPath(io);
162
+ const ledger = readUnauthenticatedAuthLedger(path);
163
+ delete ledger.entries[unauthenticatedRateLimitKey(io)];
164
+ writeUnauthenticatedAuthLedger(path, ledger);
165
+ };
61
166
  const normalizeBaaStatus = (value) => {
62
167
  if (value === "signed" || value === "pending" || value === "not_required") {
63
168
  return value;
@@ -526,14 +631,11 @@ async function resolveInitApiKey(flags, io, apiBaseUrl, onAuthChecked = async ()
526
631
  failureCategory: validation.failureCategory,
527
632
  });
528
633
  io.stdout.write(`${validation.message}\n`);
634
+ const rateLimitError = recordUnauthenticatedAuthFailure(io);
635
+ if (rateLimitError)
636
+ throw rateLimitError;
529
637
  continue;
530
638
  }
531
- await onAuthChecked({
532
- authResult: "success",
533
- baaStatus: validation.baaStatus,
534
- keySource: authTelemetry.keySource,
535
- failureCategory: null,
536
- });
537
639
  if (validation.accountType !== accountType) {
538
640
  if (validation.accountType === "enterprise") {
539
641
  io.stdout.write(`This API key belongs to an enterprise account. ${enterpriseUnavailableMessage(urls.developer)}\n`);
@@ -543,12 +645,14 @@ async function resolveInitApiKey(flags, io, apiBaseUrl, onAuthChecked = async ()
543
645
  continue;
544
646
  }
545
647
  writeStoredApiKey(normalizedApiKey, credentialsPath);
648
+ resetUnauthenticatedAuthFailures(io);
546
649
  io.stdout.write(`Stored CareCompass credentials at ${credentialsPath}\n`);
547
650
  io.stdout.write("Sign-in successful.\n");
548
651
  return {
549
652
  apiKey: normalizedApiKey,
550
653
  authMethod: authTelemetry.authMethod,
551
654
  baaStatus: validation.baaStatus,
655
+ keySource: authTelemetry.keySource,
552
656
  setup: validation.setup,
553
657
  };
554
658
  }
@@ -790,12 +894,14 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
790
894
  const sessionStartedAt = Date.now();
791
895
  const rawCommand = argv[0] ?? "help";
792
896
  const telemetryCommandName = sanitizeCommandName(rawCommand);
793
- const commandStartedAt = Date.now();
897
+ let commandStartedAt = sessionStartedAt;
898
+ let commandStartedCaptured = false;
794
899
  let commandsAttempted = 0;
795
900
  let commandsSucceeded = 0;
796
901
  let commandsFailed = 0;
797
902
  let activeCommand = rawCommand;
798
903
  let authCheckedCaptured = false;
904
+ let initStartedCaptured = false;
799
905
  let sessionEndedCaptured = false;
800
906
  const captureAuthChecked = async (telemetry) => {
801
907
  if (authCheckedCaptured)
@@ -811,7 +917,19 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
811
917
  keySource: telemetry.keySource,
812
918
  });
813
919
  };
920
+ const captureCommandStarted = async () => {
921
+ if (commandStartedCaptured)
922
+ return;
923
+ commandStartedCaptured = true;
924
+ commandStartedAt = Date.now();
925
+ commandsAttempted += 1;
926
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_COMMAND_STARTED, {
927
+ commandName: telemetryCommandName,
928
+ commandArgsCount: Math.max(argv.length - 1, 0),
929
+ });
930
+ };
814
931
  const captureCommandCompleted = async () => {
932
+ await captureCommandStarted();
815
933
  commandsSucceeded += 1;
816
934
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_COMMAND_COMPLETED, {
817
935
  commandName: telemetryCommandName,
@@ -820,6 +938,34 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
820
938
  modelId: null,
821
939
  });
822
940
  };
941
+ const authenticateConfigCommand = async (flags) => {
942
+ let credentials;
943
+ try {
944
+ credentials = requireApiKey(flags, io);
945
+ }
946
+ catch (error) {
947
+ const failure = classifyCliFailure(error);
948
+ await captureAuthChecked({
949
+ authResult: "failure",
950
+ baaStatus: "signed",
951
+ keySource: "cli_flag",
952
+ errorCode: failure.errorCode,
953
+ failureCategory: failure.failureCategory,
954
+ });
955
+ const rateLimitError = recordUnauthenticatedAuthFailure(io);
956
+ throw rateLimitError ?? error;
957
+ }
958
+ const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
959
+ resetUnauthenticatedAuthFailures(io);
960
+ cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
961
+ await captureAuthChecked({
962
+ authResult: "success",
963
+ baaStatus: "not_required",
964
+ keySource: authTelemetryForSource(credentials.source).keySource,
965
+ failureCategory: null,
966
+ });
967
+ return { config, credentials };
968
+ };
823
969
  const captureSessionEnded = async (exitReason) => {
824
970
  if (sessionEndedCaptured)
825
971
  return;
@@ -833,11 +979,6 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
833
979
  });
834
980
  };
835
981
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_STARTED);
836
- await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_COMMAND_STARTED, {
837
- commandName: telemetryCommandName,
838
- commandArgsCount: Math.max(argv.length - 1, 0),
839
- });
840
- commandsAttempted += 1;
841
982
  try {
842
983
  const { command, flags, positionals } = parseArgs(argv);
843
984
  activeCommand = command;
@@ -847,6 +988,7 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
847
988
  return 0;
848
989
  }
849
990
  if (command === "login") {
991
+ await captureCommandStarted();
850
992
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_LOGIN_STARTED);
851
993
  const apiKey = flagString(flags, "api-key") ?? positionals[0];
852
994
  if (!apiKey) {
@@ -867,12 +1009,22 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
867
1009
  return 0;
868
1010
  }
869
1011
  if (command === "init") {
870
- const initStartedAt = Date.now();
871
- await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_STARTED);
872
1012
  rejectLegacyBaaInitFlags(flags, io);
873
1013
  const outputDir = flagString(flags, "output-dir") ?? DEFAULT_OUTPUT_DIR;
874
1014
  const apiBaseUrl = resolveTrustedApiBaseUrl(flagString(flags, "api-base-url") ?? DEFAULT_API_BASE_URL);
875
1015
  const initAuth = await resolveInitApiKey(flags, io, apiBaseUrl, captureAuthChecked);
1016
+ const config = buildInitConfig(flags, initAuth.setup);
1017
+ cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
1018
+ await captureAuthChecked({
1019
+ authResult: "success",
1020
+ baaStatus: initAuth.baaStatus,
1021
+ keySource: initAuth.keySource,
1022
+ failureCategory: null,
1023
+ });
1024
+ await captureCommandStarted();
1025
+ const initStartedAt = Date.now();
1026
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_STARTED);
1027
+ initStartedCaptured = true;
876
1028
  const output = flagString(flags, "output") ??
877
1029
  join(outputDir, "carecompass.config.json");
878
1030
  const snippetOutput = flagString(flags, "snippet-output") ??
@@ -895,8 +1047,6 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
895
1047
  }
896
1048
  }
897
1049
  mkdirSync(outputDir, { recursive: true });
898
- const config = buildInitConfig(flags, initAuth.setup);
899
- cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
900
1050
  writeFileSync(output, `${JSON.stringify(config, null, 2)}\n`);
901
1051
  writeFileSync(promptOutput, `${DEFAULT_SYSTEM_PROMPT}\n`);
902
1052
  const resolvedConfig = readConfig(output);
@@ -946,9 +1096,8 @@ Next command:
946
1096
  }
947
1097
  if (command === "validate") {
948
1098
  try {
949
- const credentials = requireApiKey(flags, io);
950
- const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
951
- cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
1099
+ const { config, credentials } = await authenticateConfigCommand(flags);
1100
+ await captureCommandStarted();
952
1101
  if (flags["local-only"] !== true) {
953
1102
  await assertHostedConfig(config, credentials.apiKey);
954
1103
  }
@@ -959,17 +1108,18 @@ Next command:
959
1108
  }
960
1109
  catch (error) {
961
1110
  const failure = classifyCliFailure(error);
962
- await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_VALIDATION_FAILED, {
963
- errorCode: failure.errorCode,
964
- failureCategory: failure.failureCategory,
965
- });
1111
+ if (commandStartedCaptured) {
1112
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_VALIDATION_FAILED, {
1113
+ errorCode: failure.errorCode,
1114
+ failureCategory: failure.failureCategory,
1115
+ });
1116
+ }
966
1117
  throw error;
967
1118
  }
968
1119
  }
969
1120
  if (command === "snippet") {
970
- requireApiKey(flags, io);
971
- const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
972
- cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
1121
+ const { config } = await authenticateConfigCommand(flags);
1122
+ await captureCommandStarted();
973
1123
  const snippet = generateCareCompassSnippet(runtimeConfig(config), {
974
1124
  mode: snippetMode(flags),
975
1125
  });
@@ -987,9 +1137,8 @@ Next command:
987
1137
  return 0;
988
1138
  }
989
1139
  if (command === "doctor") {
990
- const credentials = requireApiKey(flags, io);
991
- const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
992
- cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
1140
+ const { config, credentials } = await authenticateConfigCommand(flags);
1141
+ await captureCommandStarted();
993
1142
  let hostedStatus = "not checked";
994
1143
  try {
995
1144
  await assertHostedConfig(config, credentials.apiKey);
@@ -1013,12 +1162,16 @@ Status: ready for staging verification
1013
1162
  await captureCommandCompleted();
1014
1163
  return 0;
1015
1164
  }
1165
+ await captureCommandStarted();
1016
1166
  throw new Error(`Unknown command: ${command}`);
1017
1167
  }
1018
1168
  catch (error) {
1019
1169
  commandsFailed += 1;
1170
+ if (!commandStartedCaptured && commandsAttempted === 0) {
1171
+ commandsAttempted = 1;
1172
+ }
1020
1173
  const failure = classifyCliFailure(error);
1021
- if (activeCommand === "init") {
1174
+ if (activeCommand === "init" && initStartedCaptured) {
1022
1175
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_FAILED, {
1023
1176
  errorCode: failure.errorCode,
1024
1177
  failureCategory: failure.failureCategory,
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
+ };