@herbertgao/pi-extensions 2026.9.11 → 2026.9.13
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 +4 -4
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +21 -4
- package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
- package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
- package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
- package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +62 -0
- package/node_modules/pi-mcp-adapter/README.md +75 -7
- package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +193 -0
- package/node_modules/pi-mcp-adapter/cli.js +56 -5
- package/node_modules/pi-mcp-adapter/commands.ts +96 -5
- package/node_modules/pi-mcp-adapter/config.ts +73 -6
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +22 -28
- package/node_modules/pi-mcp-adapter/direct-tools.ts +13 -5
- package/node_modules/pi-mcp-adapter/dist/abort.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/abort.js +36 -0
- package/node_modules/pi-mcp-adapter/dist/abort.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +194 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +5 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +75 -6
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +18 -0
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js +88 -0
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +316 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/errors.d.ts +131 -0
- package/node_modules/pi-mcp-adapter/dist/errors.js +278 -0
- package/node_modules/pi-mcp-adapter/dist/errors.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +8 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.js +88 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +15 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.js +340 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +77 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +2 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +22 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +85 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +55 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +56 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js +410 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/logger.d.ts +51 -0
- package/node_modules/pi-mcp-adapter/dist/logger.js +131 -0
- package/node_modules/pi-mcp-adapter/dist/logger.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +26 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +103 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +118 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1098 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +168 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +1117 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +7 -140
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +53 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +441 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +148 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +689 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +166 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +102 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +369 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +95 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +242 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +2 -11
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +505 -0
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +10 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +312 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +13 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +97 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +203 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +11 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +172 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +157 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +1743 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +34 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js +140 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +26 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js +142 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/state.d.ts +73 -0
- package/node_modules/pi-mcp-adapter/dist/state.js +2 -0
- package/node_modules/pi-mcp-adapter/dist/state.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +36 -0
- package/node_modules/pi-mcp-adapter/dist/types.js +18 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +219 -0
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +15 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +85 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +13 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/elicitation-handler.ts +29 -19
- package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +85 -0
- package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +34 -0
- package/node_modules/pi-mcp-adapter/index.ts +129 -6
- package/node_modules/pi-mcp-adapter/init.ts +6 -19
- package/node_modules/pi-mcp-adapter/jev-client.ts +273 -0
- package/node_modules/pi-mcp-adapter/jev-contracts.ts +53 -0
- package/node_modules/pi-mcp-adapter/jev-key-store.ts +79 -0
- package/node_modules/pi-mcp-adapter/lifecycle.ts +14 -3
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +7 -142
- package/node_modules/pi-mcp-adapter/mcp-code.ts +119 -11
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +5 -3
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +5 -0
- package/node_modules/pi-mcp-adapter/mcp-tasks.ts +467 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +2 -13
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +17 -9
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +319 -166
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +6 -3
- package/node_modules/pi-mcp-adapter/search-ranking.ts +38 -7
- package/node_modules/pi-mcp-adapter/secure-keyring.ts +170 -0
- package/node_modules/pi-mcp-adapter/semantic-search.ts +186 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +293 -50
- package/node_modules/pi-mcp-adapter/session-approvals.ts +14 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +2 -0
- package/node_modules/pi-mcp-adapter/state.ts +3 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +26 -4
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -14
- package/node_modules/pi-mcp-adapter/tool-registrar.ts +8 -6
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +4 -1
- package/node_modules/pi-mcp-adapter/types.ts +53 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +17 -9
- package/node_modules/pi-mcp-adapter/utils.ts +16 -0
- package/node_modules/pi-multi-account/CHANGELOG.md +23 -0
- package/node_modules/pi-multi-account/README.md +38 -11
- package/node_modules/pi-multi-account/index.ts +371 -96
- package/node_modules/pi-multi-account/package.json +5 -4
- package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
- package/node_modules/pi-multi-account/usage.ts +31 -2
- package/node_modules/pi-typesafe/README.md +3 -1
- package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
- package/node_modules/pi-typesafe/dist/auth.js +13 -7
- package/node_modules/pi-typesafe/dist/backends.d.ts +31 -0
- package/node_modules/pi-typesafe/dist/backends.js +33 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +3 -9
- package/node_modules/pi-typesafe/dist/client.js +63 -39
- package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
- package/node_modules/pi-typesafe/dist/credentials.js +15 -8
- package/node_modules/pi-typesafe/dist/extension.js +4 -1
- package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
- package/node_modules/pi-typesafe/dist/login.js +17 -8
- package/node_modules/pi-typesafe/dist/schema.js +19 -13
- package/node_modules/pi-typesafe/package.json +1 -1
- package/package.json +11 -10
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
2
|
+
import type { McpExtensionState } from "./state.ts";
|
|
3
|
+
import { isServerDisabled } from "./types.ts";
|
|
4
|
+
import { combineAbortSignals } from "./runtime-owner.ts";
|
|
5
|
+
import { resolveJevCredential, TYPESAFE_API_ORIGIN, type JevCredentialResolution } from "./jev-key-store.ts";
|
|
6
|
+
import type { JevAnswer, JevBudget, JevEvaluateInput, JevEvaluationData, JevEvaluationEnvelope, JevJson, JevQuestion, ResolvedJevSettings } from "./jev-contracts.ts";
|
|
7
|
+
|
|
8
|
+
export { TYPESAFE_API_ORIGIN };
|
|
9
|
+
export type { ResolvedJevSettings } from "./jev-contracts.ts";
|
|
10
|
+
|
|
11
|
+
const DEFAULTS: ResolvedJevSettings = {
|
|
12
|
+
semanticSearch: false, scriptEvaluation: false, allowedServers: [], model: "jev-1.13.0",
|
|
13
|
+
requestTimeoutMs: 5_000, maxRetries: 0, maxStateBytes: 262_144,
|
|
14
|
+
maxQuestionsPerRequest: 64, maxEvaluationsPerScript: 8,
|
|
15
|
+
maxEvaluationBytesPerScript: 524_288, maxEvaluationTokensPerScript: 32_768, semanticCandidateLimit: 127,
|
|
16
|
+
semanticMinProbability: 0.2,
|
|
17
|
+
};
|
|
18
|
+
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
19
|
+
|
|
20
|
+
type JevSettingsInput = Partial<ResolvedJevSettings>;
|
|
21
|
+
type Host = { client: TypeSafeClient };
|
|
22
|
+
const hosts = new WeakMap<McpExtensionState, Host>();
|
|
23
|
+
const credentials = new WeakMap<McpExtensionState, Extract<JevCredentialResolution, { status: "present" }>>();
|
|
24
|
+
|
|
25
|
+
function resolveCredential(state: McpExtensionState): JevCredentialResolution {
|
|
26
|
+
const existing = credentials.get(state);
|
|
27
|
+
if (existing) return existing;
|
|
28
|
+
const credential = resolveJevCredential();
|
|
29
|
+
if (credential.status === "present") credentials.set(state, credential);
|
|
30
|
+
return credential;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function areJevSourcesAllowed(state: McpExtensionState, settings: ResolvedJevSettings, sources: Iterable<string>): boolean {
|
|
34
|
+
for (const source of sources) {
|
|
35
|
+
const server = state.config.mcpServers[source];
|
|
36
|
+
if (!settings.allowedServers.includes(source) || !server || isServerDisabled(server)) return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
43
|
+
const prototype = Object.getPrototypeOf(value);
|
|
44
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error(`${label} must be a plain object`);
|
|
45
|
+
return value as Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
function boolean(value: unknown, fallback: boolean, label: string): boolean {
|
|
48
|
+
if (value === undefined) return fallback;
|
|
49
|
+
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function integer(value: unknown, fallback: number, min: number, max: number, label: string): number {
|
|
53
|
+
if (value === undefined) return fallback;
|
|
54
|
+
if (!Number.isInteger(value) || (value as number) < min || (value as number) > max) throw new Error(`${label} must be an integer from ${min} to ${max}`);
|
|
55
|
+
return value as number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateJevSettings(value: unknown): ResolvedJevSettings {
|
|
59
|
+
if (value === undefined || value === false) return { ...DEFAULTS, allowedServers: [...DEFAULTS.allowedServers] };
|
|
60
|
+
const input = record(value, "settings.jev") as JevSettingsInput;
|
|
61
|
+
const allowed = new Set(Object.keys(DEFAULTS));
|
|
62
|
+
for (const key of Object.keys(input)) if (!allowed.has(key)) throw new Error(`settings.jev.${key} is not supported`);
|
|
63
|
+
if (input.allowedServers !== undefined && (!Array.isArray(input.allowedServers) || input.allowedServers.some(name => typeof name !== "string" || name.length === 0 || DANGEROUS_KEYS.has(name) || /[\u0000-\u001f\u007f]/.test(name)))) {
|
|
64
|
+
throw new Error("settings.jev.allowedServers must contain non-empty safe server names");
|
|
65
|
+
}
|
|
66
|
+
const model = input.model ?? DEFAULTS.model;
|
|
67
|
+
if (typeof model !== "string" || model.length === 0 || model.length > 128 || /(?:latest|preview)/i.test(model)) throw new Error("settings.jev.model must be a pinned model name");
|
|
68
|
+
const probability = input.semanticMinProbability ?? DEFAULTS.semanticMinProbability;
|
|
69
|
+
if (typeof probability !== "number" || !Number.isFinite(probability) || probability < 0 || probability > 1) throw new Error("settings.jev.semanticMinProbability must be from 0 to 1");
|
|
70
|
+
return {
|
|
71
|
+
semanticSearch: boolean(input.semanticSearch, DEFAULTS.semanticSearch, "settings.jev.semanticSearch"),
|
|
72
|
+
scriptEvaluation: boolean(input.scriptEvaluation, DEFAULTS.scriptEvaluation, "settings.jev.scriptEvaluation"),
|
|
73
|
+
allowedServers: [...new Set(input.allowedServers ?? DEFAULTS.allowedServers)], model,
|
|
74
|
+
requestTimeoutMs: integer(input.requestTimeoutMs, DEFAULTS.requestTimeoutMs, 100, 30_000, "settings.jev.requestTimeoutMs"),
|
|
75
|
+
maxRetries: integer(input.maxRetries, DEFAULTS.maxRetries, 0, 2, "settings.jev.maxRetries"),
|
|
76
|
+
maxStateBytes: integer(input.maxStateBytes, DEFAULTS.maxStateBytes, 1, 1_048_576, "settings.jev.maxStateBytes"),
|
|
77
|
+
maxQuestionsPerRequest: integer(input.maxQuestionsPerRequest, DEFAULTS.maxQuestionsPerRequest, 1, 128, "settings.jev.maxQuestionsPerRequest"),
|
|
78
|
+
maxEvaluationsPerScript: integer(input.maxEvaluationsPerScript, DEFAULTS.maxEvaluationsPerScript, 1, 32, "settings.jev.maxEvaluationsPerScript"),
|
|
79
|
+
maxEvaluationBytesPerScript: integer(input.maxEvaluationBytesPerScript, DEFAULTS.maxEvaluationBytesPerScript, 1, 4_194_304, "settings.jev.maxEvaluationBytesPerScript"),
|
|
80
|
+
maxEvaluationTokensPerScript: integer(input.maxEvaluationTokensPerScript, DEFAULTS.maxEvaluationTokensPerScript, 1, 1_000_000, "settings.jev.maxEvaluationTokensPerScript"),
|
|
81
|
+
semanticCandidateLimit: integer(input.semanticCandidateLimit, DEFAULTS.semanticCandidateLimit, 2, 127, "settings.jev.semanticCandidateLimit"),
|
|
82
|
+
semanticMinProbability: probability,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveSemanticJevSettings(
|
|
87
|
+
state: McpExtensionState,
|
|
88
|
+
credentialResolver?: () => JevCredentialResolution,
|
|
89
|
+
): ResolvedJevSettings {
|
|
90
|
+
const configured = state.config.settings?.jev;
|
|
91
|
+
const settings = validateJevSettings(configured);
|
|
92
|
+
if (configured === false || configured?.semanticSearch === false) return settings;
|
|
93
|
+
const semanticSearch = settings.semanticSearch || (credentialResolver ? credentialResolver() : resolveCredential(state)).status === "present";
|
|
94
|
+
const hasExplicitAllowlist = configured !== undefined && Object.hasOwn(configured, "allowedServers");
|
|
95
|
+
const allowedServers = hasExplicitAllowlist
|
|
96
|
+
? settings.allowedServers
|
|
97
|
+
: Object.keys(state.config.mcpServers).filter((name) => !isServerDisabled(state.config.mcpServers[name]));
|
|
98
|
+
return { ...settings, semanticSearch, allowedServers };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateJson(value: unknown, path: string, seen = new Set<object>()): asserts value is JevJson {
|
|
102
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
103
|
+
if (typeof value === "number") { if (Number.isFinite(value)) return; throw new Error(`${path} contains a non-finite number`); }
|
|
104
|
+
if (typeof value !== "object") throw new Error(`${path} is not JSON-compatible`);
|
|
105
|
+
if (seen.has(value)) throw new Error(`${path} contains a cycle`);
|
|
106
|
+
seen.add(value);
|
|
107
|
+
if (Array.isArray(value)) value.forEach((item, index) => validateJson(item, `${path}[${index}]`, seen));
|
|
108
|
+
else {
|
|
109
|
+
const prototype = Object.getPrototypeOf(value);
|
|
110
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error(`${path} contains a non-plain object`);
|
|
111
|
+
for (const [key, item] of Object.entries(value)) {
|
|
112
|
+
if (DANGEROUS_KEYS.has(key)) throw new Error(`${path} contains an unsafe key`);
|
|
113
|
+
validateJson(item, `${path}.${key}`, seen);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
seen.delete(value);
|
|
117
|
+
}
|
|
118
|
+
function validateId(id: string, label: string): void {
|
|
119
|
+
if (id.length === 0 || id.length > 128 || DANGEROUS_KEYS.has(id) || /[\u0000-\u001f\u007f]/.test(id)) throw new Error(`${label} contains an invalid identifier`);
|
|
120
|
+
}
|
|
121
|
+
function validateSource(source: string): void {
|
|
122
|
+
if (source.length === 0 || DANGEROUS_KEYS.has(source) || /[\u0000-\u001f\u007f]/.test(source)) throw new Error("sources contains an invalid server name");
|
|
123
|
+
}
|
|
124
|
+
function exactKeys(value: Record<string, unknown>, allowed: string[], label: string): void {
|
|
125
|
+
if (Object.keys(value).some(key => !allowed.includes(key))) throw new Error(`${label} contains unsupported fields`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function validateJevEvaluateInput(value: unknown, limits: ResolvedJevSettings): JevEvaluateInput {
|
|
129
|
+
const input = record(value, "Jev evaluation input");
|
|
130
|
+
exactKeys(input, ["state", "questions", "sources"], "Jev evaluation input");
|
|
131
|
+
if (!("state" in input)) throw new Error("Jev evaluation input requires state");
|
|
132
|
+
validateJson(input.state, "state");
|
|
133
|
+
const stateBytes = Buffer.byteLength(JSON.stringify(input.state));
|
|
134
|
+
if (stateBytes > limits.maxStateBytes) throw new Error("Jev state exceeds maxStateBytes");
|
|
135
|
+
const questions = record(input.questions, "questions");
|
|
136
|
+
const entries = Object.entries(questions);
|
|
137
|
+
if (entries.length < 1 || entries.length > limits.maxQuestionsPerRequest) throw new Error("Jev question count is outside the configured limit");
|
|
138
|
+
for (const [id, raw] of entries) {
|
|
139
|
+
validateId(id, "questions");
|
|
140
|
+
const question = record(raw, `question ${id}`);
|
|
141
|
+
if (question.type === "choice") {
|
|
142
|
+
exactKeys(question, ["type", "instructions", "criteria"], `question ${id}`);
|
|
143
|
+
const criteria = record(question.criteria, `question ${id} criteria`);
|
|
144
|
+
const labels = Object.keys(criteria);
|
|
145
|
+
if (labels.length < 2 || labels.length > 128) throw new Error(`question ${id} choice criteria must have 2 to 128 labels`);
|
|
146
|
+
for (const label of labels) { validateId(label, `question ${id} criteria`); validateJson(criteria[label], `question ${id} criteria.${label}`); }
|
|
147
|
+
} else if (question.type === "score") {
|
|
148
|
+
exactKeys(question, ["type", "instructions", "criteria"], `question ${id}`);
|
|
149
|
+
if (!Array.isArray(question.criteria) || question.criteria.length < 2 || question.criteria.length > 16) throw new Error(`question ${id} score criteria must have 2 to 16 entries`);
|
|
150
|
+
validateJson(question.criteria, `question ${id} criteria`);
|
|
151
|
+
} else if (question.type === "noul") {
|
|
152
|
+
exactKeys(question, ["type", "instructions", "criteria"], `question ${id}`);
|
|
153
|
+
if (question.criteria !== undefined) {
|
|
154
|
+
const criteria = record(question.criteria, `question ${id} criteria`);
|
|
155
|
+
exactKeys(criteria, ["true", "false"], `question ${id} criteria`);
|
|
156
|
+
validateJson(criteria, `question ${id} criteria`);
|
|
157
|
+
}
|
|
158
|
+
} else throw new Error(`question ${id} has an invalid type`);
|
|
159
|
+
if (question.instructions !== undefined) validateJson(question.instructions, `question ${id} instructions`);
|
|
160
|
+
}
|
|
161
|
+
let sources: string[] | undefined;
|
|
162
|
+
if (input.sources !== undefined) {
|
|
163
|
+
if (!Array.isArray(input.sources)) throw new Error("sources must be an array");
|
|
164
|
+
sources = input.sources.map((source, index) => { if (typeof source !== "string") throw new Error(`sources[${index}] must be a string`); validateSource(source); return source; });
|
|
165
|
+
if (new Set(sources).size !== sources.length) throw new Error("sources must not contain duplicates");
|
|
166
|
+
}
|
|
167
|
+
const normalized = { state: input.state, questions: questions as Record<string, JevQuestion>, ...(sources ? { sources } : {}) } as JevEvaluateInput;
|
|
168
|
+
if (Buffer.byteLength(JSON.stringify(normalized)) > limits.maxEvaluationBytesPerScript) throw new Error("Jev request exceeds the configured byte limit");
|
|
169
|
+
return normalized;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function safeNumber(value: unknown, label: string, integerOnly = false): number {
|
|
173
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || (integerOnly && !Number.isInteger(value))) throw new Error(`Invalid ${label}`);
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
function probability(value: unknown, label: string): number {
|
|
177
|
+
const number = safeNumber(value, label);
|
|
178
|
+
if (number > 1) throw new Error(`Invalid ${label}`);
|
|
179
|
+
return number;
|
|
180
|
+
}
|
|
181
|
+
function sameKeys(actual: Record<string, unknown>, expected: string[]): boolean {
|
|
182
|
+
const keys = Object.keys(actual);
|
|
183
|
+
return keys.length === expected.length && expected.every(key => Object.hasOwn(actual, key));
|
|
184
|
+
}
|
|
185
|
+
function validateResponse(value: unknown, input: JevEvaluateInput): JevEvaluationData {
|
|
186
|
+
const response = record(value, "response");
|
|
187
|
+
exactKeys(response, ["model", "answers", "usage"], "response");
|
|
188
|
+
const answers = record(response.answers, "response answers");
|
|
189
|
+
const questionNames = Object.keys(input.questions);
|
|
190
|
+
if (!sameKeys(answers, questionNames)) throw new Error("Response answer keys do not match questions");
|
|
191
|
+
const validated: Record<string, JevAnswer> = {};
|
|
192
|
+
for (const name of questionNames) {
|
|
193
|
+
const question = input.questions[name]!;
|
|
194
|
+
const answer = record(answers[name], `answer ${name}`);
|
|
195
|
+
if (answer.type !== question.type) throw new Error(`Answer ${name} type does not match`);
|
|
196
|
+
if (question.type === "choice") {
|
|
197
|
+
exactKeys(answer, ["type", "choice", "confidence", "probabilities"], `answer ${name}`);
|
|
198
|
+
const labels = Object.keys(question.criteria);
|
|
199
|
+
if (typeof answer.choice !== "string" || !Object.hasOwn(question.criteria, answer.choice)) throw new Error(`Answer ${name} choice is invalid`);
|
|
200
|
+
const probabilities = record(answer.probabilities, `answer ${name} probabilities`);
|
|
201
|
+
if (!sameKeys(probabilities, labels)) throw new Error(`Answer ${name} probability keys are invalid`);
|
|
202
|
+
validated[name] = { type: "choice", choice: answer.choice, confidence: probability(answer.confidence, "confidence"), probabilities: Object.fromEntries(labels.map(label => [label, probability(probabilities[label], "probability")])) };
|
|
203
|
+
} else if (question.type === "score") {
|
|
204
|
+
exactKeys(answer, ["type", "score", "confidence", "probabilities", "legend"], `answer ${name}`);
|
|
205
|
+
const labels = question.criteria.map((_, index) => String(index));
|
|
206
|
+
const probabilities = record(answer.probabilities, `answer ${name} probabilities`);
|
|
207
|
+
if (!sameKeys(probabilities, labels)) throw new Error(`Answer ${name} probability keys are invalid`);
|
|
208
|
+
const score = safeNumber(answer.score, "score");
|
|
209
|
+
if (score > question.criteria.length - 1) throw new Error(`Answer ${name} score is invalid`);
|
|
210
|
+
validated[name] = { type: "score", score, confidence: probability(answer.confidence, "confidence"), probabilities: Object.fromEntries(labels.map(label => [label, probability(probabilities[label], "probability")])) };
|
|
211
|
+
} else {
|
|
212
|
+
exactKeys(answer, ["type", "noul"], `answer ${name}`);
|
|
213
|
+
validated[name] = { type: "noul", noul: probability(answer.noul, "noul") };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (typeof response.model !== "string" || response.model.length === 0 || response.model.length > 128) throw new Error("Invalid response model");
|
|
217
|
+
const usage = record(response.usage, "response usage");
|
|
218
|
+
if (!sameKeys(usage, ["input_tokens", "output_tokens"])) throw new Error("Invalid response usage");
|
|
219
|
+
return { answers: validated, model: response.model, usage: { inputTokens: safeNumber(usage.input_tokens, "input tokens", true), outputTokens: safeNumber(usage.output_tokens, "output tokens", true) } };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function fixedOriginFetch(input: string, init?: RequestInit): Promise<Response> {
|
|
223
|
+
const url = new URL(input);
|
|
224
|
+
if (url.origin !== TYPESAFE_API_ORIGIN) throw new Error("TypeSafe request origin rejected");
|
|
225
|
+
return fetch(input, { ...init, redirect: "error" });
|
|
226
|
+
}
|
|
227
|
+
function getHost(state: McpExtensionState, settings: ResolvedJevSettings): Host | JevEvaluationEnvelope {
|
|
228
|
+
const existing = hosts.get(state);
|
|
229
|
+
if (existing) return existing;
|
|
230
|
+
const credential = resolveCredential(state);
|
|
231
|
+
if (credential.status === "missing") return { ok: false, error: { code: "credential_missing", message: "TypeSafe API key is not configured." } };
|
|
232
|
+
if (credential.status === "unavailable") return { ok: false, error: { code: "credential_unavailable", message: credential.message } };
|
|
233
|
+
const host = { client: new TypeSafeClient({ apiKey: credential.apiKey, baseURL: TYPESAFE_API_ORIGIN, defaultModel: settings.model, logLevel: "off", retry: { maxRetries: settings.maxRetries }, timeout: settings.requestTimeoutMs, defaultHeaders: {}, fetch: fixedOriginFetch }) };
|
|
234
|
+
hosts.set(state, host);
|
|
235
|
+
return host;
|
|
236
|
+
}
|
|
237
|
+
function failure(error: unknown, signal: AbortSignal | undefined, timedOut: boolean): JevEvaluationEnvelope {
|
|
238
|
+
if (timedOut) return { ok: false, error: { code: "timeout", message: "TypeSafe evaluation timed out.", retryable: true } };
|
|
239
|
+
if (signal?.aborted || (error instanceof Error && error.name === "APIUserAbortError")) return { ok: false, error: { code: "aborted", message: "TypeSafe evaluation was aborted." } };
|
|
240
|
+
const status = typeof error === "object" && error !== null && "status" in error ? (error as { status?: unknown }).status : undefined;
|
|
241
|
+
if (status === 401 || status === 403) return { ok: false, error: { code: "authentication_failed", message: "TypeSafe authentication failed." } };
|
|
242
|
+
if (status === 408) return { ok: false, error: { code: "timeout", message: "TypeSafe evaluation timed out.", retryable: true } };
|
|
243
|
+
if (status === 429) return { ok: false, error: { code: "rate_limited", message: "TypeSafe rate limit exceeded.", retryable: true } };
|
|
244
|
+
if (typeof status === "number" && status >= 500) return { ok: false, error: { code: "service_unavailable", message: "TypeSafe service is unavailable.", retryable: true } };
|
|
245
|
+
if (status === 400 || status === 422) return { ok: false, error: { code: "invalid_request", message: "TypeSafe rejected the evaluation request." } };
|
|
246
|
+
if (error instanceof Error && (error.name === "APIConnectionError" || error.name === "APITimeoutError")) return { ok: false, error: { code: "service_unavailable", message: "TypeSafe service is unavailable.", retryable: true } };
|
|
247
|
+
return { ok: false, error: { code: "invalid_response", message: "TypeSafe returned an invalid response." } };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function evaluateJev(state: McpExtensionState, value: JevEvaluateInput, options: { purpose: "script" | "semantic-search"; signal?: AbortSignal; budget?: JevBudget; observedSources?: readonly string[] }): Promise<JevEvaluationEnvelope> {
|
|
251
|
+
let settings: ResolvedJevSettings;
|
|
252
|
+
let input: JevEvaluateInput;
|
|
253
|
+
try { settings = options.purpose === "semantic-search" ? resolveSemanticJevSettings(state) : validateJevSettings(state.config.settings?.jev); input = validateJevEvaluateInput(value, settings); }
|
|
254
|
+
catch { return { ok: false, error: { code: "invalid_request", message: "Invalid TypeSafe evaluation request or settings." } }; }
|
|
255
|
+
if ((options.purpose === "script" && !settings.scriptEvaluation) || (options.purpose === "semantic-search" && !settings.semanticSearch)) return { ok: false, error: { code: "disabled", message: "TypeSafe evaluation is disabled." } };
|
|
256
|
+
const sources = new Set([...(input.sources ?? []), ...(options.observedSources ?? [])]);
|
|
257
|
+
if (!areJevSourcesAllowed(state, settings, sources)) return { ok: false, error: { code: "data_policy_denied", message: "Evaluation sources are not allowed by policy." } };
|
|
258
|
+
const bytes = Buffer.byteLength(JSON.stringify(input));
|
|
259
|
+
if (options.budget && !options.budget.consume(bytes)) return { ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation budget exhausted." } };
|
|
260
|
+
const resolved = getHost(state, settings);
|
|
261
|
+
if (!("client" in resolved)) return resolved;
|
|
262
|
+
const deadline = new AbortController();
|
|
263
|
+
let timedOut = false;
|
|
264
|
+
const timer = setTimeout(() => { timedOut = true; deadline.abort(new Error("TypeSafe evaluation deadline exceeded")); }, settings.requestTimeoutMs);
|
|
265
|
+
const signal = combineAbortSignals(options.signal, state.owner.signal, deadline.signal);
|
|
266
|
+
try {
|
|
267
|
+
const request = { state: input.state, questions: input.questions, model: settings.model };
|
|
268
|
+
const raw = await resolved.client.systemOne(request as Parameters<TypeSafeClient["systemOne"]>[0], { ...(signal ? { signal } : {}), timeout: settings.requestTimeoutMs, retry: { maxRetries: settings.maxRetries } });
|
|
269
|
+
try { return { ok: true, data: validateResponse(raw, input) }; }
|
|
270
|
+
catch { return { ok: false, error: { code: "invalid_response", message: "TypeSafe returned an invalid response." } }; }
|
|
271
|
+
} catch (error) { return failure(error, signal, timedOut); }
|
|
272
|
+
finally { clearTimeout(timer); }
|
|
273
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type JevJson = null | boolean | number | string | JevJson[] | { [key: string]: JevJson };
|
|
2
|
+
|
|
3
|
+
export type JevQuestion =
|
|
4
|
+
| { type: "choice"; instructions?: JevJson; criteria: Record<string, JevJson> }
|
|
5
|
+
| { type: "score"; instructions?: JevJson; criteria: [JevJson, JevJson, ...JevJson[]] }
|
|
6
|
+
| { type: "noul"; instructions?: JevJson; criteria?: { true?: JevJson; false?: JevJson } };
|
|
7
|
+
|
|
8
|
+
export interface JevEvaluateInput {
|
|
9
|
+
state: JevJson;
|
|
10
|
+
questions: Record<string, JevQuestion>;
|
|
11
|
+
/** MCP servers whose metadata/results were copied into state. Empty/omitted means no MCP-derived data. */
|
|
12
|
+
sources?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type JevAnswer =
|
|
16
|
+
| { type: "choice"; choice: string; confidence: number; probabilities: Record<string, number> }
|
|
17
|
+
| { type: "score"; score: number; confidence: number; probabilities: Record<string, number> }
|
|
18
|
+
| { type: "noul"; noul: number };
|
|
19
|
+
|
|
20
|
+
export interface JevEvaluationData {
|
|
21
|
+
answers: Record<string, JevAnswer>;
|
|
22
|
+
model: string;
|
|
23
|
+
usage: { inputTokens: number; outputTokens: number };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type JevErrorCode =
|
|
27
|
+
| "disabled" | "invalid_request" | "data_policy_denied" | "budget_exhausted"
|
|
28
|
+
| "credential_missing" | "credential_unavailable" | "authentication_failed"
|
|
29
|
+
| "timeout" | "aborted" | "rate_limited" | "service_unavailable" | "invalid_response";
|
|
30
|
+
|
|
31
|
+
export type JevEvaluationEnvelope =
|
|
32
|
+
| { ok: true; data: JevEvaluationData }
|
|
33
|
+
| { ok: false; error: { code: JevErrorCode; message: string; retryable?: boolean } };
|
|
34
|
+
|
|
35
|
+
export interface ResolvedJevSettings {
|
|
36
|
+
semanticSearch: boolean;
|
|
37
|
+
scriptEvaluation: boolean;
|
|
38
|
+
allowedServers: string[];
|
|
39
|
+
model: string;
|
|
40
|
+
requestTimeoutMs: number;
|
|
41
|
+
maxRetries: number;
|
|
42
|
+
maxStateBytes: number;
|
|
43
|
+
maxQuestionsPerRequest: number;
|
|
44
|
+
maxEvaluationsPerScript: number;
|
|
45
|
+
maxEvaluationBytesPerScript: number;
|
|
46
|
+
maxEvaluationTokensPerScript: number;
|
|
47
|
+
semanticCandidateLimit: number;
|
|
48
|
+
semanticMinProbability: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface JevBudget {
|
|
52
|
+
consume(bytes: number): boolean;
|
|
53
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createSecureKeyringStore, type SecureKeyringStore } from "./secure-keyring.ts";
|
|
2
|
+
|
|
3
|
+
export const TYPESAFE_API_ORIGIN = "https://api.typesafe.ai";
|
|
4
|
+
export const JEV_KEYRING_SERVICE = "pi-mcp-adapter.service-key";
|
|
5
|
+
export const JEV_KEYRING_ACCOUNT = "typesafe@sha256(https://api.typesafe.ai)";
|
|
6
|
+
|
|
7
|
+
interface StoredJevKey {
|
|
8
|
+
version: 1;
|
|
9
|
+
provider: "typesafe";
|
|
10
|
+
origin: typeof TYPESAFE_API_ORIGIN;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class JevCredentialStoreError extends Error {
|
|
15
|
+
readonly code = "JEV_CREDENTIAL_STORE_UNAVAILABLE";
|
|
16
|
+
constructor(readonly operation: "read" | "write" | "remove", cause: unknown) {
|
|
17
|
+
super(`TypeSafe API key secure credential store unavailable during ${operation}. Configure or unlock the OS credential store and retry.`, { cause });
|
|
18
|
+
this.name = "JevCredentialStoreError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type JevCredentialResolution =
|
|
23
|
+
| { status: "present"; source: "environment" | "keyring"; apiKey: string }
|
|
24
|
+
| { status: "missing" }
|
|
25
|
+
| { status: "unavailable"; message: string };
|
|
26
|
+
|
|
27
|
+
function validateApiKey(value: unknown): string {
|
|
28
|
+
if (typeof value !== "string" || value.trim().length === 0 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
29
|
+
throw new Error("TypeSafe API key must be a non-empty string without control characters");
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function store(): SecureKeyringStore {
|
|
35
|
+
return createSecureKeyringStore(JEV_KEYRING_SERVICE);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readStoredKey(secretStore: SecureKeyringStore = store()): string | undefined {
|
|
39
|
+
let payload: string | undefined;
|
|
40
|
+
try { payload = secretStore.read(JEV_KEYRING_ACCOUNT); }
|
|
41
|
+
catch (error) { throw new JevCredentialStoreError("read", error); }
|
|
42
|
+
if (payload === undefined) return undefined;
|
|
43
|
+
try {
|
|
44
|
+
const value: unknown = JSON.parse(payload);
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid record");
|
|
46
|
+
const record = value as Partial<StoredJevKey>;
|
|
47
|
+
const keys = Object.keys(record);
|
|
48
|
+
if (keys.length !== 4 || !["version", "provider", "origin", "apiKey"].every(key => Object.hasOwn(record, key))) throw new Error("invalid record fields");
|
|
49
|
+
if (record.version !== 1 || record.provider !== "typesafe" || record.origin !== TYPESAFE_API_ORIGIN) throw new Error("invalid or mismatched record");
|
|
50
|
+
return validateApiKey(record.apiKey);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new JevCredentialStoreError("read", error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveJevCredential(env: NodeJS.ProcessEnv = process.env, secretStore?: SecureKeyringStore): JevCredentialResolution {
|
|
57
|
+
if (Object.hasOwn(env, "TYPESAFE_API_KEY")) {
|
|
58
|
+
try { return { status: "present", source: "environment", apiKey: validateApiKey(env.TYPESAFE_API_KEY) }; }
|
|
59
|
+
catch { return { status: "unavailable", message: "TYPESAFE_API_KEY is present but invalid." }; }
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const apiKey = readStoredKey(secretStore ?? store());
|
|
63
|
+
return apiKey === undefined ? { status: "missing" } : { status: "present", source: "keyring", apiKey };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (!(error instanceof JevCredentialStoreError)) throw error;
|
|
66
|
+
return { status: "unavailable", message: error.message };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveJevApiKey(apiKey: string, secretStore: SecureKeyringStore = store()): void {
|
|
71
|
+
const record: StoredJevKey = { version: 1, provider: "typesafe", origin: TYPESAFE_API_ORIGIN, apiKey: validateApiKey(apiKey) };
|
|
72
|
+
try { secretStore.write(JEV_KEYRING_ACCOUNT, JSON.stringify(record)); }
|
|
73
|
+
catch (error) { throw new JevCredentialStoreError("write", error); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function removeJevApiKey(secretStore: SecureKeyringStore = store()): void {
|
|
77
|
+
try { secretStore.remove(JEV_KEYRING_ACCOUNT); }
|
|
78
|
+
catch (error) { throw new JevCredentialStoreError("remove", error); }
|
|
79
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
|
|
2
2
|
import { isServerDisabled, type ServerDefinition } from "./types.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isTransientHttpConnectError,
|
|
5
|
+
isUnauthorizedHttpError,
|
|
6
|
+
type McpServerManager,
|
|
7
|
+
type ServerConnection,
|
|
8
|
+
} from "./server-manager.ts";
|
|
4
9
|
import { hasPendingAuth } from "./mcp-auth-flow.ts";
|
|
5
10
|
import { logger } from "./logger.ts";
|
|
6
11
|
import { formatTerminalError, parallelLimit, sanitizeTerminalText } from "./utils.ts";
|
|
@@ -223,7 +228,7 @@ export class McpLifecycleManager {
|
|
|
223
228
|
await this.handleSupersededConnection(name, definition, connection, signal, retrySuperseded);
|
|
224
229
|
return;
|
|
225
230
|
}
|
|
226
|
-
if (!shouldReconnectAfterRefresh(error, hadSessionId)) {
|
|
231
|
+
if (!shouldReconnectAfterRefresh(error, hadSessionId, definition)) {
|
|
227
232
|
this.reportConnectionFailure(name, definition, error, "refresh", connection);
|
|
228
233
|
return;
|
|
229
234
|
}
|
|
@@ -439,8 +444,14 @@ export class McpLifecycleManager {
|
|
|
439
444
|
}
|
|
440
445
|
}
|
|
441
446
|
|
|
442
|
-
function shouldReconnectAfterRefresh(
|
|
447
|
+
function shouldReconnectAfterRefresh(
|
|
448
|
+
error: unknown,
|
|
449
|
+
hadSessionId: boolean,
|
|
450
|
+
definition: ServerDefinition,
|
|
451
|
+
): boolean {
|
|
443
452
|
if (isTerminatedSession(error, hadSessionId)) return true;
|
|
453
|
+
// Reconnect baked bearer sources so connect-time resolution runs again.
|
|
454
|
+
if (definition.auth === "bearer" && isUnauthorizedHttpError(error)) return true;
|
|
444
455
|
return error instanceof SdkError
|
|
445
456
|
&& (error.code === SdkErrorCode.NotConnected || error.code === SdkErrorCode.ConnectionClosed);
|
|
446
457
|
}
|
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
|
|
5
|
-
type KeyringEntry = {
|
|
6
|
-
getPassword(): string | null;
|
|
7
|
-
setPassword(password: string): void;
|
|
8
|
-
deleteCredential(): boolean;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
type KeyringEntryConstructor = new (service: string, account: string) => KeyringEntry;
|
|
12
|
-
type KeyringModule = { Entry: KeyringEntryConstructor };
|
|
13
|
-
type KeyringRequire = ((id: string) => unknown) & { resolve(id: string): string };
|
|
2
|
+
import { createSecureKeyringStore, getTestSecureKeyringEntries, getTestSecureKeyringReadCount, removeTestSecureKeyringEntry, resetTestSecureKeyring, setTestSecureKeyringEntry } from "./secure-keyring.ts";
|
|
14
3
|
|
|
15
4
|
type BearerSecretStore = {
|
|
16
5
|
read(account: string): string | undefined;
|
|
@@ -29,15 +18,8 @@ interface BearerChunkManifest {
|
|
|
29
18
|
chunkDigest: string;
|
|
30
19
|
}
|
|
31
20
|
|
|
32
|
-
const require = createRequire(import.meta.url);
|
|
33
21
|
const BEARER_SECRET_SERVICE = "pi-mcp-adapter.bearer";
|
|
34
|
-
const TEST_AUTH_STORE_ENV = "PI_MCP_ADAPTER_TEST_AUTH_STORE";
|
|
35
22
|
const BEARER_SECRET_CHUNK_SIZE = 1000;
|
|
36
|
-
const BEARER_SECRET_VALUE_LIMIT = 1280;
|
|
37
|
-
|
|
38
|
-
let KeyringEntryClass: KeyringEntryConstructor | undefined;
|
|
39
|
-
const memoryBearerEntries = new Map<string, string>();
|
|
40
|
-
let testBearerSecretStoreReadCount = 0;
|
|
41
23
|
|
|
42
24
|
export class BearerCredentialStoreError extends Error {
|
|
43
25
|
readonly code = "BEARER_CREDENTIAL_STORE_UNAVAILABLE";
|
|
@@ -56,124 +38,8 @@ export type BearerCredentialStatus =
|
|
|
56
38
|
| { status: "url-mismatch" }
|
|
57
39
|
| { status: "unavailable"; message: string };
|
|
58
40
|
|
|
59
|
-
const memoryBearerSecretStore: BearerSecretStore = {
|
|
60
|
-
read(account) {
|
|
61
|
-
testBearerSecretStoreReadCount++;
|
|
62
|
-
return memoryBearerEntries.get(account);
|
|
63
|
-
},
|
|
64
|
-
write(account, payload) {
|
|
65
|
-
memoryBearerEntries.set(account, payload);
|
|
66
|
-
},
|
|
67
|
-
remove(account) {
|
|
68
|
-
memoryBearerEntries.delete(account);
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const sizeLimitedBearerSecretStore: BearerSecretStore = {
|
|
73
|
-
read(account) {
|
|
74
|
-
testBearerSecretStoreReadCount++;
|
|
75
|
-
return memoryBearerEntries.get(account);
|
|
76
|
-
},
|
|
77
|
-
write(account, payload) {
|
|
78
|
-
if (payload.length > BEARER_SECRET_VALUE_LIMIT) {
|
|
79
|
-
throw new Error(`Value of 'password encoded as UTF-16' is longer than the platform limit of ${BEARER_SECRET_VALUE_LIMIT * 2} chars`);
|
|
80
|
-
}
|
|
81
|
-
memoryBearerEntries.set(account, payload);
|
|
82
|
-
},
|
|
83
|
-
remove(account) {
|
|
84
|
-
memoryBearerEntries.delete(account);
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const unavailableBearerSecretStore: BearerSecretStore = {
|
|
89
|
-
read() {
|
|
90
|
-
testBearerSecretStoreReadCount++;
|
|
91
|
-
throw new Error("simulated secure credential store unavailable");
|
|
92
|
-
},
|
|
93
|
-
write() {
|
|
94
|
-
throw new Error("simulated secure credential store unavailable");
|
|
95
|
-
},
|
|
96
|
-
remove() {
|
|
97
|
-
throw new Error("simulated secure credential store unavailable");
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const keyringBearerSecretStore: BearerSecretStore = {
|
|
102
|
-
read(account) {
|
|
103
|
-
return getKeyringEntry(account).getPassword() ?? undefined;
|
|
104
|
-
},
|
|
105
|
-
write(account, payload) {
|
|
106
|
-
getKeyringEntry(account).setPassword(payload);
|
|
107
|
-
},
|
|
108
|
-
remove(account) {
|
|
109
|
-
getKeyringEntry(account).deleteCredential();
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
|
|
113
41
|
function getBearerSecretStore(): BearerSecretStore {
|
|
114
|
-
|
|
115
|
-
if (process.env[TEST_AUTH_STORE_ENV] === "sizelimited") return sizeLimitedBearerSecretStore;
|
|
116
|
-
if (process.env[TEST_AUTH_STORE_ENV] === "unavailable") return unavailableBearerSecretStore;
|
|
117
|
-
return keyringBearerSecretStore;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getKeyringEntry(account: string): KeyringEntry {
|
|
121
|
-
try {
|
|
122
|
-
KeyringEntryClass ??= loadKeyringEntryClass();
|
|
123
|
-
return new KeyringEntryClass(BEARER_SECRET_SERVICE, account);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
throw new Error("Bearer token secure credential storage is unavailable. Configure the OS credential store and retry.", { cause: error });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function loadKeyringEntryClass(keyringRequire: KeyringRequire = require, platform: NodeJS.Platform = process.platform, arch: NodeJS.Architecture = process.arch): KeyringEntryConstructor {
|
|
130
|
-
try {
|
|
131
|
-
return (keyringRequire("@napi-rs/keyring") as KeyringModule).Entry;
|
|
132
|
-
} catch (loaderError) {
|
|
133
|
-
try {
|
|
134
|
-
return loadKeyringNativeBindingFallback(keyringRequire, platform, arch).Entry;
|
|
135
|
-
} catch (fallbackError) {
|
|
136
|
-
const message = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
137
|
-
throw new Error(`Failed to load @napi-rs/keyring; absolute-path native binding fallback also failed: ${message}`, { cause: loaderError });
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function loadKeyringNativeBindingFallback(keyringRequire: KeyringRequire, platform: NodeJS.Platform, arch: NodeJS.Architecture): KeyringModule {
|
|
143
|
-
const targets = getKeyringNativeBindingSuffixes(platform, arch).map(suffix => ({
|
|
144
|
-
packageName: `@napi-rs/keyring-${suffix}`,
|
|
145
|
-
bindingFile: `keyring.${suffix}.node`,
|
|
146
|
-
}));
|
|
147
|
-
let lastError: unknown;
|
|
148
|
-
for (const target of targets) {
|
|
149
|
-
try {
|
|
150
|
-
const packageJsonPath = keyringRequire.resolve(`${target.packageName}/package.json`);
|
|
151
|
-
return keyringRequire(join(dirname(packageJsonPath), target.bindingFile)) as KeyringModule;
|
|
152
|
-
} catch (error) {
|
|
153
|
-
lastError = error;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function getKeyringNativeBindingSuffixes(platform: NodeJS.Platform, arch: NodeJS.Architecture): string[] {
|
|
160
|
-
if (platform === "darwin") {
|
|
161
|
-
if (arch === "arm64") return ["darwin-arm64"];
|
|
162
|
-
if (arch === "x64") return ["darwin-x64"];
|
|
163
|
-
}
|
|
164
|
-
if (platform === "win32") {
|
|
165
|
-
if (arch === "arm64") return ["win32-arm64-msvc"];
|
|
166
|
-
if (arch === "x64") return ["win32-x64-msvc"];
|
|
167
|
-
if (arch === "ia32") return ["win32-ia32-msvc"];
|
|
168
|
-
}
|
|
169
|
-
if (platform === "linux") {
|
|
170
|
-
if (arch === "arm64") return ["linux-arm64-gnu", "linux-arm64-musl"];
|
|
171
|
-
if (arch === "arm") return ["linux-arm-gnueabihf"];
|
|
172
|
-
if (arch === "riscv64") return ["linux-riscv64-gnu"];
|
|
173
|
-
if (arch === "x64") return ["linux-x64-gnu", "linux-x64-musl"];
|
|
174
|
-
}
|
|
175
|
-
if (platform === "freebsd" && arch === "x64") return ["freebsd-x64"];
|
|
176
|
-
return [];
|
|
42
|
+
return createSecureKeyringStore(BEARER_SECRET_SERVICE);
|
|
177
43
|
}
|
|
178
44
|
|
|
179
45
|
function getBearerAccount(serverName: string): string {
|
|
@@ -364,22 +230,21 @@ export function inspectBearerTokenForUrl(serverName: string, serverUrl: string):
|
|
|
364
230
|
}
|
|
365
231
|
|
|
366
232
|
export function resetTestBearerTokenStore(): void {
|
|
367
|
-
|
|
368
|
-
testBearerSecretStoreReadCount = 0;
|
|
233
|
+
resetTestSecureKeyring();
|
|
369
234
|
}
|
|
370
235
|
|
|
371
236
|
export function getTestBearerTokenStoreEntries(): [string, string][] {
|
|
372
|
-
return [
|
|
237
|
+
return getTestSecureKeyringEntries().filter(([key]) => key.startsWith(`${BEARER_SECRET_SERVICE}\0`)).map(([key, value]) => [key.slice(BEARER_SECRET_SERVICE.length + 1), value]);
|
|
373
238
|
}
|
|
374
239
|
|
|
375
240
|
export function removeTestBearerTokenStoreEntry(account: string): void {
|
|
376
|
-
|
|
241
|
+
removeTestSecureKeyringEntry(BEARER_SECRET_SERVICE, account);
|
|
377
242
|
}
|
|
378
243
|
|
|
379
244
|
export function setTestBearerTokenStoreEntry(account: string, payload: string): void {
|
|
380
|
-
|
|
245
|
+
setTestSecureKeyringEntry(BEARER_SECRET_SERVICE, account, payload);
|
|
381
246
|
}
|
|
382
247
|
|
|
383
248
|
export function getTestBearerTokenStoreReadCount(): number {
|
|
384
|
-
return
|
|
249
|
+
return getTestSecureKeyringReadCount();
|
|
385
250
|
}
|