@roarkanalytics/sdk-mcp 4.12.1 → 4.13.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/oauth.mjs CHANGED
@@ -8,6 +8,8 @@
8
8
  // key (minted at consent with the permissions the user picked, expiring and
9
9
  // rotated by the client's refresh token), so it is forwarded to customer-api as
10
10
  // the SDK bearer unchanged: customer-api validates it on every call.
11
+ import { createHash } from 'node:crypto';
12
+ import { readEnv } from "./util.mjs";
11
13
  /** Thrown when a request cannot be authorized; carries the WWW-Authenticate value to return. */
12
14
  export class UnauthorizedError extends Error {
13
15
  wwwAuthenticate;
@@ -90,4 +92,130 @@ export const requireBearer = (req, config, connector = ORIGIN_CONNECTOR) => {
90
92
  }
91
93
  return options;
92
94
  };
95
+ /** The SDK's own default, for when neither an option nor the environment names one. */
96
+ export const DEFAULT_API_BASE_URL = 'https://api.roark.ai';
97
+ /**
98
+ * The base URL the SDK will actually call, resolved exactly the way the SDK resolves it:
99
+ * explicit option, then `ROARK_BASE_URL`, then the default.
100
+ *
101
+ * This has to agree with the SDK or the liveness check asks the wrong host. `launchStreamableHTTPServer`
102
+ * passes no `clientOptions` at all, so for the deployed server the answer always comes from the
103
+ * environment: a hardcoded prod default would send every check to prod while data calls went to
104
+ * whatever `ROARK_BASE_URL` names. On any stage but prod that turns a valid token into a 401 from
105
+ * a server it was never issued for, and this file would then refuse every request - a dead
106
+ * connector, which is strictly worse than the bug it is here to fix.
107
+ */
108
+ export const resolveApiBaseUrl = (clientOptions = {}) => clientOptions.baseURL ?? readEnv('ROARK_BASE_URL') ?? DEFAULT_API_BASE_URL;
109
+ /**
110
+ * How long a verdict is reused. `newServer` runs per POST, not per session, so an uncached check
111
+ * would add a round trip to every single MCP request. The window is what a revoked token buys:
112
+ * it can still complete a handshake and read the tool list for up to this long. It cannot read
113
+ * anything, because every data path carries the same token to customer-api and is refused there
114
+ * immediately.
115
+ */
116
+ const BEARER_CACHE_TTL_MS = 60_000;
117
+ /**
118
+ * An unreachable customer-api is handled by the catch below, but a customer-api that accepts the
119
+ * connection and then never answers is not: without a deadline every MCP request would hang on
120
+ * it, which is worse than the failure this check exists to report. A hang becomes `unknown` like
121
+ * any other transport failure.
122
+ */
123
+ const BEARER_CHECK_TIMEOUT_MS = 3_000;
124
+ /**
125
+ * Bounded so that spraying invented tokens cannot grow the process's memory. Keys are hashes,
126
+ * not the tokens themselves: these are live credentials and there is no reason to keep a second
127
+ * copy of them in a long-lived map.
128
+ */
129
+ const BEARER_CACHE_MAX = 1_000;
130
+ const bearerVerdicts = new Map();
131
+ /**
132
+ * Checks already in flight, so concurrent requests bearing the same uncached token make one call
133
+ * rather than one each. That is the common shape right after a verdict expires, and whenever a
134
+ * client opens several requests at once.
135
+ */
136
+ const inFlightChecks = new Map();
137
+ /** Test seam. */
138
+ export const clearBearerVerdictCache = () => {
139
+ bearerVerdicts.clear();
140
+ inFlightChecks.clear();
141
+ };
142
+ const hashToken = (token) => createHash('sha256').update(token).digest('hex');
143
+ const rememberVerdict = (key, verdict, expiresAt) => {
144
+ // Evict the oldest entry rather than clearing the map. A `clear()` would let anyone spraying
145
+ // invented tokens flush every valid verdict too, making legitimate callers pay a round trip
146
+ // each and handing customer-api the extra work. Map iterates in insertion order, so the first
147
+ // key is the oldest.
148
+ if (bearerVerdicts.size >= BEARER_CACHE_MAX) {
149
+ const oldest = bearerVerdicts.keys().next();
150
+ if (!oldest.done)
151
+ bearerVerdicts.delete(oldest.value);
152
+ }
153
+ bearerVerdicts.set(key, { verdict, expiresAt });
154
+ };
155
+ /**
156
+ * Asks customer-api whether a bearer is still live, via the `/v1/me` endpoint it already exposes
157
+ * for exactly this ("who is this credential"), and caches the answer briefly.
158
+ *
159
+ * Only a 401 counts as invalid. A 403 means the token authenticated and then failed a permission
160
+ * check, which is a live token; treating it as invalid would tell a client to re-authorize when
161
+ * re-authorizing cannot help. Anything else - a 5xx, a timeout, a DNS failure - is `unknown`.
162
+ */
163
+ export const verifyBearer = async (token, deps) => {
164
+ const { baseURL, fetchImpl = fetch, now = Date.now, ttlMs = BEARER_CACHE_TTL_MS, timeoutMs = BEARER_CHECK_TIMEOUT_MS, } = deps;
165
+ const key = hashToken(token);
166
+ const cached = bearerVerdicts.get(key);
167
+ if (cached && cached.expiresAt > now())
168
+ return cached.verdict;
169
+ const alreadyRunning = inFlightChecks.get(key);
170
+ if (alreadyRunning)
171
+ return alreadyRunning;
172
+ const check = (async () => {
173
+ let response;
174
+ try {
175
+ response = await fetchImpl(`${stripSlash(baseURL)}/v1/me`, {
176
+ method: 'GET',
177
+ headers: { Authorization: `Bearer ${token}` },
178
+ signal: AbortSignal.timeout(timeoutMs),
179
+ });
180
+ }
181
+ catch {
182
+ return 'unknown';
183
+ }
184
+ const verdict = response.status === 401 ? 'invalid'
185
+ : response.ok ? 'valid'
186
+ : 'unknown';
187
+ // An `unknown` is never cached: a blip would otherwise pin the answer for the whole TTL.
188
+ if (verdict !== 'unknown')
189
+ rememberVerdict(key, verdict, now() + ttlMs);
190
+ return verdict;
191
+ })();
192
+ inFlightChecks.set(key, check);
193
+ try {
194
+ return await check;
195
+ }
196
+ finally {
197
+ inFlightChecks.delete(key);
198
+ }
199
+ };
200
+ /**
201
+ * Rejects a bearer customer-api has already disowned, with the 401 + WWW-Authenticate that tells
202
+ * an MCP client to re-run the OAuth flow.
203
+ *
204
+ * Without this the resource server only checked that a token was PRESENT. A revoked, expired or
205
+ * entirely invented token completed `initialize` and `tools/list`, and the caller learned nothing
206
+ * until a tool call failed with an opaque 401 in its result text. No data was reachable - every
207
+ * data path revalidates at customer-api - but a client that never sees a 401 from the resource
208
+ * server has no reason to refresh, which for a 24h token means a connector that looks connected
209
+ * and silently cannot work.
210
+ *
211
+ * `unknown` is allowed through deliberately. Failing closed would turn a customer-api blip into a
212
+ * dead connector for everyone, and it would buy no confidentiality: the token still has to pass
213
+ * customer-api on the very next call that touches data.
214
+ */
215
+ export const requireLiveBearer = async (token, config, connector, deps) => {
216
+ const verdict = await verifyBearer(token, deps);
217
+ if (verdict !== 'invalid')
218
+ return;
219
+ throw new UnauthorizedError('The access token is expired or has been revoked', bearerChallenge(config, connector, 'invalid_token', 'The access token is expired or has been revoked'));
220
+ };
93
221
  //# sourceMappingURL=oauth.mjs.map
