@caplets/core 0.16.0 → 0.17.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.
@@ -1,16 +1,15 @@
1
- export type NativeCapletsMode = "auto" | "local" | "remote";
1
+ import { type CapletsMode, type CapletsServerEnv, type CapletsServerInput } from "../server/options";
2
+ export type NativeCapletsMode = CapletsMode;
2
3
  export type NativeRemoteCapletsOptions = {
3
- url?: string;
4
- user?: string;
5
- password?: string;
6
4
  pollIntervalMs?: number;
7
5
  fetch?: typeof fetch;
8
6
  };
9
7
  export type NativeCapletsServiceResolutionInput = {
10
8
  mode?: NativeCapletsMode;
9
+ server?: CapletsServerInput;
11
10
  remote?: NativeRemoteCapletsOptions;
12
11
  };
13
- export type NativeCapletsEnv = Partial<Record<"CAPLETS_NATIVE_MODE" | "CAPLETS_REMOTE_URL" | "CAPLETS_REMOTE_USER" | "CAPLETS_REMOTE_PASSWORD", string>>;
12
+ export type NativeCapletsEnv = CapletsServerEnv;
14
13
  export type NativeRemoteAuthOptions = {
15
14
  enabled: false;
16
15
  user: string;
package/dist/native.js CHANGED
@@ -1,76 +1,33 @@
1
- import { K as ToolListChangedNotificationSchema, Tt as CapletsError, a as capabilityDescription, d as StreamableHTTPClientTransport, m as Client, n as generatedToolInputSchema, t as CapletsEngine } from "./engine-Brwid_mq.js";
1
+ import { $ as ToolListChangedNotificationSchema, Nt as CapletsError, a as resolveCapletsServer, i as resolveCapletsMode, n as mcpUrlForBase, o as CapletsEngine, s as generatedToolInputSchema, u as capabilityDescription, v as StreamableHTTPClientTransport, x as Client } from "./options-CJEOqS87.js";
2
2
  import { generatedToolInputJsonSchema } from "./generated-tool-input-schema.js";
3
- import { Buffer } from "node:buffer";
4
3
  //#region src/native/options.ts
5
- const DEFAULT_REMOTE_USER = "caplets";
6
4
  const DEFAULT_POLL_INTERVAL_MS = 3e4;
7
5
  function resolveNativeCapletsServiceOptions(input = {}, env = process.env) {
8
- const mode = parseMode(input.mode ?? env.CAPLETS_NATIVE_MODE ?? "auto");
9
- if (mode === "local") return { mode: "local" };
10
- const rawUrl = nonEmpty(input.remote?.url, "remote.url") ?? nonEmpty(env.CAPLETS_REMOTE_URL, "CAPLETS_REMOTE_URL");
11
- if (mode === "remote" && rawUrl === void 0) throw new CapletsError("REQUEST_INVALID", "CAPLETS_NATIVE_MODE=remote requires CAPLETS_REMOTE_URL or remote.url.");
12
- if (rawUrl === void 0) return { mode: "local" };
13
- const url = parseRemoteUrl(rawUrl);
14
- const userWasExplicit = input.remote?.user !== void 0 || hasEnv(env.CAPLETS_REMOTE_USER);
15
- const user = nonEmpty(input.remote?.user, "remote.user") ?? nonEmpty(env.CAPLETS_REMOTE_USER, "CAPLETS_REMOTE_USER") ?? DEFAULT_REMOTE_USER;
16
- const password = nonEmpty(input.remote?.password, "remote.password") ?? nonEmpty(env.CAPLETS_REMOTE_PASSWORD, "CAPLETS_REMOTE_PASSWORD");
17
- if (userWasExplicit && password === void 0) throw new CapletsError("REQUEST_INVALID", "Remote Caplets Basic Auth requires a password; set CAPLETS_REMOTE_PASSWORD or remote.password.");
18
- const auth = password === void 0 ? {
19
- enabled: false,
20
- user
21
- } : {
22
- enabled: true,
23
- user,
24
- password
25
- };
26
- const requestInit = auth.enabled ? { headers: { Authorization: basicAuthHeader(auth.user, auth.password) } } : {};
6
+ if (resolveCapletsMode({
7
+ ...input.mode ? { mode: input.mode } : {},
8
+ ...input.server?.url ? { serverUrl: input.server.url } : {}
9
+ }, env).mode === "local") return { mode: "local" };
10
+ const serverFetch = input.remote?.fetch ?? input.server?.fetch;
11
+ const server = resolveCapletsServer({
12
+ ...input.server,
13
+ ...serverFetch ? { fetch: serverFetch } : {}
14
+ }, env);
27
15
  return {
28
16
  mode: "remote",
29
17
  remote: {
30
- url,
31
- auth,
18
+ url: mcpUrlForBase(server.baseUrl),
19
+ auth: server.auth,
32
20
  pollIntervalMs: parsePollInterval(input.remote?.pollIntervalMs),
33
- requestInit,
34
- ...input.remote?.fetch ? { fetch: input.remote.fetch } : {}
21
+ requestInit: server.requestInit,
22
+ ...server.fetch ? { fetch: server.fetch } : {}
35
23
  }
36
24
  };
37
25
  }
38
- function parseMode(value) {
39
- if (value === "auto" || value === "local" || value === "remote") return value;
40
- throw new CapletsError("REQUEST_INVALID", `Expected CAPLETS_NATIVE_MODE to be auto, local, or remote, got ${value}`);
41
- }
42
- function parseRemoteUrl(value) {
43
- let url;
44
- try {
45
- url = new URL(value);
46
- } catch {
47
- throw new CapletsError("REQUEST_INVALID", "Invalid remote Caplets URL.");
48
- }
49
- if (url.username !== "" || url.password !== "") throw new CapletsError("REQUEST_INVALID", "Remote Caplets URL must not include username or password; use CAPLETS_REMOTE_USER/CAPLETS_REMOTE_PASSWORD or remote.user/remote.password instead.");
50
- if (url.protocol === "https:") return url;
51
- if (url.protocol === "http:" && isLoopbackHost(url.hostname)) return url;
52
- throw new CapletsError("REQUEST_INVALID", "Remote Caplets URL must use https except loopback development URLs.");
53
- }
54
- function isLoopbackHost(host) {
55
- return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
56
- }
57
26
  function parsePollInterval(value) {
58
27
  if (value === void 0) return DEFAULT_POLL_INTERVAL_MS;
59
28
  if (!Number.isInteger(value) || value < 1e3) throw new CapletsError("REQUEST_INVALID", "remote.pollIntervalMs must be an integer >= 1000.");
60
29
  return value;
61
30
  }
62
- function basicAuthHeader(user, password) {
63
- return `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`;
64
- }
65
- function nonEmpty(value, label) {
66
- if (value === void 0) return;
67
- const trimmed = value.trim();
68
- if (!trimmed) throw new CapletsError("REQUEST_INVALID", `${label} must not be empty`);
69
- return trimmed;
70
- }
71
- function hasEnv(value) {
72
- return value !== void 0 && value.trim() !== "";
73
- }
74
31
  //#endregion
75
32
  //#region src/native/tools.ts
76
33
  function nativeCapletToolName(capletId) {
@@ -281,7 +238,7 @@ function errorMessage(error) {
281
238
  return error instanceof Error ? error.message : String(error);
282
239
  }
283
240
  function remoteAuthError() {
284
- return new CapletsError("AUTH_FAILED", "Remote Caplets authentication failed; check CAPLETS_REMOTE_USER and CAPLETS_REMOTE_PASSWORD.");
241
+ return new CapletsError("AUTH_FAILED", "Remote Caplets authentication failed; check CAPLETS_SERVER_USER and CAPLETS_SERVER_PASSWORD.");
285
242
  }
286
243
  function isSessionFailure(error) {
287
244
  const message = errorMessage(error).toLowerCase();
@@ -9,6 +9,7 @@ import { homedir } from "node:os";
9
9
  import { PassThrough } from "node:stream";
10
10
  import { createServer } from "node:http";
11
11
  import { createHash, randomBytes } from "node:crypto";
12
+ import { Buffer as Buffer$1 } from "node:buffer";
12
13
  //#region \0rolldown/runtime.js
13
14
  var __create = Object.create;
14
15
  var __defProp = Object.defineProperty;
@@ -34,6 +35,25 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
34
35
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
35
36
  //#endregion
36
37
  //#region src/errors.ts
38
+ const CAPLETS_ERROR_CODES = [
39
+ "CONFIG_NOT_FOUND",
40
+ "CONFIG_EXISTS",
41
+ "CONFIG_INVALID",
42
+ "REQUEST_INVALID",
43
+ "SERVER_NOT_FOUND",
44
+ "SERVER_UNAVAILABLE",
45
+ "SERVER_START_TIMEOUT",
46
+ "UNKNOWN_OPERATION",
47
+ "TOOL_NOT_FOUND",
48
+ "TOOL_CALL_TIMEOUT",
49
+ "AUTH_REQUIRED",
50
+ "AUTH_FAILED",
51
+ "AUTH_REFRESH_FAILED",
52
+ "DOWNSTREAM_PROTOCOL_ERROR",
53
+ "DOWNSTREAM_TOOL_ERROR",
54
+ "UNSUPPORTED_TRANSPORT",
55
+ "INTERNAL_ERROR"
56
+ ];
37
57
  var CapletsError = class extends Error {
38
58
  code;
39
59
  details;
@@ -31189,8 +31209,45 @@ var FileOAuthProvider = class {
31189
31209
  headers.set("content-type", "application/x-www-form-urlencoded");
31190
31210
  };
31191
31211
  };
31192
- async function runOAuthFlow(server, options = {}) {
31212
+ async function startOAuthFlow(server, options) {
31193
31213
  if (server.transport === "stdio" || !server.url || server.auth?.type !== "oauth2" && server.auth?.type !== "oidc") throw new CapletsError("REQUEST_INVALID", `${server.server} is not a configured OAuth remote server`);
31214
+ let redirectUrl;
31215
+ const provider = new FileOAuthProvider(server, options.redirectUri, (url) => {
31216
+ redirectUrl = url;
31217
+ options.print?.(`Open this URL to authorize ${server.server}:\n${url.toString()}`);
31218
+ }, options.authDir);
31219
+ const scope = scopesFor(server.auth);
31220
+ try {
31221
+ if (await auth(provider, {
31222
+ serverUrl: server.url,
31223
+ ...scope ? { scope } : {}
31224
+ }) === "AUTHORIZED") return {
31225
+ authorizationUrl: "",
31226
+ complete: async () => {}
31227
+ };
31228
+ } catch (error) {
31229
+ throw normalizeMcpOAuthError(server, error);
31230
+ }
31231
+ if (!redirectUrl) throw new CapletsError("AUTH_FAILED", "OAuth authorization URL was not provided");
31232
+ return {
31233
+ authorizationUrl: redirectUrl.toString(),
31234
+ complete: async (callbackUrl) => {
31235
+ assertNoOAuthCallbackError(server, callbackUrl);
31236
+ const completion = extractCompletion(callbackUrl);
31237
+ if (completion.state !== provider.state()) throw new CapletsError("AUTH_FAILED", "OAuth callback state did not match");
31238
+ try {
31239
+ await auth(provider, {
31240
+ serverUrl: server.url,
31241
+ authorizationCode: completion.code,
31242
+ ...scope ? { scope } : {}
31243
+ });
31244
+ } catch (error) {
31245
+ throw normalizeMcpOAuthError(server, error);
31246
+ }
31247
+ }
31248
+ };
31249
+ }
31250
+ async function runOAuthFlow(server, options = {}) {
31194
31251
  let callbackCode;
31195
31252
  let callbackState;
31196
31253
  const callback = await createLoopbackCallback((url) => {
@@ -31198,37 +31255,43 @@ async function runOAuthFlow(server, options = {}) {
31198
31255
  callbackCode = url.searchParams.get("code") ?? void 0;
31199
31256
  callbackState = url.searchParams.get("state") ?? void 0;
31200
31257
  });
31201
- let redirectUrl;
31202
- const provider = new FileOAuthProvider(server, callback.redirectUri, (url) => {
31203
- redirectUrl = url;
31204
- options.print?.(`Open this URL to authorize ${server.server}:\n${url.toString()}`);
31205
- }, options.authDir);
31206
31258
  try {
31207
- const scope = scopesFor(server.auth);
31208
- const first = await auth(provider, {
31209
- serverUrl: server.url,
31210
- ...scope ? { scope } : {}
31259
+ const started = await startOAuthFlow(server, {
31260
+ redirectUri: callback.redirectUri,
31261
+ ...options.authDir ? { authDir: options.authDir } : {},
31262
+ ...options.print ? { print: options.print } : {}
31211
31263
  });
31212
- if (first === "AUTHORIZED") return first;
31213
- if (!options.noOpen && redirectUrl) await (options.open ? options.open(redirectUrl.toString()) : openBrowser(redirectUrl.toString()));
31264
+ if (!started.authorizationUrl) return "AUTHORIZED";
31265
+ if (!options.noOpen) await (options.open ? options.open(started.authorizationUrl) : openBrowser(started.authorizationUrl));
31214
31266
  const manualInput = options.manualInput ?? (options.noOpen ? await options.readManualInput?.() : void 0);
31215
31267
  const completion = manualInput ? extractCompletion(manualInput) : await callback.waitForCode(() => callbackCode ? {
31216
31268
  code: callbackCode,
31217
31269
  ...callbackState ? { state: callbackState } : {}
31218
31270
  } : void 0);
31219
- const expectedState = provider.state();
31220
- if (completion.state !== expectedState) throw new CapletsError("AUTH_FAILED", "OAuth callback state did not match");
31221
- return await auth(provider, {
31222
- serverUrl: server.url,
31223
- authorizationCode: completion.code,
31224
- ...scope ? { scope } : {}
31225
- });
31271
+ await started.complete(completion.state ? `${callback.redirectUri}?code=${encodeURIComponent(completion.code)}&state=${encodeURIComponent(completion.state)}` : `${callback.redirectUri}?code=${encodeURIComponent(completion.code)}`);
31272
+ return "AUTHORIZED";
31226
31273
  } catch (error) {
31227
31274
  throw normalizeMcpOAuthError(server, error);
31228
31275
  } finally {
31229
31276
  await callback.close();
31230
31277
  }
31231
31278
  }
31279
+ function assertNoOAuthCallbackError(target, callbackUrl) {
31280
+ let url;
31281
+ try {
31282
+ url = new URL(callbackUrl);
31283
+ } catch {
31284
+ return;
31285
+ }
31286
+ const error = url.searchParams.get("error");
31287
+ if (!error) return;
31288
+ const description = url.searchParams.get("error_description");
31289
+ throw new CapletsError("AUTH_FAILED", description ? `OAuth provider returned an error: ${description}` : "OAuth provider returned an error", redactSecrets({
31290
+ server: target.server,
31291
+ error,
31292
+ error_description: description ?? void 0
31293
+ }));
31294
+ }
31232
31295
  function normalizeMcpOAuthError(server, error) {
31233
31296
  if ((server.auth?.type === "oauth2" || server.auth?.type === "oidc") && !server.auth.clientId && !server.auth.clientMetadataUrl && error instanceof Error && error.message.includes("does not support dynamic client registration")) return new CapletsError("AUTH_FAILED", "OAuth is not available for this server without a host-specific OAuth app or PAT auth", {
31234
31297
  server: server.server,
@@ -31236,6 +31299,75 @@ function normalizeMcpOAuthError(server, error) {
31236
31299
  });
31237
31300
  return error;
31238
31301
  }
31302
+ async function startGenericOAuthFlow(target, options) {
31303
+ if (target.auth?.type !== "oauth2" && target.auth?.type !== "oidc") throw new CapletsError("REQUEST_INVALID", `${target.server} is not configured for OAuth`);
31304
+ const authConfig = target.auth;
31305
+ const redirectUri = authConfig.redirectUri ?? options.redirectUri;
31306
+ const verifier = base64url(randomBytes(32));
31307
+ const state = base64url(randomBytes(24));
31308
+ const allowLoopbackHttp = isLoopbackDevelopmentTarget(target, authConfig);
31309
+ const metadata = await discoverAuthorizationServer(target, authConfig, allowLoopbackHttp);
31310
+ const authorizationEndpoint = authConfig.authorizationUrl ?? metadata.authorization_endpoint;
31311
+ const tokenEndpoint = authConfig.tokenUrl ?? metadata.token_endpoint;
31312
+ if (!authorizationEndpoint || !tokenEndpoint) throw new CapletsError("AUTH_FAILED", "OAuth metadata is missing endpoints", { server: target.server });
31313
+ assertAllowedAuthUrl(authorizationEndpoint, "authorization endpoint", allowLoopbackHttp);
31314
+ assertAllowedAuthUrl(tokenEndpoint, "token endpoint", allowLoopbackHttp);
31315
+ const client = await resolveGenericClient(target, authConfig, metadata, redirectUri, allowLoopbackHttp);
31316
+ const scope = scopesFor(authConfig);
31317
+ const authorizationUrl = new URL(authorizationEndpoint);
31318
+ authorizationUrl.searchParams.set("response_type", "code");
31319
+ authorizationUrl.searchParams.set("client_id", client.clientId);
31320
+ authorizationUrl.searchParams.set("redirect_uri", redirectUri);
31321
+ authorizationUrl.searchParams.set("code_challenge", pkceChallenge(verifier));
31322
+ authorizationUrl.searchParams.set("code_challenge_method", "S256");
31323
+ authorizationUrl.searchParams.set("state", state);
31324
+ if (scope) authorizationUrl.searchParams.set("scope", scope);
31325
+ options.print?.(`Open this URL to authorize ${target.server}:\n${authorizationUrl.toString()}`);
31326
+ return {
31327
+ authorizationUrl: authorizationUrl.toString(),
31328
+ complete: async (callbackUrl) => {
31329
+ assertNoOAuthCallbackError(target, callbackUrl);
31330
+ const completion = extractCompletion(callbackUrl);
31331
+ if (completion.state !== state) throw new CapletsError("AUTH_FAILED", "OAuth callback state did not match");
31332
+ const params = new URLSearchParams({
31333
+ grant_type: "authorization_code",
31334
+ code: completion.code,
31335
+ redirect_uri: redirectUri,
31336
+ client_id: client.clientId,
31337
+ code_verifier: verifier
31338
+ });
31339
+ if (client.clientSecret) params.set("client_secret", client.clientSecret);
31340
+ const tokenResponse = await fetchJson(tokenEndpoint, target.requestTimeoutMs, {
31341
+ method: "POST",
31342
+ headers: { "content-type": "application/x-www-form-urlencoded" },
31343
+ body: params.toString()
31344
+ }, allowLoopbackHttp);
31345
+ const idToken = asString(tokenResponse.id_token);
31346
+ const idClaims = parseJwtPayload(idToken);
31347
+ validateOidcToken(authConfig, metadata, idToken, idClaims, client.clientId);
31348
+ writeTokenBundle(stripUndefined({
31349
+ server: target.server,
31350
+ authType: authConfig.type,
31351
+ accessToken: requireString(tokenResponse.access_token, "access_token"),
31352
+ refreshToken: asString(tokenResponse.refresh_token),
31353
+ tokenType: asString(tokenResponse.token_type),
31354
+ expiresAt: typeof tokenResponse.expires_in === "number" ? new Date(Date.now() + tokenResponse.expires_in * 1e3).toISOString() : void 0,
31355
+ scope: asString(tokenResponse.scope) ?? scope,
31356
+ idToken,
31357
+ issuer: asString(idClaims?.iss) ?? metadata.issuer ?? authConfig.issuer,
31358
+ subject: asString(idClaims?.sub),
31359
+ clientId: client.clientId,
31360
+ clientSecret: client.clientSecret,
31361
+ protectedResourceOrigin: protectedResourceOrigin(target, authConfig),
31362
+ metadata: redactSecrets({
31363
+ protectedResource: target.url ?? target.baseUrl ?? target.specUrl,
31364
+ authorizationServer: metadata,
31365
+ dynamicClient: client.dynamic ? { client_id: client.clientId } : void 0
31366
+ })
31367
+ }), options.authDir);
31368
+ }
31369
+ };
31370
+ }
31239
31371
  async function runGenericOAuthFlow(target, options = {}) {
31240
31372
  if (target.auth?.type !== "oauth2" && target.auth?.type !== "oidc") throw new CapletsError("REQUEST_INVALID", `${target.server} is not configured for OAuth`);
31241
31373
  const authConfig = target.auth;
@@ -58156,4 +58288,90 @@ function isDirectory(path) {
58156
58288
  }
58157
58289
  }
58158
58290
  //#endregion
58159
- export { getLiteralValue as $, ErrorCode as A, ListToolsRequestSchema as B, CompleteRequestSchema as C, string as Ct, DEFAULT_NEGOTIATED_PROTOCOL_VERSION as D, __commonJSMin as Dt, CreateTaskResultSchema as E, toSafeError as Et, LATEST_PROTOCOL_VERSION as F, SetLevelRequestSchema as G, McpError as H, ListPromptsRequestSchema as I, assertCompleteRequestResourceTemplate as J, ToolListChangedNotificationSchema as K, ListResourceTemplatesRequestSchema as L, InitializeRequestSchema as M, InitializedNotificationSchema as N, ElicitResultSchema as O, __require as Ot, JSONRPCMessageSchema as P, isJSONRPCResultResponse as Q, ListResourcesRequestSchema as R, CallToolResultSchema as S, object$1 as St, CreateMessageResultWithToolsSchema as T, CapletsError as Tt, ReadResourceRequestSchema as U, LoggingLevelSchema as V, SUPPORTED_PROTOCOL_VERSIONS as W, isJSONRPCErrorResponse as X, isInitializeRequest as Y, isJSONRPCRequest as Z, AjvJsonSchemaValidator as _, discoverCapletFiles as _t, capabilityDescription as a, normalizeObjectSchema as at, toJsonSchemaCompat as b, ZodOptional$1 as bt, deleteTokenBundle as c, safeParseAsync as ct, StreamableHTTPClientTransport as d, parseConfig as dt, getObjectShape as et, ReadBuffer as f, DEFAULT_AUTH_DIR as ft, assertToolsCallTaskCapability as g, resolveProjectConfigPath as gt, assertClientRequestTaskCapability as h, resolveProjectCapletsRoot as ht, ServerRegistry as i, isZ4Schema as it, GetPromptRequestSchema as j, EmptyResultSchema as k, __toESM as kt, isTokenBundleExpired as l, loadConfig as lt, Client as m, resolveConfigPath as mt, generatedToolInputSchema as n, getSchemaDescription as nt, runGenericOAuthFlow as o, objectFromShape as ot, serializeMessage as p, resolveCapletsRoot as pt, assertCompleteRequestPrompt as q, handleServerTool as r, isSchemaOptional as rt, runOAuthFlow as s, safeParse as st, CapletsEngine as t, getParseErrorMessage as tt, readTokenBundle as u, loadConfigWithSources as ut, Protocol as v, validateCapletFile as vt, CreateMessageResultSchema as w, url as wt, CallToolRequestSchema as x, literal as xt, mergeCapabilities as y, SERVER_ID_PATTERN as yt, ListRootsResultSchema as z };
58291
+ //#region src/server/options.ts
58292
+ const DEFAULT_SERVER_USER = "caplets";
58293
+ function resolveCapletsMode(input = {}, env = process.env) {
58294
+ const mode = parseCapletsMode(input.mode ?? env.CAPLETS_MODE ?? "auto");
58295
+ if (mode === "local") return { mode: "local" };
58296
+ const rawUrl = nonEmpty(input.serverUrl, "serverUrl") ?? nonEmpty(env.CAPLETS_SERVER_URL, "CAPLETS_SERVER_URL");
58297
+ if (mode === "remote") {
58298
+ if (rawUrl === void 0) throw new CapletsError("REQUEST_INVALID", "CAPLETS_MODE=remote requires CAPLETS_SERVER_URL or serverUrl.");
58299
+ return { mode: "remote" };
58300
+ }
58301
+ return rawUrl === void 0 ? { mode: "local" } : { mode: "remote" };
58302
+ }
58303
+ function resolveCapletsServer(input = {}, env = process.env) {
58304
+ const rawUrl = nonEmpty(input.url, "url") ?? nonEmpty(env.CAPLETS_SERVER_URL, "CAPLETS_SERVER_URL");
58305
+ if (rawUrl === void 0) throw new CapletsError("REQUEST_INVALID", "CAPLETS_SERVER_URL or url is required.");
58306
+ const baseUrl = parseServerBaseUrl(rawUrl);
58307
+ const userWasExplicit = input.user !== void 0 || hasEnv(env.CAPLETS_SERVER_USER);
58308
+ const user = nonEmpty(input.user, "user") ?? nonEmpty(env.CAPLETS_SERVER_USER, "CAPLETS_SERVER_USER") ?? DEFAULT_SERVER_USER;
58309
+ const password = nonEmpty(input.password, "password") ?? nonEmpty(env.CAPLETS_SERVER_PASSWORD, "CAPLETS_SERVER_PASSWORD");
58310
+ if (userWasExplicit && password === void 0) throw new CapletsError("REQUEST_INVALID", "Caplets server Basic Auth requires a password; set CAPLETS_SERVER_PASSWORD or password.");
58311
+ const auth = password === void 0 ? {
58312
+ enabled: false,
58313
+ user
58314
+ } : {
58315
+ enabled: true,
58316
+ user,
58317
+ password
58318
+ };
58319
+ const requestInit = auth.enabled ? { headers: { Authorization: basicAuthHeader(auth.user, auth.password) } } : {};
58320
+ return {
58321
+ baseUrl,
58322
+ mcpUrl: mcpUrlForBase(baseUrl),
58323
+ controlUrl: controlUrlForBase(baseUrl),
58324
+ healthUrl: healthUrlForBase(baseUrl),
58325
+ auth,
58326
+ requestInit,
58327
+ ...input.fetch ? { fetch: input.fetch } : {}
58328
+ };
58329
+ }
58330
+ function mcpUrlForBase(baseUrl) {
58331
+ return appendBasePath(baseUrl, "mcp");
58332
+ }
58333
+ function controlUrlForBase(baseUrl) {
58334
+ return appendBasePath(baseUrl, "control");
58335
+ }
58336
+ function healthUrlForBase(baseUrl) {
58337
+ return appendBasePath(baseUrl, "healthz");
58338
+ }
58339
+ function appendBasePath(baseUrl, path) {
58340
+ const url = new URL(baseUrl.href);
58341
+ url.pathname = `${url.pathname === "/" ? "" : url.pathname}/${path}`;
58342
+ return url;
58343
+ }
58344
+ function parseServerBaseUrl(value) {
58345
+ let url;
58346
+ try {
58347
+ url = new URL(value);
58348
+ } catch {
58349
+ throw new CapletsError("REQUEST_INVALID", "Invalid Caplets server URL.");
58350
+ }
58351
+ if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") throw new CapletsError("REQUEST_INVALID", "Caplets server URL must not include username, password, query string, or fragment.");
58352
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) throw new CapletsError("REQUEST_INVALID", "Caplets server URL must use https except loopback development URLs.");
58353
+ url.pathname = url.pathname === "/" ? "/" : url.pathname.replace(/\/+$/u, "");
58354
+ return url;
58355
+ }
58356
+ function isLoopbackHost(host) {
58357
+ const normalized = host.toLocaleLowerCase();
58358
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
58359
+ }
58360
+ function parseCapletsMode(value) {
58361
+ if (value === "auto" || value === "local" || value === "remote") return value;
58362
+ throw new CapletsError("REQUEST_INVALID", `Expected CAPLETS_MODE to be auto, local, or remote, got ${value}`);
58363
+ }
58364
+ function basicAuthHeader(user, password) {
58365
+ return `Basic ${Buffer$1.from(`${user}:${password}`).toString("base64")}`;
58366
+ }
58367
+ function nonEmpty(value, label) {
58368
+ if (value === void 0) return;
58369
+ const trimmed = value.trim();
58370
+ if (!trimmed) throw new CapletsError("REQUEST_INVALID", `${label} must not be empty`);
58371
+ return trimmed;
58372
+ }
58373
+ function hasEnv(value) {
58374
+ return value !== void 0 && value.trim() !== "";
58375
+ }
58376
+ //#endregion
58377
+ export { ToolListChangedNotificationSchema as $, CompleteRequestSchema as A, string as At, InitializedNotificationSchema as B, assertToolsCallTaskCapability as C, resolveProjectConfigPath as Ct, toJsonSchemaCompat as D, ZodOptional$1 as Dt, mergeCapabilities as E, SERVER_ID_PATTERN as Et, ElicitResultSchema as F, toSafeError as Ft, ListResourcesRequestSchema as G, LATEST_PROTOCOL_VERSION as H, EmptyResultSchema as I, __commonJSMin as It, LoggingLevelSchema as J, ListRootsResultSchema as K, ErrorCode as L, __require as Lt, CreateMessageResultWithToolsSchema as M, CAPLETS_ERROR_CODES as Mt, CreateTaskResultSchema as N, CapletsError as Nt, CallToolRequestSchema as O, literal as Ot, DEFAULT_NEGOTIATED_PROTOCOL_VERSION as P, redactSecrets as Pt, SetLevelRequestSchema as Q, GetPromptRequestSchema as R, __toESM as Rt, assertClientRequestTaskCapability as S, resolveProjectCapletsRoot as St, Protocol as T, validateCapletFile as Tt, ListPromptsRequestSchema as U, JSONRPCMessageSchema as V, ListResourceTemplatesRequestSchema as W, ReadResourceRequestSchema as X, McpError as Y, SUPPORTED_PROTOCOL_VERSIONS as Z, readTokenBundle as _, loadConfigWithSources as _t, resolveCapletsServer as a, isJSONRPCResultResponse as at, serializeMessage as b, resolveCapletsRoot as bt, handleServerTool as c, getParseErrorMessage as ct, runGenericOAuthFlow as d, isZ4Schema as dt, assertCompleteRequestPrompt as et, runOAuthFlow as f, normalizeObjectSchema as ft, isTokenBundleExpired as g, loadConfig as gt, deleteTokenBundle as h, safeParseAsync as ht, resolveCapletsMode as i, isJSONRPCRequest as it, CreateMessageResultSchema as j, url as jt, CallToolResultSchema as k, object$1 as kt, ServerRegistry as l, getSchemaDescription as lt, startOAuthFlow as m, safeParse as mt, mcpUrlForBase as n, isInitializeRequest as nt, CapletsEngine as o, getLiteralValue as ot, startGenericOAuthFlow as p, objectFromShape as pt, ListToolsRequestSchema as q, parseServerBaseUrl as r, isJSONRPCErrorResponse as rt, generatedToolInputSchema as s, getObjectShape as st, controlUrlForBase as t, assertCompleteRequestResourceTemplate as tt, capabilityDescription as u, isSchemaOptional as ut, StreamableHTTPClientTransport as v, parseConfig as vt, AjvJsonSchemaValidator as w, discoverCapletFiles as wt, Client as x, resolveConfigPath as xt, ReadBuffer as y, DEFAULT_AUTH_DIR as yt, InitializeRequestSchema as z };
@@ -0,0 +1,22 @@
1
+ export type RemoteAuthFlow = {
2
+ id: string;
3
+ server: string;
4
+ authorizationUrl: string;
5
+ createdAt: number;
6
+ complete(callbackUrl: string): Promise<void>;
7
+ };
8
+ export type RemoteAuthFlowStoreOptions = {
9
+ ttlMs?: number;
10
+ now?: () => number;
11
+ };
12
+ export declare class RemoteAuthFlowStore {
13
+ private readonly options;
14
+ private readonly flows;
15
+ constructor(options?: RemoteAuthFlowStoreOptions);
16
+ create(flow: Omit<RemoteAuthFlow, "id" | "createdAt">, id?: string): RemoteAuthFlow;
17
+ get(id: string): RemoteAuthFlow | undefined;
18
+ delete(id: string): void;
19
+ private pruneExpired;
20
+ private isExpired;
21
+ private now;
22
+ }
@@ -0,0 +1,11 @@
1
+ import type { RemoteCliCommand, RemoteCliRequest } from "./types";
2
+ export type RemoteControlClientOptions = {
3
+ baseUrl: URL;
4
+ requestInit: RequestInit;
5
+ fetch?: typeof fetch;
6
+ };
7
+ export declare class RemoteControlClient {
8
+ #private;
9
+ constructor(options: RemoteControlClientOptions);
10
+ request(command: RemoteCliCommand, args: RemoteCliRequest["arguments"]): Promise<unknown>;
11
+ }
@@ -0,0 +1,9 @@
1
+ import { type CapletsEngineOptions } from "../engine";
2
+ import type { RemoteAuthFlowStore } from "./auth-flow";
3
+ import type { RemoteCliRequest, RemoteCliResponse } from "./types";
4
+ export type RemoteControlDispatchContext = CapletsEngineOptions & {
5
+ projectCapletsRoot: string;
6
+ authFlowStore?: RemoteAuthFlowStore;
7
+ controlCallbackBaseUrl?: string;
8
+ };
9
+ export declare function dispatchRemoteCliRequest(request: RemoteCliRequest, context: RemoteControlDispatchContext): Promise<RemoteCliResponse>;
@@ -0,0 +1,17 @@
1
+ import type { CapletsErrorCode } from "../errors";
2
+ export type RemoteCliCommand = "list" | "get_caplet" | "check_backend" | "list_tools" | "search_tools" | "get_tool" | "call_tool" | "init" | "add" | "install" | "auth_login_start" | "auth_login_complete" | "auth_logout" | "auth_list";
3
+ export type RemoteCliRequest = {
4
+ command: RemoteCliCommand;
5
+ arguments: Record<string, unknown>;
6
+ };
7
+ export type RemoteCliResponse = {
8
+ ok: true;
9
+ result: unknown;
10
+ } | {
11
+ ok: false;
12
+ error: {
13
+ code: CapletsErrorCode;
14
+ message: string;
15
+ nextAction?: string;
16
+ };
17
+ };
@@ -1,12 +1,23 @@
1
1
  import { Hono } from "hono";
2
2
  import { CapletsEngine, type CapletsEngineOptions } from "../engine";
3
+ import { type RemoteControlDispatchContext } from "../remote-control/dispatch";
4
+ import { RemoteAuthFlowStore } from "../remote-control/auth-flow";
3
5
  import type { HttpServeOptions } from "./options";
4
6
  type HttpServeIo = {
5
7
  writeErr?: (value: string) => void;
8
+ control?: Omit<RemoteControlDispatchContext, "writeErr">;
9
+ authFlowStore?: RemoteAuthFlowStore;
6
10
  };
7
11
  export type CapletsHttpApp = Hono & {
8
12
  closeCapletsSessions: () => Promise<void>;
9
13
  };
10
14
  export declare function createHttpServeApp(options: HttpServeOptions, engine: CapletsEngine, io?: HttpServeIo): CapletsHttpApp;
11
15
  export declare function serveHttp(options: HttpServeOptions, engineOptions?: CapletsEngineOptions, writeErr?: (value: string) => void): Promise<void>;
16
+ export declare function routePath(base: string, path: string): string;
17
+ export declare function servicePaths(base: string): {
18
+ base: string;
19
+ mcp: string;
20
+ control: string;
21
+ health: string;
22
+ };
12
23
  export {};
@@ -7,6 +7,7 @@ export type RawServeOptions = {
7
7
  user?: string;
8
8
  password?: string;
9
9
  allowUnauthenticatedHttp?: boolean;
10
+ trustProxy?: boolean;
10
11
  };
11
12
  export type StdioServeOptions = {
12
13
  transport: "stdio";
@@ -19,6 +20,7 @@ export type HttpServeOptions = {
19
20
  auth: HttpBasicAuthOptions;
20
21
  warnUnauthenticatedNetwork: boolean;
21
22
  loopback: boolean;
23
+ trustProxy: boolean;
22
24
  };
23
25
  export type HttpBasicAuthOptions = {
24
26
  enabled: false;
@@ -29,6 +31,6 @@ export type HttpBasicAuthOptions = {
29
31
  password: string;
30
32
  };
31
33
  export type ServeOptions = StdioServeOptions | HttpServeOptions;
32
- export type ServeEnv = Partial<Record<"CAPLETS_SERVER_USER" | "CAPLETS_SERVER_PASSWORD", string>>;
34
+ export type ServeEnv = Partial<Record<"CAPLETS_SERVER_URL" | "CAPLETS_SERVER_USER" | "CAPLETS_SERVER_PASSWORD", string>>;
33
35
  export declare function resolveServeOptions(raw: RawServeOptions, env?: ServeEnv): ServeOptions;
34
36
  export declare function isLoopbackHost(host: string): boolean;
@@ -0,0 +1,41 @@
1
+ export type CapletsMode = "auto" | "local" | "remote";
2
+ export type CapletsServerEnv = Partial<Record<"CAPLETS_MODE" | "CAPLETS_SERVER_URL" | "CAPLETS_SERVER_USER" | "CAPLETS_SERVER_PASSWORD", string>>;
3
+ export type CapletsModeInput = {
4
+ mode?: string;
5
+ serverUrl?: string;
6
+ };
7
+ export type CapletsServerInput = {
8
+ url?: string;
9
+ user?: string;
10
+ password?: string;
11
+ fetch?: typeof fetch;
12
+ };
13
+ export type CapletsServerAuth = {
14
+ enabled: false;
15
+ user: string;
16
+ } | {
17
+ enabled: true;
18
+ user: string;
19
+ password: string;
20
+ };
21
+ export type ResolvedCapletsServer = {
22
+ baseUrl: URL;
23
+ mcpUrl: URL;
24
+ controlUrl: URL;
25
+ healthUrl: URL;
26
+ auth: CapletsServerAuth;
27
+ requestInit: RequestInit;
28
+ fetch?: typeof fetch;
29
+ };
30
+ export declare function resolveCapletsMode(input?: CapletsModeInput, env?: CapletsServerEnv): {
31
+ mode: "local";
32
+ } | {
33
+ mode: "remote";
34
+ };
35
+ export declare function resolveCapletsServer(input?: CapletsServerInput, env?: CapletsServerEnv): ResolvedCapletsServer;
36
+ export declare function mcpUrlForBase(baseUrl: URL): URL;
37
+ export declare function controlUrlForBase(baseUrl: URL): URL;
38
+ export declare function healthUrlForBase(baseUrl: URL): URL;
39
+ export declare function appendBasePath(baseUrl: URL, path: string): URL;
40
+ export declare function parseServerBaseUrl(value: string): URL;
41
+ export declare function isLoopbackHost(host: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caplets/core",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Core runtime library for Caplets progressive disclosure gateways.",
5
5
  "keywords": [
6
6
  "caplets",