package/oauth.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.mjs","sourceRoot":"","sources":["src/oauth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"oauth.mjs","sourceRoot":"","sources":["src/oauth.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roarkanalytics/sdk-mcp",
3
- "version": "4.12.1",
3
+ "version": "4.13.0",
4
4
  "description": "The official MCP Server for the Roark API",
5
5
  "author": "Roark <james@roark.ai>",
6
6
  "types": "./index.d.ts",
package/server.js CHANGED
@@ -19,7 +19,7 @@ const instructions_1 = require("./instructions.js");
19
19
  const methods_1 = require("./methods.js");
20
20
  const newMcpServer = async ({ stainlessApiKey, customInstructionsPath, }) => new mcp_js_1.McpServer({
21
21
  name: 'roarkanalytics_sdk_api',
22
- version: '4.12.1', // x-release-please-version
22
+ version: '4.13.0', // x-release-please-version
23
23
  }, {
24
24
  instructions: await (0, instructions_1.getInstructions)({ stainlessApiKey, customInstructionsPath }),
25
25
  capabilities: { tools: {}, logging: {} },
package/server.mjs CHANGED
@@ -10,7 +10,7 @@ import { getInstructions } from "./instructions.mjs";
10
10
  import { blockedMethodsForCodeTool } from "./methods.mjs";
11
11
  export const newMcpServer = async ({ stainlessApiKey, customInstructionsPath, }) => new McpServer({
12
12
  name: 'roarkanalytics_sdk_api',
13
- version: '4.12.1', // x-release-please-version
13
+ version: '4.13.0', // x-release-please-version
14
14
  }, {
15
15
  instructions: await getInstructions({ stainlessApiKey, customInstructionsPath }),
16
16
  capabilities: { tools: {}, logging: {} },
package/src/http.ts CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  ORIGIN_CONNECTOR,
16
16
  protectedResourceMetadata,
17
17
  requireBearer,
18
+ requireLiveBearer,
19
+ resolveApiBaseUrl,
18
20
  UnauthorizedError,
19
21
  } from './oauth';
20
22
  import { initMcpServer, newMcpServer } from './server';
@@ -52,7 +54,20 @@ const newServer = async ({
52
54
  // not be able to make us do that.
53
55
  let authOptions: Partial<ClientOptions>;
54
56
  if (mcpOptions.oauth) {
55
- authOptions = requireBearer(req, mcpOptions.oauth, connectorFrom(req));
57
+ const connector = connectorFrom(req);
58
+ const authorized = requireBearer(req, mcpOptions.oauth, connector);
59
+ // Presence is not validity. Ask customer-api whether the token is still live, so a revoked or
60
+ // expired one is answered with 401 + WWW-Authenticate here - the signal a client needs to
61
+ // re-run the OAuth flow - rather than completing the handshake and failing later inside a
62
+ // tool result, where nothing is listening for it.
63
+ //
64
+ // The base URL is resolved the way the SDK resolves it, NOT defaulted to prod: this server is
65
+ // launched without `clientOptions`, so on any stage but prod a prod default would check the
66
+ // token against a server that never issued it and refuse every request.
67
+ await requireLiveBearer(authorized.bearerToken, mcpOptions.oauth, connector, {
68
+ baseURL: resolveApiBaseUrl(clientOptions),
69
+ });
70
+ authOptions = authorized;
56
71
  } else {
57
72
  authOptions = parseClientAuthHeaders(req, false);
58
73
  }
@@ -1001,9 +1001,9 @@ const EMBEDDED_METHODS: MethodEntry[] = [
1001
1001
  qualified: 'client.simulationJob.lookup',
1002
1002
  params: ['roarkPhoneNumber: string;', 'callReceivedAt?: string;'],
1003
1003
  response:
1004
- "{ data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }; }",
1004
+ "{ data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; backgroundNoise: { backgroundNoise: string; backgroundNoiseVolume: number; source: 'ENVIRONMENT' | 'PERSONA'; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }; }",
1005
1005
  markdown:
1006
- "## lookup\n\n`client.simulationJob.lookup(roarkPhoneNumber: string, callReceivedAt?: string): { data: object; }`\n\n**get** `/v1/simulation/job/lookup`\n\nFind the matching simulation using the number used by the Roark simulation agent.\n\n### Parameters\n\n- `roarkPhoneNumber: string`\n Phone number provisioned by Roark for the simulation job in E.164 format. In the case of an inbound simulation, this is the number that calls your agent; in the case of an outbound simulation, this is the number you call from your agent.\n\n- `callReceivedAt?: string`\n ISO 8601 timestamp of when the call was received. Alternatively, any time between the start and end of the call is valid. Defaults to the current time, which fetches any jobs that are currently ongoing.\n\n### Returns\n\n- `{ data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }; }`\n\n - `data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }`\n\n### Example\n\n```typescript\nimport Roark from '@roarkanalytics/sdk';\n\nconst client = new Roark();\n\nconst response = await client.simulationJob.lookup({ roarkPhoneNumber: 'roarkPhoneNumber' });\n\nconsole.log(response);\n```",
1006
+ "## lookup\n\n`client.simulationJob.lookup(roarkPhoneNumber: string, callReceivedAt?: string): { data: object; }`\n\n**get** `/v1/simulation/job/lookup`\n\nFind the matching simulation using the number used by the Roark simulation agent.\n\n### Parameters\n\n- `roarkPhoneNumber: string`\n Phone number provisioned by Roark for the simulation job in E.164 format. In the case of an inbound simulation, this is the number that calls your agent; in the case of an outbound simulation, this is the number you call from your agent.\n\n- `callReceivedAt?: string`\n ISO 8601 timestamp of when the call was received. Alternatively, any time between the start and end of the call is valid. Defaults to the current time, which fetches any jobs that are currently ongoing.\n\n### Returns\n\n- `{ data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; backgroundNoise: { backgroundNoise: string; backgroundNoiseVolume: number; source: 'ENVIRONMENT' | 'PERSONA'; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }; }`\n\n - `data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; backgroundNoise: { backgroundNoise: string; backgroundNoiseVolume: number; source: 'ENVIRONMENT' | 'PERSONA'; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }`\n\n### Example\n\n```typescript\nimport Roark from '@roarkanalytics/sdk';\n\nconst client = new Roark();\n\nconst response = await client.simulationJob.lookup({ roarkPhoneNumber: 'roarkPhoneNumber' });\n\nconsole.log(response);\n```",
1007
1007
  perLanguage: {
1008
1008
  typescript: {
1009
1009
  method: 'client.simulationJob.lookup',
@@ -1032,9 +1032,9 @@ const EMBEDDED_METHODS: MethodEntry[] = [
1032
1032
  qualified: 'client.simulationJob.getByID',
1033
1033
  params: ['jobId: string;'],
1034
1034
  response:
1035
- "{ data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }; }",
1035
+ "{ data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; backgroundNoise: { backgroundNoise: string; backgroundNoiseVolume: number; source: 'ENVIRONMENT' | 'PERSONA'; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }; }",
1036
1036
  markdown:
1037
- "## getById\n\n`client.simulationJob.getByID(jobId: string): { data: object; }`\n\n**get** `/v1/simulation/job/{jobId}`\n\nGet a individual simulation run directly by its ID. This is generally part of a larger simulation run plan job.\n\n### Parameters\n\n- `jobId: string`\n\n### Returns\n\n- `{ data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }; }`\n\n - `data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }`\n\n### Example\n\n```typescript\nimport Roark from '@roarkanalytics/sdk';\n\nconst client = new Roark();\n\nconst response = await client.simulationJob.getByID('7f3e4d2c-8a91-4b5c-9e6f-1a2b3c4d5e6f');\n\nconsole.log(response);\n```",
1037
+ "## getById\n\n`client.simulationJob.getByID(jobId: string): { data: object; }`\n\n**get** `/v1/simulation/job/{jobId}`\n\nGet a individual simulation run directly by its ID. This is generally part of a larger simulation run plan job.\n\n### Parameters\n\n- `jobId: string`\n\n### Returns\n\n- `{ data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; backgroundNoise: { backgroundNoise: string; backgroundNoiseVolume: number; source: 'ENVIRONMENT' | 'PERSONA'; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }; }`\n\n - `data: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; backgroundNoise: { backgroundNoise: string; backgroundNoiseVolume: number; source: 'ENVIRONMENT' | 'PERSONA'; }; createdAt: string; enrichment: { outcome: 'NOT_REQUESTED' | 'WAITING' | 'MERGED' | 'TIMED_OUT' | 'SKIPPED' | 'UNKNOWN'; requested: boolean; metricsNotCollected?: number; metricsScoredOnSimulationInstead?: number; waitEndedAt?: string; waitStartedAt?: string; }; failureOwner: 'ROARK' | 'AGENT'; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; queuePosition: number; runPlan: { id: string; name: string; variables: object; }; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }`\n\n### Example\n\n```typescript\nimport Roark from '@roarkanalytics/sdk';\n\nconst client = new Roark();\n\nconst response = await client.simulationJob.getByID('7f3e4d2c-8a91-4b5c-9e6f-1a2b3c4d5e6f');\n\nconsole.log(response);\n```",
1038
1038
  perLanguage: {
1039
1039
  typescript: {
1040
1040
  method: 'client.simulationJob.getByID',
@@ -1455,9 +1455,9 @@ const EMBEDDED_METHODS: MethodEntry[] = [
1455
1455
  qualified: 'client.simulationRunPlanJob.getByID',
1456
1456
  params: ['jobId: string;'],
1457
1457
  response:
1458
- '{ data: { createdAt: string; simulationJobs: { agentEndpoint: object; createdAt: string; invalidation: object; persona: object; processingStatus: string; scenario: object; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }[]; simulationRunPlanId: string; simulationRunPlanJobId: string; status: string; endedAt?: string; startedAt?: string; sweepAttribution?: { caveats: string[]; checks: object[]; falseDiscoveryRate: number; foundUnattributedFailureRate: number; minArmCalls: number; minimumDetectableGap: number; testableValueCount: number; testedComparisonCount: number; values: object[]; }; verdict?: { checks: object[]; failures: object[]; passed: boolean; score: number; }; }; }',
1458
+ '{ data: { createdAt: string; simulationJobs: { agentEndpoint: object; backgroundNoise: object; createdAt: string; invalidation: object; persona: object; processingStatus: string; scenario: object; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }[]; simulationRunPlanId: string; simulationRunPlanJobId: string; status: string; endedAt?: string; startedAt?: string; sweepAttribution?: { caveats: string[]; checks: object[]; falseDiscoveryRate: number; foundUnattributedFailureRate: number; minArmCalls: number; minimumDetectableGap: number; testableValueCount: number; testedComparisonCount: number; values: object[]; }; verdict?: { checks: object[]; failures: object[]; passed: boolean; score: number; }; }; }',
1459
1459
  markdown:
1460
- "## getById\n\n`client.simulationRunPlanJob.getByID(jobId: string): { data: object; }`\n\n**get** `/v1/simulation/plan/job/{jobId}`\n\nRetrieve details of a simulation plan job including all associated simulation jobs (calls)\n\n### Parameters\n\n- `jobId: string`\n\n### Returns\n\n- `{ data: { createdAt: string; simulationJobs: { agentEndpoint: object; createdAt: string; invalidation: object; persona: object; processingStatus: string; scenario: object; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }[]; simulationRunPlanId: string; simulationRunPlanJobId: string; status: string; endedAt?: string; startedAt?: string; sweepAttribution?: { caveats: string[]; checks: object[]; falseDiscoveryRate: number; foundUnattributedFailureRate: number; minArmCalls: number; minimumDetectableGap: number; testableValueCount: number; testedComparisonCount: number; values: object[]; }; verdict?: { checks: object[]; failures: object[]; passed: boolean; score: number; }; }; }`\n\n - `data: { createdAt: string; simulationJobs: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; createdAt: string; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }[]; simulationRunPlanId: string; simulationRunPlanJobId: string; status: string; endedAt?: string; startedAt?: string; sweepAttribution?: { caveats: string[]; checks: { attribution: 'PROPERTY_ATTRIBUTABLE' | 'FOUND_UNATTRIBUTED' | 'WITHIN_NOISE'; evaluated: number; failed: number; failureRate: number; metricDefinitionId: string; metricName: string; worseValues: object[]; }[]; falseDiscoveryRate: number; foundUnattributedFailureRate: number; minArmCalls: number; minimumDetectableGap: number; testableValueCount: number; testedComparisonCount: number; values: { attempted: number; counted: number; isBaseline: boolean; label: string; score: number; testable: boolean; value: string; }[]; }; verdict?: { checks: { evaluatedSims: number; inherited: boolean; metricDefinitionId: string; minPassRate: number; passRate: number; passed: boolean; passedSims: number; metricName?: string; }[]; failures: { status: string; type: 'RUN_NOT_COMPLETED'; } | { evaluatedCalls: number; expectedCalls: number; type: 'INCOMPLETE_COVERAGE'; } | { metricDefinitionId: string; type: 'METRIC_NOT_EVALUATED'; metricName?: string; } | { inherited: boolean; metricDefinitionId: string; minPassRate: number; passRate: number; type: 'METRIC_BELOW_MIN_PASS_RATE'; metricName?: string; }[]; passed: boolean; score: number; }; }`\n\n### Example\n\n```typescript\nimport Roark from '@roarkanalytics/sdk';\n\nconst client = new Roark();\n\nconst response = await client.simulationRunPlanJob.getByID('7f3e4d2c-8a91-4b5c-9e6f-1a2b3c4d5e6f');\n\nconsole.log(response);\n```",
1460
+ "## getById\n\n`client.simulationRunPlanJob.getByID(jobId: string): { data: object; }`\n\n**get** `/v1/simulation/plan/job/{jobId}`\n\nRetrieve details of a simulation plan job including all associated simulation jobs (calls)\n\n### Parameters\n\n- `jobId: string`\n\n### Returns\n\n- `{ data: { createdAt: string; simulationJobs: { agentEndpoint: object; backgroundNoise: object; createdAt: string; invalidation: object; persona: object; processingStatus: string; scenario: object; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }[]; simulationRunPlanId: string; simulationRunPlanJobId: string; status: string; endedAt?: string; startedAt?: string; sweepAttribution?: { caveats: string[]; checks: object[]; falseDiscoveryRate: number; foundUnattributedFailureRate: number; minArmCalls: number; minimumDetectableGap: number; testableValueCount: number; testedComparisonCount: number; values: object[]; }; verdict?: { checks: object[]; failures: object[]; passed: boolean; score: number; }; }; }`\n\n - `data: { createdAt: string; simulationJobs: { agentEndpoint: { id: string; name: string; phoneNumber: string; type: string; }; backgroundNoise: { backgroundNoise: string; backgroundNoiseVolume: number; source: 'ENVIRONMENT' | 'PERSONA'; }; createdAt: string; invalidation: { invalidatedAt: string; reason: 'SCRIPT_DIVERGED' | 'REQUIRED_STEP_NOT_REACHED' | 'REQUIRED_STAGE_INCOMPLETE' | 'CALLER_NEVER_TOOK_OVER' | 'AGENT_NEVER_SPOKE'; detail?: string; }; persona: { id: string; accent: string; age: 'CHILD' | 'TEENAGER' | 'ADULT' | 'ELDERLY'; backgroundNoise: string; baseEmotion: string; confirmationStyle: 'EXPLICIT' | 'VAGUE'; createdAt: string; gender: 'MALE' | 'FEMALE'; hasDisfluencies: boolean; idleMessageMaxSpokenCount: number; idleMessageResetCountOnUserSpeechEnabled: boolean; idleMessages: string[]; idleTimeoutSeconds: number; intentClarity: 'CLEAR' | 'INDIRECT' | 'VAGUE'; interruption: 'OFF' | 'BACKCHANNEL' | 'OCCASIONAL' | 'HEAVY'; language: string; memoryReliability: 'HIGH' | 'LOW'; name: string; properties: object; responseTiming: 'RELAXED' | 'NORMAL' | 'QUICK' | 'BARGE_IN'; speechClarity: 'CLEAR' | 'VAGUE' | 'RAMBLING'; speechPace: 'SUPER_SLOW' | 'SLOW' | 'NORMAL' | 'FAST' | 'SUPER_FAST'; understoodLanguages: string[]; updatedAt: string; backstoryPrompt?: string; description?: string; displayName?: string; secondaryLanguage?: 'EN'; }; processingStatus: string; scenario: { id: string; description?: string; }; simulationJobId: string; status: string; callId?: string; completedAt?: string; roarkPhoneNumber?: string; startedAt?: string; }[]; simulationRunPlanId: string; simulationRunPlanJobId: string; status: string; endedAt?: string; startedAt?: string; sweepAttribution?: { caveats: string[]; checks: { attribution: 'PROPERTY_ATTRIBUTABLE' | 'FOUND_UNATTRIBUTED' | 'WITHIN_NOISE'; evaluated: number; failed: number; failureRate: number; metricDefinitionId: string; metricName: string; worseValues: object[]; }[]; falseDiscoveryRate: number; foundUnattributedFailureRate: number; minArmCalls: number; minimumDetectableGap: number; testableValueCount: number; testedComparisonCount: number; values: { attempted: number; counted: number; isBaseline: boolean; label: string; score: number; testable: boolean; value: string; }[]; }; verdict?: { checks: { evaluatedSims: number; inherited: boolean; metricDefinitionId: string; minPassRate: number; passRate: number; passed: boolean; passedSims: number; metricName?: string; }[]; failures: { status: string; type: 'RUN_NOT_COMPLETED'; } | { evaluatedCalls: number; expectedCalls: number; type: 'INCOMPLETE_COVERAGE'; } | { metricDefinitionId: string; type: 'METRIC_NOT_EVALUATED'; metricName?: string; } | { inherited: boolean; metricDefinitionId: string; minPassRate: number; passRate: number; type: 'METRIC_BELOW_MIN_PASS_RATE'; metricName?: string; }[]; passed: boolean; score: number; }; }`\n\n### Example\n\n```typescript\nimport Roark from '@roarkanalytics/sdk';\n\nconst client = new Roark();\n\nconst response = await client.simulationRunPlanJob.getByID('7f3e4d2c-8a91-4b5c-9e6f-1a2b3c4d5e6f');\n\nconsole.log(response);\n```",
1461
1461
  perLanguage: {
1462
1462
  typescript: {
1463
1463
  method: 'client.simulationRunPlanJob.getByID',
package/src/oauth.ts CHANGED
@@ -9,8 +9,10 @@
9
9
  // rotated by the client's refresh token), so it is forwarded to customer-api as
10
10
  // the SDK bearer unchanged: customer-api validates it on every call.
11
11
 
12
+ import { createHash } from 'node:crypto';
12
13
  import { IncomingMessage } from 'node:http';
13
14
  import { ClientOptions } from '@roarkanalytics/sdk';
15
+ import { readEnv } from './util';
14
16
 
15
17
  export type OAuthConfig = {
16
18
  /** Authorization server issuer, e.g. https://mcp-oauth.api.roark.ai */
@@ -121,7 +123,7 @@ export const requireBearer = (
121
123
  req: IncomingMessage,
122
124
  config: OAuthConfig,
123
125
  connector: Connector = ORIGIN_CONNECTOR,
124
- ): Partial<ClientOptions> => {
126
+ ): Partial<ClientOptions> & { bearerToken: string } => {
125
127
  const token = extractBearer(req);
126
128
  if (!token) {
127
129
  throw new UnauthorizedError('Missing bearer token', bearerChallenge(config, connector));
@@ -129,9 +131,176 @@ export const requireBearer = (
129
131
  // Assigned rather than conditionally spread: a spread of `cond && { project }` is not checked
130
132
  // against `ClientOptions`, so on an SDK without the option the pin would compile and then
131
133
  // silently vanish. This way the dependency is enforced by the build.
132
- const options: Partial<ClientOptions> = { bearerToken: token };
134
+ const options: Partial<ClientOptions> & { bearerToken: string } = { bearerToken: token };
133
135
  if (connector.kind === 'project') {
134
136
  options.project = connector.projectId;
135
137
  }
136
138
  return options;
137
139
  };
140
+
141
+ /**
142
+ * What customer-api says about a bearer: `unknown` means it could not be asked, not that the
143
+ * token is fine.
144
+ */
145
+ export type BearerVerdict = 'valid' | 'invalid' | 'unknown';
146
+
147
+ /** The SDK's own default, for when neither an option nor the environment names one. */
148
+ export const DEFAULT_API_BASE_URL = 'https://api.roark.ai';
149
+
150
+ /**
151
+ * The base URL the SDK will actually call, resolved exactly the way the SDK resolves it:
152
+ * explicit option, then `ROARK_BASE_URL`, then the default.
153
+ *
154
+ * This has to agree with the SDK or the liveness check asks the wrong host. `launchStreamableHTTPServer`
155
+ * passes no `clientOptions` at all, so for the deployed server the answer always comes from the
156
+ * environment: a hardcoded prod default would send every check to prod while data calls went to
157
+ * whatever `ROARK_BASE_URL` names. On any stage but prod that turns a valid token into a 401 from
158
+ * a server it was never issued for, and this file would then refuse every request - a dead
159
+ * connector, which is strictly worse than the bug it is here to fix.
160
+ */
161
+ export const resolveApiBaseUrl = (clientOptions: Pick<ClientOptions, 'baseURL'> = {}): string =>
162
+ clientOptions.baseURL ?? readEnv('ROARK_BASE_URL') ?? DEFAULT_API_BASE_URL;
163
+
164
+ /**
165
+ * How long a verdict is reused. `newServer` runs per POST, not per session, so an uncached check
166
+ * would add a round trip to every single MCP request. The window is what a revoked token buys:
167
+ * it can still complete a handshake and read the tool list for up to this long. It cannot read
168
+ * anything, because every data path carries the same token to customer-api and is refused there
169
+ * immediately.
170
+ */
171
+ const BEARER_CACHE_TTL_MS = 60_000;
172
+
173
+ /**
174
+ * An unreachable customer-api is handled by the catch below, but a customer-api that accepts the
175
+ * connection and then never answers is not: without a deadline every MCP request would hang on
176
+ * it, which is worse than the failure this check exists to report. A hang becomes `unknown` like
177
+ * any other transport failure.
178
+ */
179
+ const BEARER_CHECK_TIMEOUT_MS = 3_000;
180
+
181
+ /**
182
+ * Bounded so that spraying invented tokens cannot grow the process's memory. Keys are hashes,
183
+ * not the tokens themselves: these are live credentials and there is no reason to keep a second
184
+ * copy of them in a long-lived map.
185
+ */
186
+ const BEARER_CACHE_MAX = 1_000;
187
+
188
+ const bearerVerdicts = new Map<string, { verdict: 'valid' | 'invalid'; expiresAt: number }>();
189
+
190
+ /**
191
+ * Checks already in flight, so concurrent requests bearing the same uncached token make one call
192
+ * rather than one each. That is the common shape right after a verdict expires, and whenever a
193
+ * client opens several requests at once.
194
+ */
195
+ const inFlightChecks = new Map<string, Promise<BearerVerdict>>();
196
+
197
+ /** Test seam. */
198
+ export const clearBearerVerdictCache = (): void => {
199
+ bearerVerdicts.clear();
200
+ inFlightChecks.clear();
201
+ };
202
+
203
+ const hashToken = (token: string): string => createHash('sha256').update(token).digest('hex');
204
+
205
+ const rememberVerdict = (key: string, verdict: 'valid' | 'invalid', expiresAt: number): void => {
206
+ // Evict the oldest entry rather than clearing the map. A `clear()` would let anyone spraying
207
+ // invented tokens flush every valid verdict too, making legitimate callers pay a round trip
208
+ // each and handing customer-api the extra work. Map iterates in insertion order, so the first
209
+ // key is the oldest.
210
+ if (bearerVerdicts.size >= BEARER_CACHE_MAX) {
211
+ const oldest = bearerVerdicts.keys().next();
212
+ if (!oldest.done) bearerVerdicts.delete(oldest.value);
213
+ }
214
+ bearerVerdicts.set(key, { verdict, expiresAt });
215
+ };
216
+
217
+ export interface VerifyBearerDeps {
218
+ /** customer-api base URL. Use {@link resolveApiBaseUrl} rather than passing a literal. */
219
+ baseURL: string;
220
+ fetchImpl?: typeof fetch;
221
+ now?: () => number;
222
+ ttlMs?: number;
223
+ timeoutMs?: number;
224
+ }
225
+
226
+ /**
227
+ * Asks customer-api whether a bearer is still live, via the `/v1/me` endpoint it already exposes
228
+ * for exactly this ("who is this credential"), and caches the answer briefly.
229
+ *
230
+ * Only a 401 counts as invalid. A 403 means the token authenticated and then failed a permission
231
+ * check, which is a live token; treating it as invalid would tell a client to re-authorize when
232
+ * re-authorizing cannot help. Anything else - a 5xx, a timeout, a DNS failure - is `unknown`.
233
+ */
234
+ export const verifyBearer = async (token: string, deps: VerifyBearerDeps): Promise<BearerVerdict> => {
235
+ const {
236
+ baseURL,
237
+ fetchImpl = fetch,
238
+ now = Date.now,
239
+ ttlMs = BEARER_CACHE_TTL_MS,
240
+ timeoutMs = BEARER_CHECK_TIMEOUT_MS,
241
+ } = deps;
242
+ const key = hashToken(token);
243
+
244
+ const cached = bearerVerdicts.get(key);
245
+ if (cached && cached.expiresAt > now()) return cached.verdict;
246
+
247
+ const alreadyRunning = inFlightChecks.get(key);
248
+ if (alreadyRunning) return alreadyRunning;
249
+
250
+ const check = (async (): Promise<BearerVerdict> => {
251
+ let response: Response;
252
+ try {
253
+ response = await fetchImpl(`${stripSlash(baseURL)}/v1/me`, {
254
+ method: 'GET',
255
+ headers: { Authorization: `Bearer ${token}` },
256
+ signal: AbortSignal.timeout(timeoutMs),
257
+ });
258
+ } catch {
259
+ return 'unknown';
260
+ }
261
+
262
+ const verdict: BearerVerdict =
263
+ response.status === 401 ? 'invalid'
264
+ : response.ok ? 'valid'
265
+ : 'unknown';
266
+ // An `unknown` is never cached: a blip would otherwise pin the answer for the whole TTL.
267
+ if (verdict !== 'unknown') rememberVerdict(key, verdict, now() + ttlMs);
268
+ return verdict;
269
+ })();
270
+
271
+ inFlightChecks.set(key, check);
272
+ try {
273
+ return await check;
274
+ } finally {
275
+ inFlightChecks.delete(key);
276
+ }
277
+ };
278
+
279
+ /**
280
+ * Rejects a bearer customer-api has already disowned, with the 401 + WWW-Authenticate that tells
281
+ * an MCP client to re-run the OAuth flow.
282
+ *
283
+ * Without this the resource server only checked that a token was PRESENT. A revoked, expired or
284
+ * entirely invented token completed `initialize` and `tools/list`, and the caller learned nothing
285
+ * until a tool call failed with an opaque 401 in its result text. No data was reachable - every
286
+ * data path revalidates at customer-api - but a client that never sees a 401 from the resource
287
+ * server has no reason to refresh, which for a 24h token means a connector that looks connected
288
+ * and silently cannot work.
289
+ *
290
+ * `unknown` is allowed through deliberately. Failing closed would turn a customer-api blip into a
291
+ * dead connector for everyone, and it would buy no confidentiality: the token still has to pass
292
+ * customer-api on the very next call that touches data.
293
+ */
294
+ export const requireLiveBearer = async (
295
+ token: string,
296
+ config: OAuthConfig,
297
+ connector: Connector,
298
+ deps: VerifyBearerDeps,
299
+ ): Promise<void> => {
300
+ const verdict = await verifyBearer(token, deps);
301
+ if (verdict !== 'invalid') return;
302
+ throw new UnauthorizedError(
303
+ 'The access token is expired or has been revoked',
304
+ bearerChallenge(config, connector, 'invalid_token', 'The access token is expired or has been revoked'),
305
+ );
306
+ };
package/src/server.ts CHANGED
@@ -28,7 +28,7 @@ export const newMcpServer = async ({
28
28
  new McpServer(
29
29
  {
30
30
  name: 'roarkanalytics_sdk_api',
31
- version: '4.12.1', // x-release-please-version
31
+ version: '4.13.0', // x-release-please-version
32
32
  },
33
33
  {
34
34
  instructions: await getInstructions({ stainlessApiKey, customInstructionsPath }),