@caplets/core 0.25.1 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as decodeDirectResourceUri, $t as ElicitResultSchema, A as nativeCapletToolDescription, An as objectFromShape, At as defaultCacheBaseDir, B as QuickJsCodeModeSandbox, Bt as assertClientRequestTaskCapability, C as resolveRemoteMode, Cn as getLiteralValue, Ct as startOAuthFlow, D as parseServerBaseUrl, Dn as isSchemaOptional, Dt as DEFAULT_AUTH_DIR, E as isLoopbackHost, En as getSchemaDescription, Et as readTokenBundle, F as codeModeRunInputSchema, Ft as resolveConfigPath, G as CodeModeLogStore, Gt as toJsonSchemaCompat, H as createCodeModeCapletsApi, Ht as AjvJsonSchemaValidator, I as codeModeRunParamsSchema, It as resolveProjectCapletsRoot, J as generateCodeModeDeclarations, Jt as CompleteRequestSchema, K as redactCodeModeLogText, Kt as CallToolRequestSchema, L as emptyCodeModeRunMeta, Lt as resolveProjectConfigPath, Mn as safeParseAsync, Mt as defaultConfigPath, Nt as defaultStateBaseDir, O as resolveCapletsServer, On as isZ4Schema, Pt as resolveCapletsRoot, Q as resolveExposure, R as runCodeMode, Rt as ReadBuffer, S as resolveHostedCloudRemote, Sn as isJSONRPCResultResponse, St as startGenericOAuthFlow, T as controlUrlForBase, Tn as getParseErrorMessage, Tt as isTokenBundleExpired, U as listCodeModeCallableCaplets, Ut as Protocol, V as diagnoseCodeModeTypeScript, Vt as assertToolsCallTaskCapability, W as CodeModeJournalStore, Wt as mergeCapabilities, X as minifyCodeModeDeclarationText, Xt as CreateMessageResultWithToolsSchema, Y as generateCodeModeRunToolDescription, Yt as CreateMessageResultSchema, Z as CapletsEngine, Zt as CreateTaskResultSchema, _n as assertCompleteRequestPrompt, _t as markdownCallToolResultContent, a as CloudAuthStore, an as JSONRPCMessageSchema, at as capabilityDescription, b as normalizeRemoteProfileHostUrl, bn as isJSONRPCErrorResponse, bt as runGenericOAuthFlow, c as redactedCloudAuthStatus, cn as ListResourceTemplatesRequestSchema, ct as loadConfigWithSources, d as projectBindingError, dn as ListToolsRequestSchema, dt as loadProjectConfig, en as EmptyResultSchema, et as directResourceUriMatchesTemplate, f as projectBindingRecovery, fn as LoggingLevelSchema, ft as parseConfig, g as buildProjectSyncManifest, gn as SetLevelRequestSchema, gt as hasRenderableStructuredContent, hn as SUPPORTED_PROTOCOL_VERSIONS, ht as loadCapletFilesFromMap, i as createRemoteProfileStore, in as InitializedNotificationSchema, it as ServerRegistry, j as nativeCapletToolName, jn as safeParse, jt as defaultConfigBaseDir, k as nativeCapletPromptGuidance, kn as normalizeObjectSchema, kt as DEFAULT_OBSERVED_OUTPUT_SHAPE_CACHE_DIR, l as PROJECT_BINDING_ERROR_CODES, ln as ListResourcesRequestSchema, lt as loadGlobalConfig, mn as ReadResourceRequestSchema, mt as validateCapletFile, n as resolveRemoteSelection, nn as GetPromptRequestSchema, nt as fingerprintProjectRoot, o as cloudAuthPath, on as LATEST_PROTOCOL_VERSION, ot as GoogleDiscoveryManager, p as CloudAuthClient, pn as McpError, pt as discoverCapletFiles, q as codeModeDeclarationHash, qt as CallToolResultSchema, r as cloudCredentialsFromRemoteProfile, rn as InitializeRequestSchema, rt as handleServerTool, s as migrateCredentials, sn as ListPromptsRequestSchema, st as loadConfig, t as createNativeCapletsService, tn as ErrorCode, tt as findProjectRoot, u as ProjectBindingError, un as ListRootsResultSchema, ut as loadLocalOverlayConfigWithSources, v as hostedCloudWorkspaceFromRemoteUrl, vn as assertCompleteRequestResourceTemplate, vt as markdownStructuredContent, w as appendBasePath, wn as getObjectShape, wt as deleteTokenBundle, x as resolveCapletsRemote, xn as isJSONRPCRequest, xt as runOAuthFlow, y as isCapletsCloudUrl, yn as isInitializeRequest, yt as refreshOAuthTokenBundle, z as CodeModeSessionManager, zt as serializeMessage } from "./service-Ut6dN9M8.js";
1
+ import { $ as resolveExposure, $t as mergeCapabilities, A as nativeCapletPromptGuidance, An as isJSONRPCRequest, At as runOAuthFlow, B as CodeModeSessionManager, Bn as safeParse, Bt as defaultConfigBaseDir, C as resolveHostedCloudRemote, Cn as ReadResourceRequestSchema, Ct as validateCapletFile, D as isLoopbackHost, Dn as assertCompleteRequestResourceTemplate, Dt as markdownStructuredContent, E as controlUrlForBase, En as assertCompleteRequestPrompt, Et as markdownCallToolResultContent, Fn as getSchemaDescription, Ft as readTokenBundle, G as CodeModeJournalStore, Gt as resolveProjectCapletsRoot, H as diagnoseCodeModeTypeScript, Ht as defaultStateBaseDir, I as codeModeRunInputSchema, In as isSchemaOptional, It as DEFAULT_AUTH_DIR, J as codeModeDeclarationHash, Jt as serializeMessage, K as CodeModeLogStore, Kt as resolveProjectConfigPath, L as codeModeRunParamsSchema, Ln as isZ4Schema, M as nativeCapletToolName, Mn as getLiteralValue, Mt as startOAuthFlow, Nn as getObjectShape, Nt as deleteTokenBundle, O as parseServerBaseUrl, On as isInitializeRequest, Ot as refreshOAuthTokenBundle, Pn as getParseErrorMessage, Pt as isTokenBundleExpired, Q as CapletsEngine, Qt as Protocol, R as emptyCodeModeRunMeta, Rn as normalizeObjectSchema, Rt as DEFAULT_OBSERVED_OUTPUT_SHAPE_CACHE_DIR, S as resolveCapletsRemote, Sn as McpError, St as discoverCapletFiles, T as appendBasePath, Tn as SetLevelRequestSchema, Tt as hasRenderableStructuredContent, U as createCodeModeCapletsApi, Ut as resolveCapletsRoot, V as QuickJsCodeModeSandbox, Vn as safeParseAsync, Vt as defaultConfigPath, W as listCodeModeCallableCaplets, Wt as resolveConfigPath, X as generateCodeModeRunToolDescription, Xt as assertToolsCallTaskCapability, Y as generateCodeModeDeclarations, Yt as assertClientRequestTaskCapability, Z as minifyCodeModeDeclarationText, Zt as AjvJsonSchemaValidator, _ as CapletsCloudClient, _n as ListResourceTemplatesRequestSchema, _t as FileVaultStore, a as CloudAuthStore, an as CreateMessageResultWithToolsSchema, at as ServerRegistry, b as isCapletsCloudUrl, bn as ListToolsRequestSchema, bt as decryptVaultValue, c as redactedCloudAuthStatus, cn as ElicitResultSchema, ct as loadConfig, d as projectBindingError, dn as GetPromptRequestSchema, dt as loadLocalOverlayConfigWithSources, en as toJsonSchemaCompat, et as decodeDirectResourceUri, f as projectBindingRecovery, fn as InitializeRequestSchema, ft as loadProjectConfig, g as buildProjectSyncManifest, gn as ListPromptsRequestSchema, gt as vaultStoreForAuthDir, hn as LATEST_PROTOCOL_VERSION, ht as vaultResolverForAuthDir, i as createRemoteProfileStore, in as CreateMessageResultSchema, it as handleServerTool, j as nativeCapletToolDescription, jn as isJSONRPCResultResponse, jt as startGenericOAuthFlow, k as resolveCapletsServer, kn as isJSONRPCErrorResponse, kt as runGenericOAuthFlow, l as PROJECT_BINDING_ERROR_CODES, ln as EmptyResultSchema, lt as loadConfigWithSources, mn as JSONRPCMessageSchema, mt as vaultBootstrapResolver, n as resolveRemoteSelection, nn as CallToolResultSchema, nt as findProjectRoot, o as cloudAuthPath, on as CreateTaskResultSchema, ot as capabilityDescription, p as CloudAuthClient, pn as InitializedNotificationSchema, pt as parseConfig, q as redactCodeModeLogText, qt as ReadBuffer, r as cloudCredentialsFromRemoteProfile, rn as CompleteRequestSchema, rt as fingerprintProjectRoot, s as migrateCredentials, st as GoogleDiscoveryManager, t as createNativeCapletsService, tn as CallToolRequestSchema, tt as directResourceUriMatchesTemplate, u as ProjectBindingError, un as ErrorCode, ut as loadGlobalConfig, vn as ListResourcesRequestSchema, vt as VAULT_MAX_VALUE_BYTES, w as resolveRemoteMode, wn as SUPPORTED_PROTOCOL_VERSIONS, wt as loadCapletFilesFromMap, x as normalizeRemoteProfileHostUrl, xn as LoggingLevelSchema, xt as encryptVaultValue, y as hostedCloudWorkspaceFromRemoteUrl, yn as ListRootsResultSchema, yt as validateVaultKeyName, z as runCodeMode, zn as objectFromShape, zt as defaultCacheBaseDir } from "./service-aBIn4nrw.js";
2
2
  import { _ as record, b as unknown, d as literal, m as object, n as ZodOptional, o as array, p as number, r as _enum, s as boolean, v as string, x as url } from "./schemas-BoqMu4MG.js";
3
- import { f as redactSecrets$1, i as SERVER_ID_PATTERN, l as CAPLETS_ERROR_CODES, p as toSafeError, u as CapletsError } from "./validation-C4tYXw6G.js";
3
+ import { f as redactSecrets$1, i as SERVER_ID_PATTERN, l as CAPLETS_ERROR_CODES, p as toSafeError, u as CapletsError } from "./validation-GD2x5HW1.js";
4
4
  import { generatedToolInputJsonSchemaForCaplet, generatedToolInputSchema, generatedToolInputSchemaForCaplet } from "./generated-tool-input-schema.js";
5
5
  import { f as observedOutputShapeKey, g as stableJsonStringify, h as schemaHash, i as observeOutputShape, u as FileObservedOutputShapeStore } from "./observed-output-shapes-DuP7mJQf.js";
6
- import { a as formatCapletList, c as resolveCliConfigPaths, l as cliCommands$1, n as completionScript, o as formatConfigPaths, s as listCaplets, t as completeCliWords, u as completionShells } from "./completion-De4t5MtT.js";
6
+ import { a as formatCapletList, c as resolveCliConfigPaths, l as cliCommands$1, n as completionScript, o as formatConfigPaths, s as listCaplets, t as completeCliWords, u as completionShells } from "./completion-CFOJucl5.js";
7
7
  import { n as normalizeCapletSourcePath, t as FilesystemCapletSource } from "./filesystem-Kkg32TOJ.js";
8
8
  import { parseConfig as parseConfig$1 } from "./config-runtime.js";
9
9
  import fs, { accessSync, chmodSync, closeSync, constants, copyFileSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync, writeSync } from "node:fs";
@@ -14,9 +14,9 @@ import { Readable, Writable } from "node:stream";
14
14
  import { STATUS_CODES, createServer } from "node:http";
15
15
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
16
16
  import { homedir, tmpdir, userInfo } from "node:os";
17
+ import { Buffer as Buffer$1 } from "node:buffer";
17
18
  import { EventEmitter } from "node:events";
18
19
  import { promisify, stripVTControlCharacters } from "node:util";
19
- import { Buffer as Buffer$1 } from "node:buffer";
20
20
  import { createInterface } from "node:readline/promises";
21
21
  import { Http2ServerRequest, constants as constants$1 } from "node:http2";
22
22
  //#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
@@ -1553,7 +1553,7 @@ const EMPTY_COMPLETION_RESULT = { completion: {
1553
1553
  } };
1554
1554
  //#endregion
1555
1555
  //#region package.json
1556
- var version = "0.25.1";
1556
+ var version = "0.26.1";
1557
1557
  //#endregion
1558
1558
  //#region src/serve/session.ts
1559
1559
  var CapletsMcpSession = class {
@@ -5353,7 +5353,7 @@ function collectFiles(root) {
5353
5353
  //#endregion
5354
5354
  //#region src/cli/auth.ts
5355
5355
  async function loginAuth(serverId, options) {
5356
- const server = await resolveAuthTarget$1(serverId, options.config ?? loadConfig(options.configPath), options.authDir);
5356
+ const server = await resolveAuthTarget$1(serverId, options.config ?? loadAuthResolvedConfig(options), options.authDir);
5357
5357
  assertLoginTarget(server, serverId);
5358
5358
  try {
5359
5359
  const flowOptions = {
@@ -5375,7 +5375,7 @@ function logoutAuth(serverId, options) {
5375
5375
  else options.writeOut(`No OAuth credentials found for \`${serverId}\`.\n`);
5376
5376
  }
5377
5377
  function logoutAuthResult(serverId, options) {
5378
- assertLoginTarget(findAuthTarget(serverId, options.config ?? loadConfig(options.configPath)), serverId);
5378
+ assertLoginTarget(findAuthTarget(serverId, options.config ?? loadConfig(options.configPath, void 0, { vaultResolver: vaultBootstrapResolver })), serverId);
5379
5379
  return {
5380
5380
  server: serverId,
5381
5381
  deleted: deleteTokenBundle(serverId, options.authDir)
@@ -5386,13 +5386,16 @@ async function refreshAuth(serverId, options) {
5386
5386
  options.writeOut(`Refreshed OAuth credentials for \`${serverId}\`.\n`);
5387
5387
  }
5388
5388
  async function refreshAuthResult(serverId, options) {
5389
- const target = await resolveAuthTarget$1(serverId, options.config ?? loadConfig(options.configPath), options.authDir);
5389
+ const target = await resolveAuthTarget$1(serverId, options.config ?? loadAuthResolvedConfig(options), options.authDir);
5390
5390
  assertLoginTarget(target, serverId);
5391
5391
  await refreshOAuthTokenBundle(target, options.authDir);
5392
5392
  return { server: serverId };
5393
5393
  }
5394
5394
  function listAuthRows(options) {
5395
- return authRowsForTargets(authTargets(loadConfig(options.configPath)), options.authDir);
5395
+ return authRowsForTargets(authTargets(loadConfig(options.configPath, void 0, { vaultResolver: vaultBootstrapResolver })), options.authDir);
5396
+ }
5397
+ function loadAuthResolvedConfig(options) {
5398
+ return loadConfig(options.configPath, void 0, { vaultResolver: vaultResolverForAuthDir(options.authDir) });
5396
5399
  }
5397
5400
  function listLocalAuthRows(options) {
5398
5401
  return authRowsForTargets(localAuthTargets(options), options.authDir);
@@ -5402,7 +5405,7 @@ function localAuthTargets(options) {
5402
5405
  }
5403
5406
  function localAuthConfigForTarget(options) {
5404
5407
  assertLoginTarget(localAuthTargets(options).find((candidate) => candidate.server === options.serverId), options.serverId);
5405
- return loadConfigForSource(options.source, options);
5408
+ return loadConfigForSource(options.source, options, { vaultResolver: vaultResolverForAuthDir(options.authDir) });
5406
5409
  }
5407
5410
  function authTargetsForSource(source, options) {
5408
5411
  try {
@@ -5415,9 +5418,9 @@ function authTargetsForSource(source, options) {
5415
5418
  throw error;
5416
5419
  }
5417
5420
  }
5418
- function loadConfigForSource(source, options) {
5419
- if (source === "global") return loadGlobalConfig(options.configPath);
5420
- return loadProjectConfig(options.projectConfigPath);
5421
+ function loadConfigForSource(source, options, loadOptions = { vaultResolver: vaultBootstrapResolver }) {
5422
+ if (source === "global") return loadGlobalConfig(options.configPath, loadOptions);
5423
+ return loadProjectConfig(options.projectConfigPath, loadOptions);
5421
5424
  }
5422
5425
  function authRowsForTargets(targets, authDir) {
5423
5426
  return targets.sort((left, right) => left.server.localeCompare(right.server)).map((server) => {
@@ -5861,6 +5864,21 @@ function assertPathSegment(value, label) {
5861
5864
  if (!value || value.includes("/") || value.includes("\\") || value.includes("\0") || value === "." || value === "..") throw new Error(`Invalid ${label}: ${value}`);
5862
5865
  }
5863
5866
  //#endregion
5867
+ //#region src/daemon/host-path.ts
5868
+ /**
5869
+ * Node's POSIX filesystem APIs do not treat backslashes as path separators. Tests
5870
+ * sometimes emulate Windows daemon operations with POSIX temp directories, which
5871
+ * `node:path.win32` renders as drive-less absolute paths such as
5872
+ * `\tmp\caplets-...`. On POSIX hosts those strings would otherwise be created as
5873
+ * single backslash-named entries in the current working directory.
5874
+ */
5875
+ function daemonHostPath(path) {
5876
+ if (process.platform === "win32") return path;
5877
+ if (!path.startsWith("\\") || path.startsWith("\\\\")) return path;
5878
+ if (/^[A-Za-z]:/u.test(path)) return path;
5879
+ return path.replaceAll("\\", "/");
5880
+ }
5881
+ //#endregion
5864
5882
  //#region src/daemon/config.ts
5865
5883
  function readDaemonConfig(paths) {
5866
5884
  return readJson(paths.configFile);
@@ -5874,10 +5892,10 @@ function writeDaemonState(paths, state) {
5874
5892
  return state;
5875
5893
  }
5876
5894
  function removeDaemonConfig(paths) {
5877
- rmSync(paths.configFile, { force: true });
5895
+ rmSync(daemonHostPath(paths.configFile), { force: true });
5878
5896
  }
5879
5897
  function removeDaemonState(paths) {
5880
- rmSync(paths.stateFile, { force: true });
5898
+ rmSync(daemonHostPath(paths.stateFile), { force: true });
5881
5899
  }
5882
5900
  function mergeDaemonEnv(existing, install) {
5883
5901
  const values = { ...existing?.values };
@@ -5901,20 +5919,22 @@ function validateEnvName(value) {
5901
5919
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value)) throw new CapletsError("REQUEST_INVALID", `Invalid environment variable name: ${value}`);
5902
5920
  }
5903
5921
  function readJson(path) {
5922
+ const hostPath = daemonHostPath(path);
5904
5923
  try {
5905
- return JSON.parse(readFileSync(path, "utf8"));
5924
+ return JSON.parse(readFileSync(hostPath, "utf8"));
5906
5925
  } catch (error) {
5907
5926
  if (error.code === "ENOENT") return void 0;
5908
5927
  throw error;
5909
5928
  }
5910
5929
  }
5911
5930
  function writeJson(path, value) {
5912
- mkdirSync(dirname(path), {
5931
+ const hostPath = daemonHostPath(path);
5932
+ mkdirSync(dirname(hostPath), {
5913
5933
  recursive: true,
5914
5934
  mode: 448
5915
5935
  });
5916
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
5917
- chmodSync(path, 384);
5936
+ writeFileSync(hostPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
5937
+ chmodSync(hostPath, 384);
5918
5938
  }
5919
5939
  //#endregion
5920
5940
  //#region src/daemon/logs.ts
@@ -5949,9 +5969,10 @@ async function followDaemonLogs(paths, options) {
5949
5969
  }).entries) options.write(entry);
5950
5970
  const watchers = selectedStreams(options.stream ?? "all").map((stream) => {
5951
5971
  const file = paths[stream === "stdout" ? "stdoutLog" : "stderrLog"];
5972
+ const hostFile = daemonHostPath(file);
5952
5973
  ensureLogFile(file);
5953
- let offset = existsSync(file) ? statSync(file).size : 0;
5954
- return watch(file, { persistent: true }, () => {
5974
+ let offset = existsSync(hostFile) ? statSync(hostFile).size : 0;
5975
+ return watch(hostFile, { persistent: true }, () => {
5955
5976
  const { content, nextOffset } = readFromOffset(file, offset);
5956
5977
  offset = nextOffset;
5957
5978
  for (const line of content.split(/\r?\n/u).filter(Boolean)) options.write({
@@ -5974,13 +5995,14 @@ function selectedStreams(stream) {
5974
5995
  return stream === "all" ? ["stdout", "stderr"] : [stream];
5975
5996
  }
5976
5997
  function tailLines(path, count) {
5977
- if (count === 0 || !existsSync(path)) return [];
5978
- const lines = count < 0 ? readFileSync(path, "utf8").split(/\r?\n/u) : readTailContent(path, count);
5998
+ const hostPath = daemonHostPath(path);
5999
+ if (count === 0 || !existsSync(hostPath)) return [];
6000
+ const lines = count < 0 ? readFileSync(hostPath, "utf8").split(/\r?\n/u) : readTailContent(path, count);
5979
6001
  if (lines.at(-1) === "") lines.pop();
5980
6002
  return count < 0 ? lines : lines.slice(-count);
5981
6003
  }
5982
6004
  function readTailContent(path, count) {
5983
- const fd = openSync(path, "r");
6005
+ const fd = openSync(daemonHostPath(path), "r");
5984
6006
  try {
5985
6007
  const size = fstatSync(fd).size;
5986
6008
  const chunks = [];
@@ -6001,11 +6023,12 @@ function readTailContent(path, count) {
6001
6023
  }
6002
6024
  }
6003
6025
  function readFromOffset(path, offset) {
6004
- if (!existsSync(path)) return {
6026
+ const hostPath = daemonHostPath(path);
6027
+ if (!existsSync(hostPath)) return {
6005
6028
  content: "",
6006
6029
  nextOffset: 0
6007
6030
  };
6008
- const fd = openSync(path, "r");
6031
+ const fd = openSync(hostPath, "r");
6009
6032
  try {
6010
6033
  const size = fstatSync(fd).size;
6011
6034
  if (size <= offset) return {
@@ -6046,12 +6069,13 @@ function logTimestamp(line) {
6046
6069
  return Number.isNaN(timestamp) ? void 0 : timestamp;
6047
6070
  }
6048
6071
  function ensureLogFile(path) {
6049
- if (existsSync(path)) return;
6050
- mkdirSync(dirname(path), {
6072
+ const hostPath = daemonHostPath(path);
6073
+ if (existsSync(hostPath)) return;
6074
+ mkdirSync(dirname(hostPath), {
6051
6075
  recursive: true,
6052
6076
  mode: 448
6053
6077
  });
6054
- writeFileSync(path, "", { mode: 384 });
6078
+ writeFileSync(hostPath, "", { mode: 384 });
6055
6079
  }
6056
6080
  //#endregion
6057
6081
  //#region src/daemon/xml.ts
@@ -6242,9 +6266,9 @@ function launchdManager(runner, uid = typeof process.getuid === "function" ? pro
6242
6266
  return {
6243
6267
  descriptor: buildLaunchdDescriptor,
6244
6268
  status: async (config, paths) => {
6245
- if (!existsSync(paths.descriptorFile) && !config) return notInstalled();
6269
+ if (!existsSync(daemonHostPath(paths.descriptorFile)) && !config) return notInstalled();
6246
6270
  const result = await runner.exec("launchctl", ["print", target]);
6247
- if (result.code !== 0) return existsSync(paths.descriptorFile) ? stopped({ stderr: result.stderr }) : notInstalled({ stderr: result.stderr });
6271
+ if (result.code !== 0) return existsSync(daemonHostPath(paths.descriptorFile)) ? stopped({ stderr: result.stderr }) : notInstalled({ stderr: result.stderr });
6248
6272
  const pid = parseNumberMatch(result.stdout, /\bpid\s*=\s*(\d+)/u);
6249
6273
  if (pid !== void 0) return runningOrStopped(pid, {
6250
6274
  stdout: result.stdout,
@@ -6270,7 +6294,7 @@ function launchdManager(runner, uid = typeof process.getuid === "function" ? pro
6270
6294
  };
6271
6295
  },
6272
6296
  uninstall: async (config, paths) => {
6273
- const hasDescriptor = existsSync(paths.descriptorFile);
6297
+ const hasDescriptor = existsSync(daemonHostPath(paths.descriptorFile));
6274
6298
  if (!hasDescriptor && !config) return {
6275
6299
  action: "uninstall",
6276
6300
  native: notInstalled(),
@@ -6288,7 +6312,7 @@ function launchdManager(runner, uid = typeof process.getuid === "function" ? pro
6288
6312
  ]];
6289
6313
  const result = await runner.exec(commands[0][0], commands[0].slice(1));
6290
6314
  if (result.code !== 0 && !/No such process|not found|Could not find service|No such file or directory/iu.test(result.stderr)) throw new CapletsError("SERVER_UNAVAILABLE", `launchd unregister failed: ${result.stderr || result.stdout || result.code}`);
6291
- rmSync(paths.descriptorFile, { force: true });
6315
+ rmSync(daemonHostPath(paths.descriptorFile), { force: true });
6292
6316
  return {
6293
6317
  action: "uninstall",
6294
6318
  native: notInstalled({
@@ -6301,7 +6325,7 @@ function launchdManager(runner, uid = typeof process.getuid === "function" ? pro
6301
6325
  start: async (config) => launchdStartLifecycle(runner, domain, target, config.paths.descriptorFile),
6302
6326
  restart: async (config) => launchdRestartLifecycle(runner, domain, target, config.paths.descriptorFile),
6303
6327
  stop: async (config) => {
6304
- const command = config && existsSync(config.paths.descriptorFile) ? [
6328
+ const command = config && existsSync(daemonHostPath(config.paths.descriptorFile)) ? [
6305
6329
  "launchctl",
6306
6330
  "bootout",
6307
6331
  domain,
@@ -6326,7 +6350,7 @@ function systemdManager(runner, serviceAvailable = true) {
6326
6350
  descriptor: buildSystemdDescriptor,
6327
6351
  status: async (config, paths) => {
6328
6352
  if (!serviceAvailable) return unavailable("systemd --user is not available.");
6329
- if (!existsSync(paths.descriptorFile) && !config) return notInstalled();
6353
+ if (!existsSync(daemonHostPath(paths.descriptorFile)) && !config) return notInstalled();
6330
6354
  const show = await runner.exec("systemctl", [
6331
6355
  "--user",
6332
6356
  "show",
@@ -6335,13 +6359,13 @@ function systemdManager(runner, serviceAvailable = true) {
6335
6359
  if (show.code !== 0) {
6336
6360
  const message = show.stderr || show.stdout || String(show.code);
6337
6361
  if (isSystemdUnavailable(message)) return unavailable(`systemd --user is not available: ${message}`);
6338
- return existsSync(paths.descriptorFile) ? stopped({
6362
+ return existsSync(daemonHostPath(paths.descriptorFile)) ? stopped({
6339
6363
  stderr: show.stderr,
6340
6364
  stdout: show.stdout
6341
6365
  }) : notInstalled({ stderr: show.stderr });
6342
6366
  }
6343
6367
  const raw = parseSystemdShow(show.stdout);
6344
- if (!existsSync(paths.descriptorFile) && raw.LoadState === "not-found") return notInstalled({
6368
+ if (!existsSync(daemonHostPath(paths.descriptorFile)) && raw.LoadState === "not-found") return notInstalled({
6345
6369
  raw,
6346
6370
  stderr: show.stderr
6347
6371
  });
@@ -6407,7 +6431,7 @@ function systemdManager(runner, serviceAvailable = true) {
6407
6431
  ]];
6408
6432
  await assertExecUnless(runner, commands[0], "systemd unregister failed", /not loaded|not found|No such file|does not exist/iu);
6409
6433
  const descriptorBackup = backupPath(paths.descriptorFile);
6410
- rmSync(paths.descriptorFile, { force: true });
6434
+ rmSync(daemonHostPath(paths.descriptorFile), { force: true });
6411
6435
  try {
6412
6436
  await assertExec(runner, commands[1], "systemd unregister failed");
6413
6437
  } catch (error) {
@@ -6430,7 +6454,7 @@ function windowsTaskManager(runner) {
6430
6454
  return {
6431
6455
  descriptor: buildWindowsTaskDescriptor,
6432
6456
  status: async (config, paths) => {
6433
- if (!existsSync(paths.descriptorFile) && !config) return notInstalled();
6457
+ if (!existsSync(daemonHostPath(paths.descriptorFile)) && !config) return notInstalled();
6434
6458
  const result = await runner.exec("schtasks", [
6435
6459
  "/Query",
6436
6460
  "/TN",
@@ -6485,8 +6509,8 @@ function windowsTaskManager(runner) {
6485
6509
  "/F"
6486
6510
  ];
6487
6511
  await assertExecUnless(runner, command, "Scheduled Task unregister failed", /cannot find|does not exist|not found/iu);
6488
- rmSync(paths.descriptorFile, { force: true });
6489
- rmSync(paths.wrapperFile, { force: true });
6512
+ rmSync(daemonHostPath(paths.descriptorFile), { force: true });
6513
+ rmSync(daemonHostPath(paths.wrapperFile), { force: true });
6490
6514
  return {
6491
6515
  action: "uninstall",
6492
6516
  native: notInstalled(),
@@ -6558,19 +6582,21 @@ function unsupportedManager(platform) {
6558
6582
  };
6559
6583
  }
6560
6584
  function writeDescriptor(descriptor) {
6561
- mkdirSync(dirname(descriptor.path), {
6585
+ const descriptorPath = daemonHostPath(descriptor.path);
6586
+ mkdirSync(dirname(descriptorPath), {
6562
6587
  recursive: true,
6563
6588
  mode: 448
6564
6589
  });
6565
- writeFileSync(descriptor.path, descriptor.kind === "windows-scheduled-task" ? descriptor.xml : descriptor.contents, { mode: 384 });
6566
- chmodSync(descriptor.path, 384);
6590
+ writeFileSync(descriptorPath, descriptor.kind === "windows-scheduled-task" ? descriptor.xml : descriptor.contents, { mode: 384 });
6591
+ chmodSync(descriptorPath, 384);
6567
6592
  if (descriptor.kind === "windows-scheduled-task") {
6568
- mkdirSync(dirname(descriptor.wrapper.path), {
6593
+ const wrapperPath = daemonHostPath(descriptor.wrapper.path);
6594
+ mkdirSync(dirname(wrapperPath), {
6569
6595
  recursive: true,
6570
6596
  mode: 448
6571
6597
  });
6572
- writeFileSync(descriptor.wrapper.path, descriptor.wrapper.contents, { mode: 448 });
6573
- chmodSync(descriptor.wrapper.path, 448);
6598
+ writeFileSync(wrapperPath, descriptor.wrapper.contents, { mode: 448 });
6599
+ chmodSync(wrapperPath, 448);
6574
6600
  }
6575
6601
  }
6576
6602
  async function writeDescriptorForInstall(descriptor, register, afterRestore) {
@@ -6590,23 +6616,27 @@ function backupDescriptorFiles(descriptor) {
6590
6616
  return (descriptor.kind === "windows-scheduled-task" ? [descriptor.path, descriptor.wrapper.path] : [descriptor.path]).map(backupPath);
6591
6617
  }
6592
6618
  function backupPath(path) {
6593
- const existed = existsSync(path);
6619
+ const hostPath = daemonHostPath(path);
6620
+ const existed = existsSync(hostPath);
6594
6621
  return {
6595
6622
  path,
6596
6623
  existed,
6597
- ...existed ? { contents: readFileSync(path) } : {},
6598
- ...existed ? { mode: statSync(path).mode & 511 } : {}
6624
+ ...existed ? { contents: readFileSync(hostPath) } : {},
6625
+ ...existed ? { mode: statSync(hostPath).mode & 511 } : {}
6599
6626
  };
6600
6627
  }
6601
6628
  function restoreDescriptorFiles(backups) {
6602
- for (const backup of backups) if (backup.existed && backup.contents) {
6603
- mkdirSync(dirname(backup.path), {
6604
- recursive: true,
6605
- mode: 448
6606
- });
6607
- writeFileSync(backup.path, backup.contents, { mode: backup.mode ?? 384 });
6608
- chmodSync(backup.path, backup.mode ?? 384);
6609
- } else rmSync(backup.path, { force: true });
6629
+ for (const backup of backups) {
6630
+ const hostPath = daemonHostPath(backup.path);
6631
+ if (backup.existed && backup.contents) {
6632
+ mkdirSync(dirname(hostPath), {
6633
+ recursive: true,
6634
+ mode: 448
6635
+ });
6636
+ writeFileSync(hostPath, backup.contents, { mode: backup.mode ?? 384 });
6637
+ chmodSync(hostPath, backup.mode ?? 384);
6638
+ } else rmSync(hostPath, { force: true });
6639
+ }
6610
6640
  }
6611
6641
  async function assertExec(runner, command, message) {
6612
6642
  const result = await runner.exec(command[0], command.slice(1));
@@ -11048,7 +11078,7 @@ async function dispatchRemoteCliRequest(request, context) {
11048
11078
  async function dispatch(request, context) {
11049
11079
  assertObject(request, "remote control request");
11050
11080
  assertObject(request.arguments, "remote control request arguments");
11051
- if (request.command === "list") return listCaplets(loadConfigWithSources(context.configPath, context.projectConfigPath), { includeDisabled: optionalBoolean(request.arguments, "includeDisabled") ?? false });
11081
+ if (request.command === "list") return listCaplets(loadConfigWithSources(context.configPath, context.projectConfigPath, { vaultResolver: vaultBootstrapResolver }), { includeDisabled: optionalBoolean(request.arguments, "includeDisabled") ?? false });
11052
11082
  if (ENGINE_COMMANDS.has(request.command)) {
11053
11083
  const caplet = requiredString(request.arguments, "caplet");
11054
11084
  const toolRequest = requiredEngineRequest(request.arguments, request.command);
@@ -11089,6 +11119,7 @@ async function dispatch(request, context) {
11089
11119
  ...optionalProp("configPath", context.configPath),
11090
11120
  ...optionalProp("authDir", context.authDir)
11091
11121
  });
11122
+ if (request.command.startsWith("vault_")) return dispatchVault(request, context);
11092
11123
  if (request.command === "auth_logout") return logoutAuthResult(requiredString(request.arguments, "server"), {
11093
11124
  ...optionalProp("configPath", context.configPath),
11094
11125
  ...optionalProp("authDir", context.authDir)
@@ -11101,9 +11132,76 @@ async function dispatch(request, context) {
11101
11132
  if (request.command === "auth_login_complete") return completeRemoteAuthLogin(requiredString(request.arguments, "flowId"), requiredString(request.arguments, "callbackUrl"), context);
11102
11133
  throw new CapletsError("UNKNOWN_OPERATION", `Unsupported remote control command ${request.command}`);
11103
11134
  }
11135
+ function dispatchVault(request, context) {
11136
+ const store = remoteVaultStore(context);
11137
+ switch (request.command) {
11138
+ case "vault_set": {
11139
+ const name = requiredString(request.arguments, "name");
11140
+ const value = requiredString(request.arguments, "value");
11141
+ const grant = optionalString(request.arguments, "grant");
11142
+ const grantInput = grant ? {
11143
+ storedKey: validateVaultKeyName(name),
11144
+ referenceName: validateVaultKeyName(optionalString(request.arguments, "referenceName") ?? name),
11145
+ capletId: grant,
11146
+ origin: remoteVaultAccessOrigin(grant, context)
11147
+ } : void 0;
11148
+ const existed = store.getStatus(name).present;
11149
+ const previousValue = existed && grantInput ? store.resolveValue(name) : void 0;
11150
+ const status = store.set(name, value, { force: optionalBoolean(request.arguments, "force") ?? false });
11151
+ try {
11152
+ if (grantInput) store.grantAccess(grantInput);
11153
+ } catch (error) {
11154
+ if (existed && previousValue !== void 0) store.set(name, previousValue, { force: true });
11155
+ else store.delete(name);
11156
+ throw error;
11157
+ }
11158
+ return {
11159
+ remote: true,
11160
+ ...status
11161
+ };
11162
+ }
11163
+ case "vault_list": return store.listValues();
11164
+ case "vault_get": {
11165
+ const name = requiredString(request.arguments, "name");
11166
+ if (optionalBoolean(request.arguments, "reveal") ?? false) throw new CapletsError("REQUEST_INVALID", "Self-hosted remote Vault reveal is not supported through remote control.");
11167
+ return store.getStatus(name);
11168
+ }
11169
+ case "vault_delete": return store.delete(requiredString(request.arguments, "name"));
11170
+ case "vault_access_grant": {
11171
+ const storedKey = requiredString(request.arguments, "name");
11172
+ const capletId = requiredString(request.arguments, "capletId");
11173
+ return store.grantAccess({
11174
+ storedKey,
11175
+ referenceName: optionalString(request.arguments, "referenceName") ?? storedKey,
11176
+ capletId,
11177
+ origin: remoteVaultAccessOrigin(capletId, context)
11178
+ });
11179
+ }
11180
+ case "vault_access_revoke": return store.revokeAccess({
11181
+ storedKey: requiredString(request.arguments, "name"),
11182
+ capletId: requiredString(request.arguments, "capletId"),
11183
+ ...optionalProp("referenceName", optionalString(request.arguments, "referenceName"))
11184
+ });
11185
+ case "vault_access_list": return store.listAccess({
11186
+ ...optionalProp("storedKey", optionalString(request.arguments, "name")),
11187
+ ...optionalProp("capletId", optionalString(request.arguments, "capletId"))
11188
+ });
11189
+ default: throw new CapletsError("UNKNOWN_OPERATION", `Unsupported remote control command ${request.command}`);
11190
+ }
11191
+ }
11192
+ function remoteVaultStore(context) {
11193
+ return vaultStoreForAuthDir(context.authDir);
11194
+ }
11195
+ function remoteVaultAccessOrigin(capletId, context) {
11196
+ const overlay = loadLocalOverlayConfigWithSources(context.configPath, context.projectConfigPath, { vaultResolver: vaultBootstrapResolver });
11197
+ const origin = overlay.sources[capletId];
11198
+ if (!origin) throw new CapletsError("SERVER_NOT_FOUND", `Caplet ${capletId} is not configured.`);
11199
+ if (overlay.shadows[capletId]?.length) throw new CapletsError("REQUEST_INVALID", `Caplet ${capletId} is shadowed in multiple config sources; resolve the active config before granting Vault access.`);
11200
+ return origin;
11201
+ }
11104
11202
  async function startRemoteAuthLogin(serverId, context) {
11105
11203
  if (!context.authFlowStore || !context.controlCallbackBaseUrl) throw new CapletsError("REQUEST_INVALID", "Remote auth login is not available on this server");
11106
- const config = loadConfigWithSources(context.configPath, context.projectConfigPath).config;
11204
+ const config = loadConfigWithSources(context.configPath, context.projectConfigPath, { vaultResolver: vaultResolverForAuthDir(context.authDir) }).config;
11107
11205
  const target = await resolveAuthTarget$1(serverId, config, context.authDir);
11108
11206
  assertLoginTarget(target, serverId);
11109
11207
  const flowId = randomUUID();
@@ -11399,6 +11497,12 @@ function randomPairingPart(bytes) {
11399
11497
  const DEFAULT_PAIRING_CODE_TTL_MS = 10 * 6e4;
11400
11498
  const DEFAULT_PAIRING_CODE_MAX_ATTEMPTS = 5;
11401
11499
  const DEFAULT_ACCESS_TOKEN_TTL_MS = 15 * 6e4;
11500
+ const DEFAULT_PENDING_OPERATOR_CODE_TTL_MS = 10 * 6e4;
11501
+ const DEFAULT_PENDING_FLOW_TTL_MS = 1440 * 6e4;
11502
+ const DEFAULT_PENDING_POLL_INTERVAL_SECONDS = 5;
11503
+ const DEFAULT_PENDING_MAX_ACTIVE_FLOWS = 64;
11504
+ const DEFAULT_PENDING_MAX_ACTIVE_FLOWS_PER_SOURCE = 8;
11505
+ const PENDING_TERMINAL_RETENTION_MS = 1440 * 6e4;
11402
11506
  const STALE_REFRESH_REVOKE_GRACE_MS = 3e4;
11403
11507
  const SUPERSEDED_REFRESH_TOKEN_RETENTION_MS = 1440 * 6e4;
11404
11508
  const STATE_FILE = "remote-server-credentials.json";
@@ -11410,6 +11514,199 @@ var RemoteServerCredentialStore = class {
11410
11514
  constructor(options) {
11411
11515
  this.dir = options.dir;
11412
11516
  }
11517
+ createPendingLogin(input) {
11518
+ return this.withStateLock(() => {
11519
+ const now = input.now ?? /* @__PURE__ */ new Date();
11520
+ const flowId = `rlogin_${randomToken(12)}`;
11521
+ const operatorCode = `cap_login_${randomToken(5)}`;
11522
+ const pendingRefreshSecret = `cap_pending_refresh_${randomToken(32)}`;
11523
+ const pendingCompletionSecret = `cap_pending_complete_${randomToken(32)}`;
11524
+ const codeExpiresAt = new Date(now.getTime() + DEFAULT_PENDING_OPERATOR_CODE_TTL_MS).toISOString();
11525
+ const flowExpiresAt = new Date(now.getTime() + DEFAULT_PENDING_FLOW_TTL_MS).toISOString();
11526
+ const state = this.loadState();
11527
+ cleanupPendingLogins(state, now);
11528
+ const clientLabel = boundedPendingLoginDisplayValue(input.clientLabel, PENDING_CLIENT_LABEL_MAX_LENGTH) ?? "Caplets Remote Client";
11529
+ const clientFingerprint = boundedPendingLoginDisplayValue(input.clientFingerprint, PENDING_CLIENT_FINGERPRINT_MAX_LENGTH);
11530
+ const sourceHint = boundedPendingLoginDisplayValue(input.sourceHint, PENDING_SOURCE_HINT_MAX_LENGTH);
11531
+ enforcePendingLoginQuota(state, sourceHint);
11532
+ state.pendingLogins.push({
11533
+ flowId,
11534
+ hostUrl: normalizeRemoteProfileHostUrl(input.hostUrl),
11535
+ ...input.hostIdentity ? { hostIdentity: input.hostIdentity } : {},
11536
+ operatorCodeHash: hashSecret(operatorCode),
11537
+ pendingRefreshHash: hashSecret(pendingRefreshSecret),
11538
+ supersededPendingRefreshHashes: [],
11539
+ pendingCompletionHash: hashSecret(pendingCompletionSecret),
11540
+ operatorCodeFingerprint: pendingOperatorCodeFingerprint(operatorCode),
11541
+ clientLabel,
11542
+ ...clientFingerprint ? { clientFingerprint } : {},
11543
+ ...sourceHint ? { sourceHint } : {},
11544
+ createdAt: now.toISOString(),
11545
+ codeExpiresAt,
11546
+ flowExpiresAt,
11547
+ status: "pending"
11548
+ });
11549
+ this.saveState(state);
11550
+ return {
11551
+ flowId,
11552
+ operatorCode,
11553
+ operatorCodeFingerprint: pendingOperatorCodeFingerprint(operatorCode),
11554
+ pendingRefreshSecret,
11555
+ pendingCompletionSecret,
11556
+ codeExpiresAt,
11557
+ flowExpiresAt,
11558
+ intervalSeconds: DEFAULT_PENDING_POLL_INTERVAL_SECONDS
11559
+ };
11560
+ });
11561
+ }
11562
+ pollPendingLogin(input) {
11563
+ const now = input.now ?? /* @__PURE__ */ new Date();
11564
+ const flow = this.pendingLoginForCompletion(input.flowId, input.pendingCompletionSecret, now);
11565
+ return {
11566
+ flowId: flow.flowId,
11567
+ status: flow.status
11568
+ };
11569
+ }
11570
+ refreshPendingLogin(input) {
11571
+ return this.withStateLock(() => {
11572
+ const now = input.now ?? /* @__PURE__ */ new Date();
11573
+ const state = this.loadState();
11574
+ cleanupPendingLogins(state, now);
11575
+ const flow = this.pendingLoginForCompletion(input.flowId, input.pendingCompletionSecret, now, state);
11576
+ if (flow.status !== "pending") throw new CapletsError("AUTH_FAILED", `Pending login is already ${flow.status}.`);
11577
+ const refreshHash = hashSecret(input.pendingRefreshSecret);
11578
+ if (!safeHashEqual(refreshHash, flow.pendingRefreshHash)) {
11579
+ if (flow.pendingRefreshReplay && safeHashEqual(refreshHash, flow.pendingRefreshReplay.refreshHash) && Date.parse(flow.pendingRefreshReplay.expiresAt) > now.getTime()) return decryptPendingRefreshReplay(flow.pendingRefreshReplay, input.pendingCompletionSecret);
11580
+ if (flow.supersededPendingRefreshHashes.some((entry) => safeHashEqual(refreshHash, entry.hash))) throw new CapletsError("AUTH_FAILED", "Pending login refresh material is stale.");
11581
+ throw new CapletsError("AUTH_FAILED", "Pending login refresh material is invalid.");
11582
+ }
11583
+ const operatorCode = `cap_login_${randomToken(5)}`;
11584
+ const pendingRefreshSecret = `cap_pending_refresh_${randomToken(32)}`;
11585
+ const codeExpiresAt = new Date(now.getTime() + DEFAULT_PENDING_OPERATOR_CODE_TTL_MS).toISOString();
11586
+ const response = {
11587
+ flowId: flow.flowId,
11588
+ operatorCode,
11589
+ operatorCodeFingerprint: pendingOperatorCodeFingerprint(operatorCode),
11590
+ pendingRefreshSecret,
11591
+ codeExpiresAt,
11592
+ flowExpiresAt: flow.flowExpiresAt,
11593
+ intervalSeconds: DEFAULT_PENDING_POLL_INTERVAL_SECONDS
11594
+ };
11595
+ flow.operatorCodeHash = hashSecret(operatorCode);
11596
+ flow.operatorCodeFingerprint = response.operatorCodeFingerprint;
11597
+ flow.supersededPendingRefreshHashes = pruneSupersededRefreshTokens(flow.supersededPendingRefreshHashes, now);
11598
+ flow.pendingRefreshReplay = {
11599
+ refreshHash: flow.pendingRefreshHash,
11600
+ expiresAt: new Date(now.getTime() + STALE_REFRESH_REVOKE_GRACE_MS).toISOString(),
11601
+ encryptedResponse: encryptReplayValue(response, input.pendingCompletionSecret, now)
11602
+ };
11603
+ flow.supersededPendingRefreshHashes.push({
11604
+ hash: flow.pendingRefreshHash,
11605
+ supersededAt: now.toISOString()
11606
+ });
11607
+ flow.supersededPendingRefreshHashes = capSupersededRefreshTokens(flow.supersededPendingRefreshHashes);
11608
+ flow.pendingRefreshHash = hashSecret(pendingRefreshSecret);
11609
+ flow.codeExpiresAt = codeExpiresAt;
11610
+ this.saveState(state);
11611
+ return response;
11612
+ });
11613
+ }
11614
+ denyPendingLogin(input) {
11615
+ return this.withStateLock(() => {
11616
+ const now = input.now ?? /* @__PURE__ */ new Date();
11617
+ const state = this.loadState();
11618
+ cleanupPendingLogins(state, now);
11619
+ const operatorCodeHash = hashSecret(input.operatorCode);
11620
+ const flow = state.pendingLogins.find((candidate) => safeHashEqual(operatorCodeHash, candidate.operatorCodeHash));
11621
+ if (!flow) throw new CapletsError("AUTH_FAILED", "Pending login code is unknown.");
11622
+ if (flow.status !== "pending") throw new CapletsError("AUTH_FAILED", `Pending login is already ${flow.status}.`);
11623
+ if (Date.parse(flow.flowExpiresAt) <= now.getTime()) {
11624
+ flow.status = "expired";
11625
+ this.saveState(state);
11626
+ throw new CapletsError("AUTH_FAILED", "Pending login has expired.");
11627
+ }
11628
+ flow.status = "denied";
11629
+ flow.deniedAt = now.toISOString();
11630
+ this.saveState(state);
11631
+ return {
11632
+ flowId: flow.flowId,
11633
+ status: "denied"
11634
+ };
11635
+ });
11636
+ }
11637
+ cancelPendingLogin(input) {
11638
+ return this.withStateLock(() => {
11639
+ const now = input.now ?? /* @__PURE__ */ new Date();
11640
+ const state = this.loadState();
11641
+ cleanupPendingLogins(state, now);
11642
+ const flow = this.pendingLoginForCompletion(input.flowId, input.pendingCompletionSecret, now, state);
11643
+ if (flow.status !== "pending" && flow.status !== "approved") throw new CapletsError("AUTH_FAILED", `Pending login is already ${flow.status}.`);
11644
+ flow.status = "cancelled";
11645
+ flow.cancelledAt = now.toISOString();
11646
+ this.saveState(state);
11647
+ return {
11648
+ flowId: flow.flowId,
11649
+ status: "cancelled"
11650
+ };
11651
+ });
11652
+ }
11653
+ approvePendingLogin(input) {
11654
+ return this.withStateLock(() => {
11655
+ const now = input.now ?? /* @__PURE__ */ new Date();
11656
+ const state = this.loadState();
11657
+ cleanupPendingLogins(state, now);
11658
+ const operatorCodeHash = hashSecret(input.operatorCode);
11659
+ const flow = state.pendingLogins.find((candidate) => safeHashEqual(operatorCodeHash, candidate.operatorCodeHash));
11660
+ if (!flow) throw new CapletsError("AUTH_FAILED", "Pending login code is unknown.");
11661
+ if (flow.status !== "pending") throw new CapletsError("AUTH_FAILED", `Pending login is already ${flow.status}.`);
11662
+ if (Date.parse(flow.flowExpiresAt) <= now.getTime()) {
11663
+ flow.status = "expired";
11664
+ this.saveState(state);
11665
+ throw new CapletsError("AUTH_FAILED", "Pending login has expired.");
11666
+ }
11667
+ assertPendingOperatorCodeFresh(flow, now);
11668
+ flow.status = "approved";
11669
+ flow.approvedAt = now.toISOString();
11670
+ this.saveState(state);
11671
+ return pendingApprovalStatus(flow);
11672
+ });
11673
+ }
11674
+ completePendingLogin(input) {
11675
+ return this.withStateLock(() => {
11676
+ const now = input.now ?? /* @__PURE__ */ new Date();
11677
+ const state = this.loadState();
11678
+ cleanupPendingLogins(state, now);
11679
+ const flow = this.pendingLoginForCompletion(input.flowId, input.pendingCompletionSecret, now, state);
11680
+ if (flow.hostUrl !== normalizeRemoteProfileHostUrl(input.hostUrl)) throw new CapletsError("AUTH_FAILED", "Pending login belongs to a different host.");
11681
+ if (flow.status !== "approved") {
11682
+ if (flow.status === "exchanged" && flow.completionReplay && Date.parse(flow.completionReplay.expiresAt) > now.getTime()) return decryptCompletionReplay(flow.completionReplay, input.pendingCompletionSecret);
11683
+ throw new CapletsError("AUTH_FAILED", flow.status === "exchanged" ? "Pending login has already been exchanged." : `Pending login is ${flow.status}, not approved.`);
11684
+ }
11685
+ const accessToken = `cap_remote_access_${randomToken(32)}`;
11686
+ const refreshToken = `cap_remote_refresh_${randomToken(32)}`;
11687
+ const client = {
11688
+ clientId: `rcli_${randomToken(12)}`,
11689
+ clientLabel: flow.clientLabel,
11690
+ hostUrl: flow.hostUrl,
11691
+ accessTokenHash: hashSecret(accessToken),
11692
+ accessExpiresAt: new Date(now.getTime() + DEFAULT_ACCESS_TOKEN_TTL_MS).toISOString(),
11693
+ refreshTokenHash: hashSecret(refreshToken),
11694
+ supersededRefreshTokenHashes: [],
11695
+ refreshFamilyId: randomUUID(),
11696
+ createdAt: now.toISOString()
11697
+ };
11698
+ state.clients.push(client);
11699
+ flow.status = "exchanged";
11700
+ flow.exchangedAt = now.toISOString();
11701
+ const credentials = credentialsFromClient(client, accessToken, refreshToken);
11702
+ flow.completionReplay = {
11703
+ expiresAt: new Date(now.getTime() + STALE_REFRESH_REVOKE_GRACE_MS).toISOString(),
11704
+ encryptedCredentials: encryptReplayValue(credentials, input.pendingCompletionSecret, now)
11705
+ };
11706
+ this.saveState(state);
11707
+ return credentials;
11708
+ });
11709
+ }
11413
11710
  createPairingCode(input) {
11414
11711
  return this.withStateLock(() => {
11415
11712
  const now = input.now ?? /* @__PURE__ */ new Date();
@@ -11474,6 +11771,13 @@ var RemoteServerCredentialStore = class {
11474
11771
  listClients() {
11475
11772
  return this.loadState().clients.map(clientStatus).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
11476
11773
  }
11774
+ listPendingLogins(now = /* @__PURE__ */ new Date()) {
11775
+ return this.withStateLock(() => {
11776
+ const state = this.loadState();
11777
+ if (cleanupPendingLogins(state, now)) this.saveState(state);
11778
+ return state.pendingLogins.map(pendingLoginStatus).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
11779
+ });
11780
+ }
11477
11781
  revokeClient(clientId, now = /* @__PURE__ */ new Date()) {
11478
11782
  return this.withStateLock(() => {
11479
11783
  const state = this.loadState();
@@ -11509,7 +11813,7 @@ var RemoteServerCredentialStore = class {
11509
11813
  const supersededAt = superseded ? Date.parse(superseded.supersededAt) : NaN;
11510
11814
  if (Number.isFinite(supersededAt) && now.getTime() - supersededAt >= STALE_REFRESH_REVOKE_GRACE_MS) replayedClient.revokedAt = now.toISOString();
11511
11815
  this.saveState(state);
11512
- throw new CapletsError("AUTH_FAILED", "Remote refresh credential is stale.");
11816
+ throw new CapletsError("REMOTE_CREDENTIALS_REVOKED", "Remote refresh credential is stale.");
11513
11817
  }
11514
11818
  throw new CapletsError("AUTH_FAILED", "Remote refresh credential is invalid.");
11515
11819
  }
@@ -11537,12 +11841,17 @@ var RemoteServerCredentialStore = class {
11537
11841
  if (!existsSync(path)) return {
11538
11842
  version: 1,
11539
11843
  pairingCodes: [],
11844
+ pendingLogins: [],
11540
11845
  clients: []
11541
11846
  };
11542
11847
  const parsed = JSON.parse(readFileSync(path, "utf8"));
11543
11848
  return {
11544
11849
  version: 1,
11545
11850
  pairingCodes: parsed.pairingCodes ?? [],
11851
+ pendingLogins: (parsed.pendingLogins ?? []).map((pending) => ({
11852
+ ...pending,
11853
+ supersededPendingRefreshHashes: parseSupersededRefreshTokens(pending.supersededPendingRefreshHashes)
11854
+ })),
11546
11855
  clients: (parsed.clients ?? []).map((client) => ({
11547
11856
  ...client,
11548
11857
  supersededRefreshTokenHashes: parseSupersededRefreshTokens(client.supersededRefreshTokenHashes)
@@ -11612,6 +11921,13 @@ var RemoteServerCredentialStore = class {
11612
11921
  return false;
11613
11922
  }
11614
11923
  }
11924
+ pendingLoginForCompletion(flowId, pendingCompletionSecret, now, state = this.loadState()) {
11925
+ const flow = state.pendingLogins.find((candidate) => candidate.flowId === flowId);
11926
+ if (!flow) throw new CapletsError("AUTH_FAILED", "Pending login is unknown.");
11927
+ if (!safeHashEqual(hashSecret(pendingCompletionSecret), flow.pendingCompletionHash)) throw new CapletsError("AUTH_FAILED", "Pending login possession material is invalid.");
11928
+ if (Date.parse(flow.flowExpiresAt) <= now.getTime() && isActivePendingLogin(flow)) flow.status = "expired";
11929
+ return flow;
11930
+ }
11615
11931
  };
11616
11932
  function sleepSync(ms) {
11617
11933
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -11620,14 +11936,92 @@ function isFileExistsError(error) {
11620
11936
  return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
11621
11937
  }
11622
11938
  function pruneSupersededRefreshTokens(entries, now) {
11623
- return entries.filter((entry) => {
11939
+ return capSupersededRefreshTokens(entries.filter((entry) => {
11624
11940
  const supersededAt = Date.parse(entry.supersededAt);
11625
11941
  return Number.isFinite(supersededAt) && now.getTime() - supersededAt < SUPERSEDED_REFRESH_TOKEN_RETENTION_MS;
11942
+ }));
11943
+ }
11944
+ function capSupersededRefreshTokens(entries) {
11945
+ return entries.slice(-16);
11946
+ }
11947
+ function cleanupPendingLogins(state, now) {
11948
+ let changed = false;
11949
+ for (const flow of state.pendingLogins) if (isActivePendingLogin(flow) && Date.parse(flow.flowExpiresAt) <= now.getTime()) {
11950
+ flow.status = "expired";
11951
+ changed = true;
11952
+ }
11953
+ const retained = state.pendingLogins.filter((flow) => shouldRetainPendingLogin(flow, now));
11954
+ if (retained.length !== state.pendingLogins.length) changed = true;
11955
+ state.pendingLogins = retained;
11956
+ return changed;
11957
+ }
11958
+ function enforcePendingLoginQuota(state, sourceHint) {
11959
+ const active = state.pendingLogins.filter(isActivePendingLogin);
11960
+ if (active.length >= DEFAULT_PENDING_MAX_ACTIVE_FLOWS) throw new CapletsError("AUTH_FAILED", "Too many active pending logins.");
11961
+ if (!sourceHint) return;
11962
+ const sourceKey = sourceHint;
11963
+ if (active.filter((flow) => (flow.sourceHint ?? "") === sourceKey).length >= DEFAULT_PENDING_MAX_ACTIVE_FLOWS_PER_SOURCE) throw new CapletsError("AUTH_FAILED", "Too many active pending logins for this source.");
11964
+ }
11965
+ function isActivePendingLogin(flow) {
11966
+ return flow.status === "pending" || flow.status === "approved";
11967
+ }
11968
+ function shouldRetainPendingLogin(flow, now) {
11969
+ if (isActivePendingLogin(flow)) return true;
11970
+ const terminalAt = pendingLoginTerminalTime(flow);
11971
+ return Number.isFinite(terminalAt) && now.getTime() - terminalAt < PENDING_TERMINAL_RETENTION_MS;
11972
+ }
11973
+ function pendingLoginTerminalTime(flow) {
11974
+ switch (flow.status) {
11975
+ case "denied": return Date.parse(flow.deniedAt ?? flow.flowExpiresAt);
11976
+ case "cancelled": return Date.parse(flow.cancelledAt ?? flow.flowExpiresAt);
11977
+ case "exchanged": return Date.parse(flow.exchangedAt ?? flow.flowExpiresAt);
11978
+ case "expired": return Date.parse(flow.flowExpiresAt);
11979
+ default: return NaN;
11980
+ }
11981
+ }
11982
+ function assertPendingOperatorCodeFresh(flow, now) {
11983
+ if (Date.parse(flow.codeExpiresAt) <= now.getTime()) throw new CapletsError("AUTH_FAILED", "Pending login code has expired. Refresh the pending login for a new code.");
11984
+ }
11985
+ function encryptReplayValue(value, pendingCompletionSecret, now) {
11986
+ return encryptVaultValue({
11987
+ plaintext: JSON.stringify(value),
11988
+ key: replayEncryptionKey(pendingCompletionSecret),
11989
+ now
11626
11990
  });
11627
11991
  }
11992
+ function decryptPendingRefreshReplay(replay, pendingCompletionSecret) {
11993
+ const parsed = JSON.parse(decryptVaultValue(replay.encryptedResponse, replayEncryptionKey(pendingCompletionSecret)));
11994
+ if (typeof parsed.flowId !== "string" || typeof parsed.operatorCode !== "string" || typeof parsed.pendingRefreshSecret !== "string" || typeof parsed.codeExpiresAt !== "string" || typeof parsed.flowExpiresAt !== "string" || typeof parsed.intervalSeconds !== "number") throw new CapletsError("CONFIG_INVALID", "Pending login refresh replay record is malformed.");
11995
+ return {
11996
+ flowId: parsed.flowId,
11997
+ operatorCode: parsed.operatorCode,
11998
+ operatorCodeFingerprint: typeof parsed.operatorCodeFingerprint === "string" ? parsed.operatorCodeFingerprint : pendingOperatorCodeFingerprint(parsed.operatorCode),
11999
+ pendingRefreshSecret: parsed.pendingRefreshSecret,
12000
+ codeExpiresAt: parsed.codeExpiresAt,
12001
+ flowExpiresAt: parsed.flowExpiresAt,
12002
+ intervalSeconds: parsed.intervalSeconds
12003
+ };
12004
+ }
12005
+ function decryptCompletionReplay(replay, pendingCompletionSecret) {
12006
+ const parsed = JSON.parse(decryptVaultValue(replay.encryptedCredentials, replayEncryptionKey(pendingCompletionSecret)));
12007
+ if (typeof parsed.clientId !== "string" || typeof parsed.clientLabel !== "string" || typeof parsed.hostUrl !== "string" || typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string" || typeof parsed.expiresAt !== "string" || typeof parsed.createdAt !== "string" || parsed.tokenType !== "Bearer") throw new CapletsError("CONFIG_INVALID", "Pending login completion replay record is malformed.");
12008
+ return {
12009
+ clientId: parsed.clientId,
12010
+ clientLabel: parsed.clientLabel,
12011
+ hostUrl: parsed.hostUrl,
12012
+ accessToken: parsed.accessToken,
12013
+ refreshToken: parsed.refreshToken,
12014
+ expiresAt: parsed.expiresAt,
12015
+ createdAt: parsed.createdAt,
12016
+ tokenType: "Bearer"
12017
+ };
12018
+ }
12019
+ function replayEncryptionKey(pendingCompletionSecret) {
12020
+ return createHash("sha256").update(`caplets-pending-login-replay:${pendingCompletionSecret}`).digest();
12021
+ }
11628
12022
  function validateClient(client, hostUrl, now, options = {}) {
11629
12023
  if (client.hostUrl !== hostUrl) throw new CapletsError("AUTH_FAILED", "Remote client credential is for a different host.");
11630
- if (client.revokedAt) throw new CapletsError("AUTH_FAILED", "Remote client credential has been revoked.");
12024
+ if (client.revokedAt) throw new CapletsError("REMOTE_CREDENTIALS_REVOKED", "Remote client credential has been revoked.");
11631
12025
  if (!options.allowExpiredAccess && Date.parse(client.accessExpiresAt) <= now.getTime()) throw new CapletsError("AUTH_FAILED", "Remote client credential has expired.");
11632
12026
  }
11633
12027
  function credentialsFromClient(client, accessToken, refreshToken) {
@@ -11652,9 +12046,48 @@ function clientStatus(client) {
11652
12046
  ...client.revokedAt ? { revokedAt: client.revokedAt } : {}
11653
12047
  };
11654
12048
  }
12049
+ function pendingLoginStatus(flow) {
12050
+ return {
12051
+ flowId: flow.flowId,
12052
+ hostUrl: flow.hostUrl,
12053
+ ...flow.hostIdentity ? { hostIdentity: flow.hostIdentity } : {},
12054
+ status: flow.status,
12055
+ ...flow.operatorCodeFingerprint ? { operatorCodeFingerprint: flow.operatorCodeFingerprint } : {},
12056
+ clientLabel: flow.clientLabel,
12057
+ ...flow.clientFingerprint ? { clientFingerprint: flow.clientFingerprint } : {},
12058
+ ...flow.sourceHint ? { sourceHint: flow.sourceHint } : {},
12059
+ createdAt: flow.createdAt,
12060
+ codeExpiresAt: flow.codeExpiresAt,
12061
+ flowExpiresAt: flow.flowExpiresAt,
12062
+ ...flow.approvedAt ? { approvedAt: flow.approvedAt } : {},
12063
+ ...flow.deniedAt ? { deniedAt: flow.deniedAt } : {},
12064
+ ...flow.cancelledAt ? { cancelledAt: flow.cancelledAt } : {},
12065
+ ...flow.exchangedAt ? { exchangedAt: flow.exchangedAt } : {}
12066
+ };
12067
+ }
12068
+ function pendingApprovalStatus(flow) {
12069
+ return {
12070
+ flowId: flow.flowId,
12071
+ status: "approved",
12072
+ clientLabel: flow.clientLabel,
12073
+ ...flow.clientFingerprint ? { clientFingerprint: flow.clientFingerprint } : {},
12074
+ ...flow.sourceHint ? { sourceHint: flow.sourceHint } : {}
12075
+ };
12076
+ }
11655
12077
  function hashSecret(secret) {
11656
12078
  return createHash("sha256").update(secret).digest("base64url");
11657
12079
  }
12080
+ const PENDING_CLIENT_LABEL_MAX_LENGTH = 120;
12081
+ const PENDING_CLIENT_FINGERPRINT_MAX_LENGTH = 256;
12082
+ const PENDING_SOURCE_HINT_MAX_LENGTH = 256;
12083
+ function boundedPendingLoginDisplayValue(value, maxLength) {
12084
+ const trimmed = value?.trim();
12085
+ if (!trimmed) return void 0;
12086
+ return trimmed.length > maxLength ? trimmed.slice(0, maxLength) : trimmed;
12087
+ }
12088
+ function pendingOperatorCodeFingerprint(operatorCode) {
12089
+ return hashSecret(operatorCode).slice(0, 8);
12090
+ }
11658
12091
  function safeHashEqual(left, right) {
11659
12092
  const leftBuffer = Buffer$1.from(left);
11660
12093
  const rightBuffer = Buffer$1.from(right);
@@ -11687,41 +12120,95 @@ function createHttpServeApp(options, engine, io = {}) {
11687
12120
  const remoteCredentialStore = remoteCredentialStoreForOptions(options, io.remoteCredentialStore);
11688
12121
  if (options.auth.type === "remote_credentials" && options.trustProxy === true && options.publicOrigin === void 0) throw new CapletsError("REQUEST_INVALID", "Remote credential auth with --trust-proxy requires CAPLETS_SERVER_URL.");
11689
12122
  const protectedRouteAuth = routeAuth(options, remoteCredentialStore, paths.base);
12123
+ const attachHostProtection = dnsRebindingProtection(options);
11690
12124
  app.use("*", logger((message, ...rest) => {
11691
12125
  writeErr(`${[message, ...rest].join(" ")}\n`);
11692
12126
  }));
11693
- app.get(paths.base, (c) => c.json({
11694
- name: "caplets",
11695
- transport: "http",
11696
- base: paths.base,
11697
- versions: [versionDiscovery(paths, exposeAttach)],
11698
- auth: { type: options.auth.type }
11699
- }));
11700
- app.get(paths.version, (c) => c.json(versionDiscovery(paths, exposeAttach)));
12127
+ app.get(paths.base, (c) => {
12128
+ const remote = remoteCredentialStore ? remoteHostMetadata(c.req.url, paths.base, options, (name) => c.req.header(name)) : void 0;
12129
+ return c.json({
12130
+ name: "caplets",
12131
+ transport: "http",
12132
+ base: paths.base,
12133
+ versions: [versionDiscovery(paths, exposeAttach, remote)],
12134
+ auth: { type: options.auth.type },
12135
+ ...remote ? { remote } : {}
12136
+ });
12137
+ });
12138
+ app.get(paths.version, (c) => {
12139
+ const remote = remoteCredentialStore ? remoteHostMetadata(c.req.url, paths.base, options, (name) => c.req.header(name)) : void 0;
12140
+ return c.json(versionDiscovery(paths, exposeAttach, remote));
12141
+ });
11701
12142
  app.get(paths.health, (c) => c.json({ status: "ok" }));
11702
12143
  if (remoteCredentialStore) {
11703
- app.post(paths.pairingExchange, async (c) => {
12144
+ app.post(paths.remoteLoginStart, attachHostProtection, async (c) => {
11704
12145
  try {
11705
- const parsed = await parseJsonObject(c.req.json(), "Pairing exchange request");
11706
- const code = stringField(parsed, "code");
12146
+ const parsed = await parseJsonObject(c.req.json(), "Pending remote login start request");
11707
12147
  const clientLabel = optionalStringField(parsed, "clientLabel");
11708
- const credentials = remoteCredentialStore.exchangePairingCode({
11709
- hostUrl: remoteCredentialHostUrl(c.req.url, paths.base, options.publicOrigin, options.trustProxy, (name) => c.req.header(name)),
11710
- code,
11711
- ...clientLabel ? { clientLabel } : {}
12148
+ const clientFingerprint = optionalStringField(parsed, "clientFingerprint");
12149
+ const hostUrl = remoteCredentialHostUrl(c.req.url, paths.base, options.publicOrigin, options.trustProxy, (name) => c.req.header(name));
12150
+ const pending = remoteCredentialStore.createPendingLogin({
12151
+ hostUrl,
12152
+ hostIdentity: hostUrl,
12153
+ ...clientLabel ? { clientLabel } : {},
12154
+ ...remoteCredentialSourceHint(options.trustProxy, (name) => c.req.header(name)),
12155
+ ...clientFingerprint ? { clientFingerprint } : {}
11712
12156
  });
11713
- return c.json({
11714
- clientId: credentials.clientId,
11715
- clientLabel: credentials.clientLabel,
11716
- accessToken: credentials.accessToken,
11717
- refreshToken: credentials.refreshToken,
11718
- tokenType: credentials.tokenType,
11719
- expiresAt: credentials.expiresAt
12157
+ return c.json(pending);
12158
+ } catch (error) {
12159
+ return remoteCredentialErrorResponse(error);
12160
+ }
12161
+ });
12162
+ app.post(paths.remoteLoginPoll, attachHostProtection, async (c) => {
12163
+ try {
12164
+ const parsed = await parseJsonObject(c.req.json(), "Pending remote login poll request");
12165
+ return c.json(remoteCredentialStore.pollPendingLogin({
12166
+ flowId: stringField(parsed, "flowId"),
12167
+ pendingCompletionSecret: stringField(parsed, "pendingCompletionSecret")
12168
+ }));
12169
+ } catch (error) {
12170
+ return remoteCredentialErrorResponse(error);
12171
+ }
12172
+ });
12173
+ app.post(paths.remoteLoginRefresh, attachHostProtection, async (c) => {
12174
+ try {
12175
+ const parsed = await parseJsonObject(c.req.json(), "Pending remote login refresh request");
12176
+ return c.json(remoteCredentialStore.refreshPendingLogin({
12177
+ flowId: stringField(parsed, "flowId"),
12178
+ pendingRefreshSecret: stringField(parsed, "pendingRefreshSecret"),
12179
+ pendingCompletionSecret: stringField(parsed, "pendingCompletionSecret")
12180
+ }));
12181
+ } catch (error) {
12182
+ return remoteCredentialErrorResponse(error);
12183
+ }
12184
+ });
12185
+ app.post(paths.remoteLoginComplete, attachHostProtection, async (c) => {
12186
+ try {
12187
+ const parsed = await parseJsonObject(c.req.json(), "Pending remote login complete request");
12188
+ const credentials = remoteCredentialStore.completePendingLogin({
12189
+ hostUrl: remoteCredentialHostUrl(c.req.url, paths.base, options.publicOrigin, options.trustProxy, (name) => c.req.header(name)),
12190
+ flowId: stringField(parsed, "flowId"),
12191
+ pendingCompletionSecret: stringField(parsed, "pendingCompletionSecret")
11720
12192
  });
12193
+ return c.json(credentials);
12194
+ } catch (error) {
12195
+ return remoteCredentialErrorResponse(error);
12196
+ }
12197
+ });
12198
+ app.post(paths.remoteLoginCancel, attachHostProtection, async (c) => {
12199
+ try {
12200
+ const parsed = await parseJsonObject(c.req.json(), "Pending remote login cancel request");
12201
+ return c.json(remoteCredentialStore.cancelPendingLogin({
12202
+ flowId: stringField(parsed, "flowId"),
12203
+ pendingCompletionSecret: stringField(parsed, "pendingCompletionSecret")
12204
+ }));
11721
12205
  } catch (error) {
11722
12206
  return remoteCredentialErrorResponse(error);
11723
12207
  }
11724
12208
  });
12209
+ app.post(paths.pairingExchange, async (_c) => {
12210
+ return remoteCredentialErrorResponse(legacyPairingCodeUnsupportedError());
12211
+ });
11725
12212
  app.post(paths.remoteRefresh, async (c) => {
11726
12213
  try {
11727
12214
  const refreshToken = stringField(await parseJsonObject(c.req.json(), "Remote refresh request"), "refreshToken");
@@ -11784,7 +12271,6 @@ function createHttpServeApp(options, engine, io = {}) {
11784
12271
  sessions.set(nextSessionId, session);
11785
12272
  return session.transport.handleRequest(c);
11786
12273
  });
11787
- const attachHostProtection = dnsRebindingProtection(options);
11788
12274
  if (exposeAttach) {
11789
12275
  app.get(paths.attachManifest, attachHostProtection, protectedRouteAuth, async (c) => {
11790
12276
  const attachProjection = await buildAttachProjection(engine);
@@ -11899,13 +12385,26 @@ function remoteCredentialHostUrl(requestUrl, basePath, publicOrigin, trustProxy,
11899
12385
  if (trustProxy && !publicOrigin) throw new CapletsError("REQUEST_INVALID", "Remote credential auth with --trust-proxy requires CAPLETS_SERVER_URL.");
11900
12386
  return publicHostUrl(requestUrl, basePath, publicOrigin, trustProxy, header);
11901
12387
  }
12388
+ function remoteCredentialSourceHint(trustProxy, header) {
12389
+ if (!trustProxy) return {};
12390
+ const sourceHint = firstForwardedValue(header("x-forwarded-for")) ?? firstForwardedValue(header("x-real-ip")) ?? firstForwardedValue(header("cf-connecting-ip"));
12391
+ return sourceHint ? { sourceHint } : {};
12392
+ }
11902
12393
  function firstForwardedValue(value) {
11903
12394
  return value?.split(",", 1)[0]?.trim() || void 0;
11904
12395
  }
11905
- function versionDiscovery(paths, exposeAttach = true) {
12396
+ function remoteHostMetadata(requestUrl, basePath, options, header) {
12397
+ const audience = remoteCredentialHostUrl(requestUrl, basePath, options.publicOrigin, options.trustProxy, header);
12398
+ return {
12399
+ hostIdentity: audience,
12400
+ audience
12401
+ };
12402
+ }
12403
+ function versionDiscovery(paths, exposeAttach = true, remote) {
11906
12404
  return {
11907
12405
  version: 1,
11908
12406
  path: paths.version,
12407
+ ...remote ? { remote } : {},
11909
12408
  links: {
11910
12409
  mcp: paths.mcp,
11911
12410
  admin: paths.control,
@@ -11978,6 +12477,7 @@ function attachEventsResponse(engine, activeStreams) {
11978
12477
  async function serveHttp(options, engineOptions = {}, writeErr = (value) => process.stderr.write(value)) {
11979
12478
  const resolvedEngineOptions = {
11980
12479
  exposeLocalArtifactPaths: false,
12480
+ vaultRecoveryTarget: "remote",
11981
12481
  ...engineOptions
11982
12482
  };
11983
12483
  const engine = new CapletsEngine(resolvedEngineOptions);
@@ -12054,6 +12554,11 @@ function servicePaths(base) {
12054
12554
  attachInvoke: routePath(attach, "invoke"),
12055
12555
  projectBindings: routePath(attach, "project-bindings"),
12056
12556
  pairingExchange: routePath(remote, "pairing/exchange"),
12557
+ remoteLoginStart: routePath(remote, "login/start"),
12558
+ remoteLoginPoll: routePath(remote, "login/poll"),
12559
+ remoteLoginRefresh: routePath(remote, "login/refresh"),
12560
+ remoteLoginComplete: routePath(remote, "login/complete"),
12561
+ remoteLoginCancel: routePath(remote, "login/cancel"),
12057
12562
  remoteRefresh: routePath(remote, "refresh"),
12058
12563
  remoteClient: routePath(remote, "client"),
12059
12564
  health: routePath(version, "healthz")
@@ -12138,6 +12643,9 @@ function remoteCredentialErrorResponse(error) {
12138
12643
  error: safe
12139
12644
  }, { status });
12140
12645
  }
12646
+ function legacyPairingCodeUnsupportedError() {
12647
+ return new CapletsError("REQUEST_INVALID", "Self-hosted Pairing Code exchange is no longer supported. Run caplets remote login <url> and approve the pending login from the host.");
12648
+ }
12141
12649
  function dnsRebindingProtection(options) {
12142
12650
  if (!options.loopback) return async (_c, next) => {
12143
12651
  await next();
@@ -12366,7 +12874,7 @@ async function installDaemon(install = {}, options = {}) {
12366
12874
  "write-descriptor",
12367
12875
  "register-service"
12368
12876
  ];
12369
- const existingNative = persisted || existsSync(paths.descriptorFile) ? await manager.status(persisted, paths) : void 0;
12877
+ const existingNative = persisted || existsSync(daemonHostPath(paths.descriptorFile)) ? await manager.status(persisted, paths) : void 0;
12370
12878
  const restartDecisionRequired = existingNative?.running === true && !install.start && !install.restart && !install.noRestart;
12371
12879
  if (restartDecisionRequired && !install.dryRun && (!options.isInteractive || !options.readPrompt)) throw new CapletsError("REQUEST_INVALID", "Daemon is already running; rerun with --restart, --start, or --no-restart.");
12372
12880
  if (install.dryRun) return {
@@ -12387,13 +12895,13 @@ async function installDaemon(install = {}, options = {}) {
12387
12895
  });
12388
12896
  assertDaemonHealth(validation, "Daemon install validation");
12389
12897
  }
12390
- mkdirSync(paths.logDir, {
12898
+ mkdirSync(daemonHostPath(paths.logDir), {
12391
12899
  recursive: true,
12392
12900
  mode: 448
12393
12901
  });
12394
12902
  ensureDaemonLogFiles(paths);
12395
12903
  const persistenceBackups = backupPersistenceFiles([paths.configFile, paths.stateFile]);
12396
- const hadExistingDescriptor = existsSync(paths.descriptorFile);
12904
+ const hadExistingDescriptor = existsSync(daemonHostPath(paths.descriptorFile));
12397
12905
  let native;
12398
12906
  try {
12399
12907
  writeDaemonConfig(paths, config);
@@ -12543,11 +13051,11 @@ async function uninstallDaemon(uninstall = {}, options = {}) {
12543
13051
  if (uninstall.purge) {
12544
13052
  removeDaemonConfig(paths);
12545
13053
  removeDaemonState(paths);
12546
- rmSync(paths.logDir, {
13054
+ rmSync(daemonHostPath(paths.logDir), {
12547
13055
  recursive: true,
12548
13056
  force: true
12549
13057
  });
12550
- rmSync(dirname(paths.configFile), {
13058
+ rmSync(dirname(daemonHostPath(paths.configFile)), {
12551
13059
  recursive: true,
12552
13060
  force: true
12553
13061
  });
@@ -12787,25 +13295,32 @@ function mergeServeOptions(existing, install) {
12787
13295
  };
12788
13296
  }
12789
13297
  function backupPersistenceFiles(paths) {
12790
- return paths.map((path) => ({
12791
- path,
12792
- existed: existsSync(path),
12793
- ...existsSync(path) ? { contents: readFileSync(path) } : {},
12794
- ...existsSync(path) ? { mode: statSync(path).mode & 511 } : {}
12795
- }));
13298
+ return paths.map((path) => {
13299
+ const hostPath = daemonHostPath(path);
13300
+ const existed = existsSync(hostPath);
13301
+ return {
13302
+ path,
13303
+ existed,
13304
+ ...existed ? { contents: readFileSync(hostPath) } : {},
13305
+ ...existed ? { mode: statSync(hostPath).mode & 511 } : {}
13306
+ };
13307
+ });
12796
13308
  }
12797
13309
  function restorePersistenceFiles(backups) {
12798
- for (const backup of backups) if (backup.existed && backup.contents) {
12799
- mkdirSync(dirname(backup.path), {
13310
+ for (const backup of backups) {
13311
+ const hostPath = daemonHostPath(backup.path);
13312
+ if (backup.existed && backup.contents) {
13313
+ mkdirSync(dirname(hostPath), {
13314
+ recursive: true,
13315
+ mode: 448
13316
+ });
13317
+ writeFileSync(hostPath, backup.contents, { mode: backup.mode ?? 384 });
13318
+ chmodSync(hostPath, backup.mode ?? 384);
13319
+ } else rmSync(hostPath, {
12800
13320
  recursive: true,
12801
- mode: 448
13321
+ force: true
12802
13322
  });
12803
- writeFileSync(backup.path, backup.contents, { mode: backup.mode ?? 384 });
12804
- chmodSync(backup.path, backup.mode ?? 384);
12805
- } else rmSync(backup.path, {
12806
- recursive: true,
12807
- force: true
12808
- });
13323
+ }
12809
13324
  }
12810
13325
  async function rollbackNativeInstall(manager, persisted, paths, hadExistingDescriptor) {
12811
13326
  try {
@@ -12857,6 +13372,7 @@ async function doctorJsonReport(options = {}) {
12857
13372
  },
12858
13373
  daemon: await resolveDaemonSection(env, options.daemon),
12859
13374
  remoteLogin: remoteLogin.report,
13375
+ vault: resolveVaultSection(env, root),
12860
13376
  exposure: await resolveExposureSection(env),
12861
13377
  codeMode: await resolveCodeModeSection(options, env)
12862
13378
  };
@@ -12907,6 +13423,11 @@ async function formatDoctorReport(options = {}) {
12907
13423
  ...report.remoteLogin.workspaceSlug || report.remoteLogin.workspaceId ? [` Selected Workspace: ${report.remoteLogin.workspaceSlug ?? report.remoteLogin.workspaceId}`] : [],
12908
13424
  ...report.remoteLogin.clientId ? [` Client: ${report.remoteLogin.clientId}`] : [],
12909
13425
  "",
13426
+ "Vault",
13427
+ ` OK: ${yesNo(Boolean(report.vault.ok))}`,
13428
+ ...!report.vault.ok && typeof report.vault.message === "string" ? [` Error: ${report.vault.message}`] : [],
13429
+ ...Array.isArray(report.vault.issues) ? report.vault.issues.map((issue) => ` ${issue.capletId}: ${issue.reason} ${issue.key} (${issue.recoveryCommand})`) : [],
13430
+ "",
12910
13431
  "Exposure",
12911
13432
  ` Default: ${report.exposure.default ?? "unknown"}`,
12912
13433
  ` Discovery timeout: ${report.exposure.discoveryTimeoutMs ?? "unknown"}ms`,
@@ -12924,6 +13445,37 @@ async function formatDoctorReport(options = {}) {
12924
13445
  ...observedOutputShapePath(report.codeMode.observedOutputShapes) ? [` Observed output shape cache: ${observedOutputShapePath(report.codeMode.observedOutputShapes)}`] : []
12925
13446
  ].join("\n")}\n`;
12926
13447
  }
13448
+ function resolveVaultSection(env, cwd = process.cwd()) {
13449
+ const configPath = env.CAPLETS_CONFIG?.trim() ? env.CAPLETS_CONFIG.trim() : resolveConfigPath();
13450
+ const projectConfigPath = env.CAPLETS_PROJECT_CONFIG?.trim() ? env.CAPLETS_PROJECT_CONFIG.trim() : resolveProjectConfigPath(cwd);
13451
+ try {
13452
+ const issues = loadLocalOverlayConfigWithSources(configPath, projectConfigPath).warnings.filter((warning) => warning.message.includes("Vault key")).map((warning) => vaultIssueFromWarning(warning.message, warning.path)).filter((issue) => issue !== void 0);
13453
+ return {
13454
+ ok: issues.length === 0,
13455
+ issues
13456
+ };
13457
+ } catch (error) {
13458
+ return {
13459
+ ok: false,
13460
+ issues: [],
13461
+ message: error instanceof Error ? error.message : String(error)
13462
+ };
13463
+ }
13464
+ }
13465
+ function vaultIssueFromWarning(message, path) {
13466
+ const match = message.match(/^Caplet ([^ ]+) references ([^ ]+) Vault key ([^ ]+) at ([^;]+); run `([^`]+)`/u);
13467
+ if (!match) return void 0;
13468
+ const recoveryCommand = match[5] ?? "";
13469
+ return {
13470
+ capletId: match[1],
13471
+ reason: match[2],
13472
+ key: match[3],
13473
+ configPath: path,
13474
+ referencePath: match[4],
13475
+ target: recoveryCommand.includes("--remote") ? "remote" : "global",
13476
+ recoveryCommand
13477
+ };
13478
+ }
12927
13479
  async function resolveDaemonSection(env, options) {
12928
13480
  try {
12929
13481
  const status = await daemonStatus({
@@ -13258,6 +13810,42 @@ async function openBrowserUrl(url, options = {}) {
13258
13810
  });
13259
13811
  }
13260
13812
  //#endregion
13813
+ //#region src/cli/vault.ts
13814
+ function formatVaultValueStatus(status, json = false) {
13815
+ if (json) return `${JSON.stringify(status, null, 2)}\n`;
13816
+ if (!status.present) return `Vault key ${status.key} is not set.\n`;
13817
+ return [
13818
+ `Vault key ${status.key} is set.`,
13819
+ status.valueBytes === void 0 ? void 0 : `Value bytes: ${status.valueBytes}`,
13820
+ status.updatedAt === void 0 ? void 0 : `Updated: ${status.updatedAt}`
13821
+ ].filter((line) => line !== void 0).join("\n").concat("\n");
13822
+ }
13823
+ function formatVaultValueList(statuses, json = false) {
13824
+ if (json) return `${JSON.stringify(statuses, null, 2)}\n`;
13825
+ if (statuses.length === 0) return "No Vault keys set.\n";
13826
+ return `${statuses.map((status) => status.key).join("\n")}\n`;
13827
+ }
13828
+ function formatVaultDeleteStatus(status, json = false) {
13829
+ if (json) return `${JSON.stringify(status, null, 2)}\n`;
13830
+ return status.deleted ? `Deleted Vault key ${status.key}. ${status.grantsRetained} access grant${status.grantsRetained === 1 ? "" : "s"} retained.\n` : `No Vault key ${status.key} found.\n`;
13831
+ }
13832
+ function formatVaultAccessGrant(grant, json = false) {
13833
+ if (json) return `${JSON.stringify(grant, null, 2)}\n`;
13834
+ return `Granted Vault key ${grant.storedKey} to ${grant.capletId} as ${grant.referenceName}.\n`;
13835
+ }
13836
+ function formatVaultAccessList(grants, json = false) {
13837
+ if (json) return `${JSON.stringify(grants, null, 2)}\n`;
13838
+ if (grants.length === 0) return "No Vault access grants.\n";
13839
+ return `${grants.map((grant) => {
13840
+ const origin = grant.origin ? ` (${grant.origin.kind} ${grant.origin.path})` : "";
13841
+ return `${grant.storedKey} -> ${grant.capletId}:${grant.referenceName}${origin}`;
13842
+ }).join("\n")}\n`;
13843
+ }
13844
+ function formatVaultAccessRevoke(count, json = false) {
13845
+ if (json) return `${JSON.stringify({ revoked: count }, null, 2)}\n`;
13846
+ return `Revoked ${count} Vault access grant${count === 1 ? "" : "s"}.\n`;
13847
+ }
13848
+ //#endregion
13261
13849
  //#region src/setup/hash.ts
13262
13850
  function capletSetupContentHash(caplet) {
13263
13851
  return createHash("sha256").update(stableJson(stableCapletForHash(caplet))).digest("hex");
@@ -13820,11 +14408,7 @@ function remoteSetupDefinition(id, options) {
13820
14408
  type: "command",
13821
14409
  label: "Add remote-backed Caplets MCP server to Codex",
13822
14410
  command: "codex",
13823
- args: codexMcpAddArgs([
13824
- "attach",
13825
- "--remote-url",
13826
- serverUrl
13827
- ])
14411
+ args: codexMcpAddArgs(["attach", serverUrl])
13828
14412
  }],
13829
14413
  nextSteps: [`Run caplets remote login ${serverUrl} before using this MCP config.`, "In Codex, run /mcp to confirm the caplets server is connected."]
13830
14414
  };
@@ -13834,11 +14418,7 @@ function remoteSetupDefinition(id, options) {
13834
14418
  type: "command",
13835
14419
  label: "Add remote-backed Caplets MCP server to Claude Code",
13836
14420
  command: "claude",
13837
- args: claudeMcpAddArgs([
13838
- "attach",
13839
- "--remote-url",
13840
- serverUrl
13841
- ])
14421
+ args: claudeMcpAddArgs(["attach", serverUrl])
13842
14422
  }],
13843
14423
  nextSteps: [`Run caplets remote login ${serverUrl} before using this MCP config.`, "In Claude Code, run /mcp to confirm the caplets server is connected."]
13844
14424
  };
@@ -13851,11 +14431,7 @@ function remoteSetupDefinition(id, options) {
13851
14431
  path: options.output,
13852
14432
  content: `${JSON.stringify({ mcpServers: { caplets: {
13853
14433
  command: "caplets",
13854
- args: [
13855
- "attach",
13856
- "--remote-url",
13857
- serverUrl
13858
- ]
14434
+ args: ["attach", serverUrl]
13859
14435
  } } }, null, 2)}\n`
13860
14436
  }],
13861
14437
  nextSteps: [`Run caplets remote login ${serverUrl} before using this MCP config.`, "Import the written MCP config into your MCP client."]
@@ -14627,7 +15203,7 @@ var RemoteControlClient = class {
14627
15203
  if (response.status === 401 || response.status === 403) throw new CapletsError("AUTH_FAILED", `Caplets remote authentication failed. Run caplets remote login ${safeBaseUrl(resolved.baseUrl)}.`);
14628
15204
  if (!response.ok) throw new CapletsError("SERVER_UNAVAILABLE", `Caplets server at ${safeBaseUrl(resolved.baseUrl)} returned HTTP ${response.status}.`);
14629
15205
  const payload = await parseRemoteCliResponse(response);
14630
- if (!payload.ok) throw new CapletsError(payload.error.code, redactRemoteMessage(payload.error.message), payload.error.nextAction === void 0 ? void 0 : { nextAction: payload.error.nextAction });
15206
+ if (!payload.ok) throw new CapletsError(payload.error.code, redactRemoteMessage(payload.error.message, sensitiveValues(command, args)), payload.error.nextAction === void 0 ? void 0 : { nextAction: payload.error.nextAction });
14631
15207
  return payload.result;
14632
15208
  }
14633
15209
  };
@@ -14684,8 +15260,17 @@ function isRecord$1(value) {
14684
15260
  function isCapletsErrorCode(value) {
14685
15261
  return CAPLETS_ERROR_CODES.includes(value);
14686
15262
  }
14687
- function redactRemoteMessage(message) {
14688
- return String(redactSecrets$1(message)).replace(/\b(authorization\s*:\s*(?:basic|bearer)\s+)[^\s,;]+/giu, "$1[REDACTED]").replace(/\b((?:access_)?token=)[^\s,;]+/giu, "$1[REDACTED]").replace(/\b((?:token|secret|authorization|auth|api[-_]?key|password|credential|clientsecret|client_secret|code|refresh(?:_token)?)\s*[=:]\s*)[^\s,;]+/giu, "$1[REDACTED]");
15263
+ function redactRemoteMessage(message, values = []) {
15264
+ let redacted = String(redactSecrets$1(message));
15265
+ for (const value of values) {
15266
+ if (value.length === 0) continue;
15267
+ redacted = redacted.split(value).join("[REDACTED]");
15268
+ }
15269
+ return redacted.replace(/\b(authorization\s*:\s*(?:basic|bearer)\s+)[^\s,;]+/giu, "$1[REDACTED]").replace(/\b((?:access_)?token=)[^\s,;]+/giu, "$1[REDACTED]").replace(/\b((?:token|secret|authorization|auth|api[-_]?key|password|credential|clientsecret|client_secret|code|refresh(?:_token)?)\s*[=:]\s*)[^\s,;]+/giu, "$1[REDACTED]");
15270
+ }
15271
+ function sensitiveValues(command, args) {
15272
+ if (command === "vault_set" && typeof args.value === "string") return [args.value];
15273
+ return [];
14689
15274
  }
14690
15275
  //#endregion
14691
15276
  //#region src/serve/stdio.ts
@@ -14784,12 +15369,17 @@ function addServeMigrationCommand(parent, name, replacement) {
14784
15369
  function collectValues(value, previous) {
14785
15370
  return [...previous, value];
14786
15371
  }
15372
+ const HIDDEN_INPUT_PROMPT_LABELS = { vaultValue: "Value: " };
14787
15373
  function remoteProfileStore(authDir, env) {
14788
15374
  return createRemoteProfileStore({
14789
15375
  authDir,
14790
15376
  env
14791
15377
  });
14792
15378
  }
15379
+ function attachRemoteUrlFromArgs(positionalUrl, legacyRemoteUrl) {
15380
+ if (positionalUrl && legacyRemoteUrl && positionalUrl !== legacyRemoteUrl) throw new CapletsError("REQUEST_INVALID", "Pass either attach URL or --remote-url, not both. Use caplets attach <url> for new configs.");
15381
+ return positionalUrl ?? legacyRemoteUrl;
15382
+ }
14793
15383
  function remoteServerCredentialStore(statePath, env) {
14794
15384
  return new RemoteServerCredentialStore({ dir: statePath ?? env.CAPLETS_REMOTE_SERVER_STATE_DIR ?? join(DEFAULT_AUTH_DIR, "remote-server") });
14795
15385
  }
@@ -14878,40 +15468,33 @@ async function loginCloudRemoteProfile(url, options, store, io) {
14878
15468
  }
14879
15469
  });
14880
15470
  }
14881
- async function pairingCodeFromOptions(options, readStdin, writeErr) {
14882
- if (options.code?.trim()) {
14883
- writeErr("Warning: --code may store the Pairing Code in shell history; prefer the hidden prompt or --code-stdin for automation.\n");
14884
- return options.code.trim();
14885
- }
14886
- if (options.codeStdin) {
14887
- const code = (readStdin ? await readStdin() : await readAllStdin()).trim();
14888
- if (code) return code;
14889
- throw new CapletsError("REQUEST_INVALID", "Pairing Code is required when --code-stdin is used.");
14890
- }
14891
- const output = new HiddenPromptOutput(process.stdout);
15471
+ async function readHiddenInput(label, options = {}) {
15472
+ const input = options.input ?? process.stdin;
15473
+ const output = options.output ?? process.stdout;
15474
+ output.write(label);
14892
15475
  const readline = createInterface({
14893
- input: process.stdin,
14894
- output,
15476
+ input,
15477
+ output: new HiddenPromptOutput(output, { echoFirstChunk: false }),
14895
15478
  terminal: true
14896
15479
  });
14897
15480
  try {
14898
- const code = (await readline.question("Pairing Code: ")).trim();
14899
- if (code) return code;
15481
+ return await readline.question("");
14900
15482
  } finally {
14901
15483
  readline.close();
14902
- process.stdout.write("\n");
15484
+ output.write("\n");
14903
15485
  }
14904
- throw new CapletsError("REQUEST_INVALID", "Pairing Code is required for self-hosted Remote Login.");
14905
15486
  }
14906
15487
  var HiddenPromptOutput = class extends Writable {
14907
15488
  output;
15489
+ options;
14908
15490
  wrotePrompt = false;
14909
- constructor(output) {
15491
+ constructor(output, options = { echoFirstChunk: true }) {
14910
15492
  super();
14911
15493
  this.output = output;
15494
+ this.options = options;
14912
15495
  }
14913
15496
  _write(chunk, _encoding, callback) {
14914
- if (!this.wrotePrompt) {
15497
+ if (this.options.echoFirstChunk !== false && !this.wrotePrompt) {
14915
15498
  this.output.write(chunk);
14916
15499
  this.wrotePrompt = true;
14917
15500
  }
@@ -14929,6 +15512,7 @@ async function parseRemoteLoginCredentials(response) {
14929
15512
  const record = parsed;
14930
15513
  if (typeof record.clientId !== "string" || typeof record.accessToken !== "string" || typeof record.refreshToken !== "string") throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Remote Login response is missing credentials.");
14931
15514
  return {
15515
+ ...typeof record.hostUrl === "string" ? { hostUrl: record.hostUrl } : {},
14932
15516
  clientId: record.clientId,
14933
15517
  clientLabel: typeof record.clientLabel === "string" ? record.clientLabel : "Caplets CLI",
14934
15518
  accessToken: record.accessToken,
@@ -14937,6 +15521,170 @@ async function parseRemoteLoginCredentials(response) {
14937
15521
  ...typeof record.expiresAt === "string" ? { expiresAt: record.expiresAt } : {}
14938
15522
  };
14939
15523
  }
15524
+ async function parsePendingRemoteLoginStart(response, options = {}) {
15525
+ const parsed = await response.json();
15526
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Pending Remote Login response must be an object.");
15527
+ const record = parsed;
15528
+ if (typeof record.flowId !== "string" || typeof record.operatorCode !== "string" || typeof record.pendingRefreshSecret !== "string" || typeof record.codeExpiresAt !== "string" || typeof record.flowExpiresAt !== "string") throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Pending Remote Login response is missing pending material.");
15529
+ const pendingCompletionSecret = typeof record.pendingCompletionSecret === "string" ? record.pendingCompletionSecret : options.pendingCompletionSecret;
15530
+ if (!pendingCompletionSecret) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Pending Remote Login response is missing completion material.");
15531
+ return {
15532
+ flowId: record.flowId,
15533
+ operatorCode: record.operatorCode,
15534
+ ...typeof record.operatorCodeFingerprint === "string" ? { operatorCodeFingerprint: record.operatorCodeFingerprint } : {},
15535
+ pendingRefreshSecret: record.pendingRefreshSecret,
15536
+ pendingCompletionSecret,
15537
+ codeExpiresAt: record.codeExpiresAt,
15538
+ flowExpiresAt: record.flowExpiresAt,
15539
+ intervalSeconds: typeof record.intervalSeconds === "number" ? record.intervalSeconds : 5
15540
+ };
15541
+ }
15542
+ async function parsePendingRemoteLoginStatus(response) {
15543
+ const parsed = await response.json();
15544
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Pending Remote Login status response must be an object.");
15545
+ const status = parsed.status;
15546
+ if (typeof status !== "string") throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Pending Remote Login status response is missing status.");
15547
+ return status;
15548
+ }
15549
+ async function selfHostedPendingRemoteLogin(url, input) {
15550
+ const fetchImpl = input.fetch ?? fetch;
15551
+ const baseUrl = new URL(normalizeRemoteProfileHostUrl(url));
15552
+ const startBody = input.clientLabel ? { clientLabel: input.clientLabel } : {};
15553
+ const start = await fetchImpl(appendBasePath(baseUrl, "v1/remote/login/start"), {
15554
+ method: "POST",
15555
+ headers: { "content-type": "application/json" },
15556
+ body: JSON.stringify(startBody)
15557
+ });
15558
+ if (!start.ok) throw new CapletsError("AUTH_FAILED", "Remote Login pending start failed.");
15559
+ let pending = await parsePendingRemoteLoginStart(start);
15560
+ if (input.json) input.writeOut(`${JSON.stringify({
15561
+ code: "pending_login_started",
15562
+ flowId: pending.flowId,
15563
+ operatorCode: pending.operatorCode,
15564
+ operatorCodeFingerprint: pending.operatorCodeFingerprint,
15565
+ codeExpiresAt: pending.codeExpiresAt,
15566
+ flowExpiresAt: pending.flowExpiresAt
15567
+ })}\n`);
15568
+ else {
15569
+ input.writeOut(`Remote Login Code: ${pending.operatorCode}\n`);
15570
+ if (pending.operatorCodeFingerprint) input.writeOut(`Code fingerprint: ${pending.operatorCodeFingerprint}\n`);
15571
+ input.writeOut(`Approve from the host with caplets remote host approve ${pending.operatorCode} --yes\n`);
15572
+ }
15573
+ const intervalMs = numberEnv(input.env.CAPLETS_REMOTE_LOGIN_POLL_INTERVAL_MS, pending.intervalSeconds * 1e3);
15574
+ try {
15575
+ while (true) {
15576
+ const poll = await fetchPendingRemoteLoginStatus(fetchImpl, baseUrl, pending, input.signal);
15577
+ if (!poll.ok) throw new CapletsError("AUTH_FAILED", "Remote Login pending poll failed.");
15578
+ const status = await parsePendingRemoteLoginStatus(poll);
15579
+ if (status === "approved") {
15580
+ if (input.json) input.writeOut(`${JSON.stringify({
15581
+ code: "pending_login_approved",
15582
+ flowId: pending.flowId
15583
+ })}\n`);
15584
+ break;
15585
+ }
15586
+ if (status !== "pending") {
15587
+ if (input.json) input.writeOut(`${JSON.stringify({
15588
+ code: `pending_login_${status}`,
15589
+ flowId: pending.flowId
15590
+ })}\n`);
15591
+ throw new CapletsError("AUTH_FAILED", `Remote Login pending flow ${status}.`);
15592
+ }
15593
+ if (Date.parse(pending.codeExpiresAt) <= Date.now()) {
15594
+ const refresh = await fetchImpl(appendBasePath(baseUrl, "v1/remote/login/refresh"), {
15595
+ method: "POST",
15596
+ headers: { "content-type": "application/json" },
15597
+ body: JSON.stringify({
15598
+ flowId: pending.flowId,
15599
+ pendingRefreshSecret: pending.pendingRefreshSecret,
15600
+ pendingCompletionSecret: pending.pendingCompletionSecret
15601
+ }),
15602
+ ...input.signal ? { signal: input.signal } : {}
15603
+ });
15604
+ if (!refresh.ok) {
15605
+ const retryPoll = await fetchPendingRemoteLoginStatus(fetchImpl, baseUrl, pending);
15606
+ if (retryPoll.ok && await parsePendingRemoteLoginStatus(retryPoll) === "approved") {
15607
+ if (input.json) input.writeOut(`${JSON.stringify({
15608
+ code: "pending_login_approved",
15609
+ flowId: pending.flowId
15610
+ })}\n`);
15611
+ break;
15612
+ }
15613
+ throw new CapletsError("AUTH_FAILED", "Remote Login pending refresh failed.");
15614
+ }
15615
+ pending = await parsePendingRemoteLoginStart(refresh, { pendingCompletionSecret: pending.pendingCompletionSecret });
15616
+ if (input.json) input.writeOut(`${JSON.stringify({
15617
+ code: "pending_login_code_refreshed",
15618
+ flowId: pending.flowId,
15619
+ operatorCode: pending.operatorCode,
15620
+ operatorCodeFingerprint: pending.operatorCodeFingerprint,
15621
+ codeExpiresAt: pending.codeExpiresAt,
15622
+ flowExpiresAt: pending.flowExpiresAt
15623
+ })}\n`);
15624
+ else {
15625
+ input.writeOut(`Remote Login Code refreshed: ${pending.operatorCode}\n`);
15626
+ if (pending.operatorCodeFingerprint) input.writeOut(`Code fingerprint: ${pending.operatorCodeFingerprint}\n`);
15627
+ }
15628
+ }
15629
+ await sleep(intervalMs, input.signal);
15630
+ }
15631
+ const complete = await completePendingRemoteLogin(fetchImpl, baseUrl, pending, input.signal);
15632
+ if (!complete.ok) throw new CapletsError("AUTH_FAILED", "Remote Login pending complete failed.");
15633
+ return parseRemoteLoginCredentials(complete);
15634
+ } catch (error) {
15635
+ if (input.signal?.aborted || isAbortError(error)) {
15636
+ await cancelPendingRemoteLogin(fetchImpl, baseUrl, pending);
15637
+ if (input.json) input.writeOut(`${JSON.stringify({
15638
+ code: "pending_login_cancelled",
15639
+ flowId: pending.flowId
15640
+ })}\n`);
15641
+ throw new CapletsError("REQUEST_INVALID", "Remote Login pending flow cancelled.");
15642
+ }
15643
+ throw error;
15644
+ }
15645
+ }
15646
+ async function completePendingRemoteLogin(fetchImpl, baseUrl, pending, signal) {
15647
+ try {
15648
+ return await fetchImpl(appendBasePath(baseUrl, "v1/remote/login/complete"), pendingRemoteLoginCompletionRequest(pending, signal));
15649
+ } catch {
15650
+ return fetchImpl(appendBasePath(baseUrl, "v1/remote/login/complete"), pendingRemoteLoginCompletionRequest(pending));
15651
+ }
15652
+ }
15653
+ function pendingRemoteLoginCompletionRequest(pending, signal) {
15654
+ return {
15655
+ method: "POST",
15656
+ headers: { "content-type": "application/json" },
15657
+ body: JSON.stringify({
15658
+ flowId: pending.flowId,
15659
+ pendingCompletionSecret: pending.pendingCompletionSecret
15660
+ }),
15661
+ ...signal ? { signal } : {}
15662
+ };
15663
+ }
15664
+ async function fetchPendingRemoteLoginStatus(fetchImpl, baseUrl, pending, signal) {
15665
+ return fetchImpl(appendBasePath(baseUrl, "v1/remote/login/poll"), {
15666
+ method: "POST",
15667
+ headers: { "content-type": "application/json" },
15668
+ body: JSON.stringify({
15669
+ flowId: pending.flowId,
15670
+ pendingCompletionSecret: pending.pendingCompletionSecret
15671
+ }),
15672
+ ...signal ? { signal } : {}
15673
+ });
15674
+ }
15675
+ async function cancelPendingRemoteLogin(fetchImpl, baseUrl, pending) {
15676
+ await fetchImpl(appendBasePath(baseUrl, "v1/remote/login/cancel"), {
15677
+ method: "POST",
15678
+ headers: { "content-type": "application/json" },
15679
+ body: JSON.stringify({
15680
+ flowId: pending.flowId,
15681
+ pendingCompletionSecret: pending.pendingCompletionSecret
15682
+ })
15683
+ }).catch(() => void 0);
15684
+ }
15685
+ function isAbortError(error) {
15686
+ return error instanceof Error && error.name === "AbortError" || typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError";
15687
+ }
14940
15688
  async function revokeSelfHostedRemoteClient(remoteUrl, accessToken, fetchImpl = fetch) {
14941
15689
  await fetchImpl(appendBasePath(new URL(normalizeRemoteProfileHostUrl(remoteUrl)), "v1/remote/client"), {
14942
15690
  method: "DELETE",
@@ -15018,9 +15766,35 @@ function createSetupPromptHandle(io, writeOut) {
15018
15766
  close: () => readline.close()
15019
15767
  };
15020
15768
  }
15021
- async function sleep(ms) {
15022
- if (ms <= 0) return;
15023
- await new Promise((resolve) => setTimeout(resolve, ms));
15769
+ async function sleep(ms, signal) {
15770
+ if (ms <= 0 || signal?.aborted) return;
15771
+ await new Promise((resolve) => {
15772
+ const timeout = setTimeout(done, ms);
15773
+ const abort = () => done();
15774
+ function done() {
15775
+ clearTimeout(timeout);
15776
+ signal?.removeEventListener("abort", abort);
15777
+ resolve();
15778
+ }
15779
+ signal?.addEventListener("abort", abort, { once: true });
15780
+ });
15781
+ }
15782
+ function cliInterruptSignal(existing) {
15783
+ if (existing) return {
15784
+ signal: existing,
15785
+ dispose: () => {}
15786
+ };
15787
+ const controller = new AbortController();
15788
+ const abort = () => controller.abort();
15789
+ process.once("SIGINT", abort);
15790
+ process.once("SIGTERM", abort);
15791
+ return {
15792
+ signal: controller.signal,
15793
+ dispose: () => {
15794
+ process.off("SIGINT", abort);
15795
+ process.off("SIGTERM", abort);
15796
+ }
15797
+ };
15024
15798
  }
15025
15799
  function createProgram(io = {}) {
15026
15800
  const writeOut = io.writeOut ?? ((value) => process.stdout.write(value));
@@ -15249,10 +16023,12 @@ function createProgram(io = {}) {
15249
16023
  }
15250
16024
  for (const entry of result.entries) writeOut(stream === "all" ? `[${entry.stream}] ${entry.line}\n` : `${entry.line}\n`);
15251
16025
  });
15252
- program.command(cliCommands$1.attach).description("Start a remote-backed Caplets MCP server.").option("--transport <transport>", "server transport: stdio or http").option("--host <host>", "HTTP bind host").option("--port <port>", "HTTP bind port").option("--path <path>", "HTTP service base path").option("--remote-url <url>", "remote Caplets service base URL").option("--workspace <workspace>", "hosted Cloud workspace ID or slug").option("--allow-unauthenticated-http", "allow unauthenticated HTTP serving on non-loopback hosts").option("--trust-proxy", "trust X-Forwarded-* headers from a reverse proxy").option("--json", "print JSON status events").option("--verbose", "print detailed attach diagnostics").option("--once", "validate Project Binding once and exit").option("--project-root <path>", "test-only project root override").action(async (options) => {
16026
+ program.command(cliCommands$1.attach).description("Start a remote-backed Caplets MCP server.").argument("[url]", "remote Caplets service base URL").option("--transport <transport>", "server transport: stdio or http").option("--host <host>", "HTTP bind host").option("--port <port>", "HTTP bind port").option("--path <path>", "HTTP service base path").addOption(new Option("--remote-url <url>", "legacy alias for the remote Caplets service base URL").hideHelp()).option("--workspace <workspace>", "hosted Cloud workspace ID or slug").option("--allow-unauthenticated-http", "allow unauthenticated HTTP serving on non-loopback hosts").option("--trust-proxy", "trust X-Forwarded-* headers from a reverse proxy").option("--json", "print JSON status events").option("--verbose", "print detailed attach diagnostics").option("--once", "validate Project Binding once and exit").option("--project-root <path>", "test-only project root override").action(async (url, options) => {
15253
16027
  try {
16028
+ const remoteUrl = attachRemoteUrlFromArgs(url, options.remoteUrl);
15254
16029
  const attachOptions = {
15255
16030
  ...options,
16031
+ ...remoteUrl ? { remoteUrl } : {},
15256
16032
  ...io.fetch ? { fetch: io.fetch } : {},
15257
16033
  ...io.authDir ? { authDir: io.authDir } : {}
15258
16034
  };
@@ -15307,7 +16083,7 @@ function createProgram(io = {}) {
15307
16083
  }
15308
16084
  });
15309
16085
  const remote = program.command(cliCommands$1.remote).description("Manage Caplets Remote Login.");
15310
- remote.command("login").description("Log this machine into a Caplets host.").argument("<url>", "Caplets host URL").option("--workspace <workspace>", "Cloud workspace ID or slug to select").option("--client-label <label>", "client label for this machine").option("--device-name <name>", "Cloud device label for this machine").option("--code <code>", "Pairing Code for explicit noninteractive self-hosted login").option("--code-stdin", "read the Pairing Code from stdin").option("--no-open", "print the Cloud login URL without opening a browser").option("--json", "print JSON output").action(async (url, options) => {
16086
+ remote.command("login").description("Log this machine into a Caplets host.").argument("<url>", "Caplets host URL").option("--workspace <workspace>", "Cloud workspace ID or slug to select").option("--client-label <label>", "client label for this machine").option("--device-name <name>", "Cloud device label for this machine").addOption(new Option("--code <code>", "legacy Pairing Code input").hideHelp()).addOption(new Option("--code-stdin", "legacy Pairing Code stdin input").hideHelp()).option("--no-open", "print the Cloud login URL without opening a browser").option("--json", "print JSON output").action(async (url, options) => {
15311
16087
  const store = remoteProfileStore(io.authDir, env);
15312
16088
  if (isCapletsCloudUrl(url)) {
15313
16089
  writeRemoteStatus(await loginCloudRemoteProfile(url, options, store, {
@@ -15317,20 +16093,24 @@ function createProgram(io = {}) {
15317
16093
  }), options.json === true, writeOut);
15318
16094
  return;
15319
16095
  }
15320
- const code = await pairingCodeFromOptions(options, io.readStdin, writeErr);
15321
- const exchangeUrl = appendBasePath(new URL(normalizeRemoteProfileHostUrl(url)), "v1/remote/pairing/exchange");
15322
- const response = await (io.fetch ?? fetch)(exchangeUrl, {
15323
- method: "POST",
15324
- headers: { "content-type": "application/json" },
15325
- body: JSON.stringify({
15326
- code,
15327
- ...options.clientLabel ? { clientLabel: options.clientLabel } : {}
15328
- })
15329
- });
15330
- if (!response.ok) throw new CapletsError("AUTH_FAILED", "Remote Login pairing exchange failed.");
15331
- const credentials = await parseRemoteLoginCredentials(response);
15332
- writeRemoteStatus(await store.saveSelfHostedProfile({
16096
+ if (options.code?.trim() || options.codeStdin) throw new CapletsError("REQUEST_INVALID", `Self-hosted Remote Login no longer accepts Pairing Codes. Run caplets remote login ${normalizeRemoteProfileHostUrl(url)} without --code and approve the pending login from the host.`);
16097
+ const interrupt = cliInterruptSignal(io.signal);
16098
+ let credentials;
16099
+ try {
16100
+ credentials = await selfHostedPendingRemoteLogin(url, {
16101
+ ...options.clientLabel ? { clientLabel: options.clientLabel } : {},
16102
+ json: options.json,
16103
+ ...io.fetch ? { fetch: io.fetch } : {},
16104
+ ...interrupt.signal ? { signal: interrupt.signal } : {},
16105
+ writeOut,
16106
+ env
16107
+ });
16108
+ } finally {
16109
+ interrupt.dispose();
16110
+ }
16111
+ const status = await store.saveSelfHostedProfile({
15333
16112
  hostUrl: url,
16113
+ hostIdentity: normalizeRemoteProfileHostUrl(credentials.hostUrl ?? url),
15334
16114
  clientId: credentials.clientId,
15335
16115
  clientLabel: credentials.clientLabel,
15336
16116
  credentials: {
@@ -15339,7 +16119,12 @@ function createProgram(io = {}) {
15339
16119
  tokenType: credentials.tokenType,
15340
16120
  expiresAt: credentials.expiresAt
15341
16121
  }
15342
- }), options.json === true, writeOut);
16122
+ });
16123
+ if (options.json === true) writeOut(`${JSON.stringify({
16124
+ code: "remote_profile_saved",
16125
+ ...status
16126
+ })}\n`);
16127
+ else writeRemoteStatus(status, false, writeOut);
15343
16128
  });
15344
16129
  remote.command("status").description("Show saved Remote Login status.").argument("[url]", "Caplets host URL").option("--workspace <workspace>", "Cloud workspace ID or slug").option("--json", "print JSON output").action(async (url, options) => {
15345
16130
  if (!url) {
@@ -15400,18 +16185,19 @@ function createProgram(io = {}) {
15400
16185
  writeOut(removed ? `Logged out of ${normalizeRemoteProfileHostUrl(url)}.\n` : `No Remote Login profile found for ${normalizeRemoteProfileHostUrl(url)}.\n`);
15401
16186
  });
15402
16187
  const remoteHost = remote.command("host").description("Manage self-hosted remote credentials.");
15403
- remoteHost.command("pair").description("Create a short-lived self-hosted Pairing Code from the server environment.").requiredOption("--host-url <url>", "public Caplets host URL").option("--state-path <path>", "server-owned remote credential state directory").option("--client-label <label>", "suggested client label").option("--json", "print JSON output").action(async (options) => {
15404
- const issued = remoteServerCredentialStore(options.statePath, env).createPairingCode({
15405
- hostUrl: options.hostUrl,
15406
- ...options.clientLabel ? { clientLabel: options.clientLabel } : {}
15407
- });
16188
+ remoteHost.command("pair", { hidden: true }).description("Deprecated. Pairing Code bootstrap is no longer supported.").option("--host-url <url>", "public Caplets host URL; defaults to CAPLETS_SERVER_URL").option("--state-path <path>", "server-owned remote credential state directory").option("--json", "print JSON output").action(async (options) => {
16189
+ const hostUrl = options.hostUrl ?? env.CAPLETS_SERVER_URL;
16190
+ const guidance = "Self-hosted Pairing Code bootstrap is no longer supported. Run caplets remote login <url> from the client, then approve the pending login with caplets remote host logins and caplets remote host approve <code> from the host.";
15408
16191
  if (options.json) {
15409
- writeOut(`${JSON.stringify(issued, null, 2)}\n`);
16192
+ writeOut(`${JSON.stringify({
16193
+ supported: false,
16194
+ deprecated: true,
16195
+ ...hostUrl ? { hostUrl: normalizeRemoteProfileHostUrl(hostUrl) } : {},
16196
+ message: guidance
16197
+ }, null, 2)}\n`);
15410
16198
  return;
15411
16199
  }
15412
- writeOut(`Pairing Code: ${issued.code}\n`);
15413
- writeOut(`Expires At: ${issued.expiresAt}\n`);
15414
- writeOut(`Run caplets remote login ${normalizeRemoteProfileHostUrl(options.hostUrl)} and enter the Pairing Code when prompted.\n`);
16200
+ writeOut(`${guidance}\n`);
15415
16201
  });
15416
16202
  remoteHost.command("clients").description("List paired self-hosted remote clients from server state.").option("--state-path <path>", "server-owned remote credential state directory").option("--json", "print JSON output").action((options) => {
15417
16203
  const clients = remoteServerCredentialStore(options.statePath, env).listClients();
@@ -15425,6 +16211,35 @@ function createProgram(io = {}) {
15425
16211
  }
15426
16212
  for (const client of clients) writeOut(`${client.clientId}\t${terminalSafeText(client.clientLabel)}\t${client.hostUrl}\t${client.revokedAt ? "revoked" : "active"}\n`);
15427
16213
  });
16214
+ remoteHost.command("logins").description("List pending self-hosted Remote Login approvals from server state.").option("--state-path <path>", "server-owned remote credential state directory").option("--json", "print JSON output").action((options) => {
16215
+ const pendingLogins = remoteServerCredentialStore(options.statePath, env).listPendingLogins();
16216
+ if (options.json) {
16217
+ writeOut(`${JSON.stringify({ pendingLogins }, null, 2)}\n`);
16218
+ return;
16219
+ }
16220
+ if (pendingLogins.length === 0) {
16221
+ writeOut("No pending Remote Login approvals.\n");
16222
+ return;
16223
+ }
16224
+ for (const pending of pendingLogins) writeOut(`${pending.flowId}\t${pending.operatorCodeFingerprint ?? "-"}\t${terminalSafeText(pending.clientLabel)}\t${pending.hostUrl}\t${pending.status}\n`);
16225
+ });
16226
+ remoteHost.command("approve").description("Approve one pending self-hosted Remote Login code from server state.").argument("<code>", "operator-visible Remote Login code").option("--state-path <path>", "server-owned remote credential state directory").option("--yes", "approve without an interactive confirmation prompt").option("--json", "print JSON output").action((code, options) => {
16227
+ if (!options.yes && !options.json) throw new CapletsError("REQUEST_INVALID", "Use --yes to approve this pending login.");
16228
+ const approved = remoteServerCredentialStore(options.statePath, env).approvePendingLogin({ operatorCode: code });
16229
+ if (options.json) {
16230
+ writeOut(`${JSON.stringify(approved, null, 2)}\n`);
16231
+ return;
16232
+ }
16233
+ writeOut(`Approved pending Remote Login ${approved.flowId}.\n`);
16234
+ });
16235
+ remoteHost.command("deny").description("Deny one pending self-hosted Remote Login code from server state.").argument("<code>", "operator-visible Remote Login code").option("--state-path <path>", "server-owned remote credential state directory").option("--json", "print JSON output").action((code, options) => {
16236
+ const denied = remoteServerCredentialStore(options.statePath, env).denyPendingLogin({ operatorCode: code });
16237
+ if (options.json) {
16238
+ writeOut(`${JSON.stringify(denied, null, 2)}\n`);
16239
+ return;
16240
+ }
16241
+ writeOut(`Denied pending Remote Login ${denied.flowId}.\n`);
16242
+ });
15428
16243
  remoteHost.command("revoke").description("Revoke one paired self-hosted remote client from server state.").argument("<client-id>", "remote client ID").option("--state-path <path>", "server-owned remote credential state directory").option("--json", "print JSON output").action((clientId, options) => {
15429
16244
  const revoked = remoteServerCredentialStore(options.statePath, env).revokeClient(clientId);
15430
16245
  if (options.json) {
@@ -15617,6 +16432,135 @@ function createProgram(io = {}) {
15617
16432
  ...io.daemon ? { daemon: io.daemon } : {}
15618
16433
  }));
15619
16434
  });
16435
+ const vault = program.command(cliCommands$1.vault).description("Manage Caplets Vault values.");
16436
+ vault.command("set").description("Set a local/global Vault value.").argument("<name>", "Vault key name").option("-g, --global", "target the local/global Vault").option("--remote", "target the selected remote Vault").option("--force", "overwrite an existing Vault value").option("--grant <capletId>", "grant this key to a configured Caplet after setting it").option("--as <referenceName>", "reference name the Caplet uses in config").option("--json", "print JSON output").action(async (name, options) => {
16437
+ if (parseVaultTarget(options) === "remote") {
16438
+ const value = await readVaultValue(io);
16439
+ assertVaultTransportValueSize(value);
16440
+ const status = await remoteVaultSet(io, {
16441
+ name,
16442
+ value,
16443
+ force: Boolean(options.force),
16444
+ ...options.grant ? { grant: options.grant } : {},
16445
+ ...options.as ?? options.grant ? { referenceName: options.as ?? name } : {}
16446
+ });
16447
+ if (options.json) {
16448
+ writeOut(`${JSON.stringify(status, null, 2)}\n`);
16449
+ return;
16450
+ }
16451
+ writeOut(`Set remote Vault key ${validateVaultKeyName(name)}.\n`);
16452
+ if (options.grant) writeOut(`Granted remote Vault key ${validateVaultKeyName(name)} to ${options.grant} as ${validateVaultKeyName(options.as ?? name)}.\n`);
16453
+ return;
16454
+ }
16455
+ const value = await readVaultValue(io);
16456
+ const store = new FileVaultStore({ env });
16457
+ const existed = store.getStatus(name).present;
16458
+ const previousValue = existed && options.grant ? store.resolveValue(name) : void 0;
16459
+ const status = store.set(name, value, { force: Boolean(options.force) });
16460
+ try {
16461
+ if (options.grant) {
16462
+ const origin = resolveVaultAccessOrigin(options.grant, io);
16463
+ store.grantAccess({
16464
+ storedKey: name,
16465
+ referenceName: options.as ?? name,
16466
+ capletId: options.grant,
16467
+ origin
16468
+ });
16469
+ }
16470
+ } catch (error) {
16471
+ if (existed && previousValue !== void 0) store.set(name, previousValue, { force: true });
16472
+ else store.delete(name);
16473
+ throw error;
16474
+ }
16475
+ if (options.json) {
16476
+ writeOut(`${JSON.stringify(status, null, 2)}\n`);
16477
+ return;
16478
+ }
16479
+ writeOut(`Set Vault key ${validateVaultKeyName(name)}.\n`);
16480
+ if (options.grant) writeOut(`Granted Vault key ${validateVaultKeyName(name)} to ${options.grant} as ${validateVaultKeyName(options.as ?? name)}.\n`);
16481
+ });
16482
+ vault.command("get").description("Show local/global Vault metadata, or reveal with --show.").argument("<name>", "Vault key name").option("-g, --global", "target the local/global Vault").option("--remote", "target the selected remote Vault").option("--show", "reveal the raw Vault value").option("--json", "print JSON output").action(async (name, options) => {
16483
+ if (parseVaultTarget(options) === "remote") {
16484
+ const result = await remoteVaultGet(io, {
16485
+ name,
16486
+ reveal: Boolean(options.show)
16487
+ });
16488
+ if (options.show) {
16489
+ const value = result && typeof result === "object" && "value" in result ? String(result.value) : "";
16490
+ writeOut(options.json ? `${JSON.stringify(result, null, 2)}\n` : `${value}\n`);
16491
+ return;
16492
+ }
16493
+ writeOut(formatVaultValueStatus(result, Boolean(options.json)));
16494
+ return;
16495
+ }
16496
+ const store = new FileVaultStore({ env });
16497
+ if (options.show) {
16498
+ const value = store.resolveValue(name);
16499
+ writeOut(options.json ? `${JSON.stringify({
16500
+ key: name,
16501
+ value
16502
+ }, null, 2)}\n` : `${value}\n`);
16503
+ return;
16504
+ }
16505
+ writeOut(formatVaultValueStatus(store.getStatus(name), Boolean(options.json)));
16506
+ });
16507
+ vault.command("list").description("List local/global Vault keys without revealing values.").option("-g, --global", "target the local/global Vault").option("--remote", "target the selected remote Vault").option("--json", "print JSON output").action(async (options) => {
16508
+ if (parseVaultTarget(options) === "remote") {
16509
+ writeOut(formatVaultValueList(await remoteVaultList(io), Boolean(options.json)));
16510
+ return;
16511
+ }
16512
+ writeOut(formatVaultValueList(new FileVaultStore({ env }).listValues(), Boolean(options.json)));
16513
+ });
16514
+ vault.command("delete").description("Delete a local/global Vault value without revealing it.").argument("<name>", "Vault key name").option("-g, --global", "target the local/global Vault").option("--remote", "target the selected remote Vault").option("--json", "print JSON output").action(async (name, options) => {
16515
+ if (parseVaultTarget(options) === "remote") {
16516
+ writeOut(formatVaultDeleteStatus(await remoteVaultDelete(io, name), Boolean(options.json)));
16517
+ return;
16518
+ }
16519
+ writeOut(formatVaultDeleteStatus(new FileVaultStore({ env }).delete(name), Boolean(options.json)));
16520
+ });
16521
+ const vaultAccess = vault.command("access").description("Manage Vault access grants.");
16522
+ vaultAccess.command("grant").description("Grant a Vault key to a configured Caplet.").argument("<name>", "stored Vault key name").argument("<capletId>", "configured Caplet ID").option("-g, --global", "target the local/global Vault").option("--remote", "target the selected remote Vault").option("--as <referenceName>", "reference name the Caplet uses in config").option("--json", "print JSON output").action(async (name, capletId, options) => {
16523
+ if (parseVaultTarget(options) === "remote") {
16524
+ writeOut(formatVaultAccessGrant(await remoteVaultAccessGrant(io, {
16525
+ name,
16526
+ capletId,
16527
+ referenceName: options.as ?? name
16528
+ }), Boolean(options.json)));
16529
+ return;
16530
+ }
16531
+ const origin = resolveVaultAccessOrigin(capletId, io);
16532
+ writeOut(formatVaultAccessGrant(new FileVaultStore({ env }).grantAccess({
16533
+ storedKey: name,
16534
+ referenceName: options.as ?? name,
16535
+ capletId,
16536
+ origin
16537
+ }), Boolean(options.json)));
16538
+ });
16539
+ vaultAccess.command("list").description("List Vault access grants without revealing values.").argument("[name]", "optional stored Vault key name").argument("[capletId]", "optional configured Caplet ID").option("-g, --global", "target the local/global Vault").option("--remote", "target the selected remote Vault").option("--caplet <capletId>", "filter by configured Caplet ID").option("--json", "print JSON output").action(async (name, capletId, options) => {
16540
+ if (options.caplet && capletId && options.caplet !== capletId) throw new CapletsError("REQUEST_INVALID", "Use either positional capletId or --caplet, not both.");
16541
+ const capletFilter = options.caplet ?? capletId;
16542
+ if (parseVaultTarget(options) === "remote") {
16543
+ writeOut(formatVaultAccessList(await remoteVaultAccessList(io, {
16544
+ ...name ? { name } : {},
16545
+ ...capletFilter ? { capletId: capletFilter } : {}
16546
+ }), Boolean(options.json)));
16547
+ return;
16548
+ }
16549
+ writeOut(formatVaultAccessList(new FileVaultStore({ env }).listAccess(vaultAccessFilter(name, capletFilter)), Boolean(options.json)));
16550
+ });
16551
+ vaultAccess.command("revoke").description("Revoke Vault access grants.").argument("<name>", "stored Vault key name").argument("<capletId>", "configured Caplet ID").option("-g, --global", "target the local/global Vault").option("--remote", "target the selected remote Vault").option("--as <referenceName>", "reference name the Caplet uses in config").option("--json", "print JSON output").action(async (name, capletId, options) => {
16552
+ if (parseVaultTarget(options) === "remote") {
16553
+ const revoked = await remoteVaultAccessRevoke(io, {
16554
+ name,
16555
+ capletId,
16556
+ ...options.as ? { referenceName: options.as } : {}
16557
+ });
16558
+ writeOut(formatVaultAccessRevoke(Array.isArray(revoked) ? revoked.length : 0, Boolean(options.json)));
16559
+ return;
16560
+ }
16561
+ const filter = vaultAccessFilter(name, capletId, options.as);
16562
+ writeOut(formatVaultAccessRevoke(new FileVaultStore({ env }).revokeAccess(filter).length, Boolean(options.json)));
16563
+ });
15620
16564
  program.command(cliCommands$1.list).description("List configured Caplets.").option("--all", "include disabled Caplets").option("--json", "print JSON output").option("--format <format>", "output format: plain, markdown, md, or json", parseOutputFormat).action(async (options) => {
15621
16565
  const includeDisabled = Boolean(options.all);
15622
16566
  const remote = remoteClientForCli(io);
@@ -15632,7 +16576,7 @@ function createProgram(io = {}) {
15632
16576
  writeOut(formatCapletList(rows, options.format ?? "plain"));
15633
16577
  return;
15634
16578
  }
15635
- const rows = listCaplets(loadConfigWithSources(currentConfigPath(), envProjectConfigPath(env)), { includeDisabled });
16579
+ const rows = listCaplets(loadConfigWithSources(currentConfigPath(), envProjectConfigPath(env), { vaultResolver: vaultBootstrapResolver }), { includeDisabled });
15636
16580
  if (options.json || options.format === "json") {
15637
16581
  writeOut(`${JSON.stringify(rows, null, 2)}\n`);
15638
16582
  return;
@@ -15982,6 +16926,7 @@ function createProgram(io = {}) {
15982
16926
  ...projectConfigPath ? { projectConfigPath } : {},
15983
16927
  config: localAuthConfigForTarget({
15984
16928
  serverId,
16929
+ ...io.authDir ? { authDir: io.authDir } : {},
15985
16930
  ...configPath ? { configPath } : {},
15986
16931
  ...projectConfigPath ? { projectConfigPath } : {},
15987
16932
  source: target
@@ -16002,6 +16947,7 @@ function createProgram(io = {}) {
16002
16947
  ...configPath ? { configPath } : {},
16003
16948
  config: localAuthConfigForTarget({
16004
16949
  serverId,
16950
+ ...io.authDir ? { authDir: io.authDir } : {},
16005
16951
  ...configPath ? { configPath } : {},
16006
16952
  ...projectConfigPath ? { projectConfigPath } : {},
16007
16953
  source: target
@@ -16023,6 +16969,7 @@ function createProgram(io = {}) {
16023
16969
  ...configPath ? { configPath } : {},
16024
16970
  config: localAuthConfigForTarget({
16025
16971
  serverId,
16972
+ ...io.authDir ? { authDir: io.authDir } : {},
16026
16973
  ...configPath ? { configPath } : {},
16027
16974
  ...projectConfigPath ? { projectConfigPath } : {},
16028
16975
  source: target
@@ -16117,6 +17064,123 @@ function parseMutationTarget(options) {
16117
17064
  if (options.remote) return "remote";
16118
17065
  return "project";
16119
17066
  }
17067
+ function parseVaultTarget(options) {
17068
+ const selected = [options.global ? "--global" : void 0, options.remote ? "--remote" : void 0].filter((value) => value !== void 0);
17069
+ if (selected.length > 1) throw new CapletsError("REQUEST_INVALID", `Cannot combine Vault target flags: ${selected.join(", ")}`);
17070
+ if (options.remote) return "remote";
17071
+ return "global";
17072
+ }
17073
+ async function resolveVaultRemoteTarget(io) {
17074
+ const env = io.env ?? process.env;
17075
+ const mode = resolveRemoteMode({}, env).mode;
17076
+ if (mode === "remote") return {
17077
+ kind: "self_hosted",
17078
+ client: requireRemoteClientForTarget(io)
17079
+ };
17080
+ if (mode !== "cloud") throw new CapletsError("REQUEST_INVALID", "--remote requires CAPLETS_MODE=remote or CAPLETS_MODE=cloud and CAPLETS_REMOTE_URL");
17081
+ const selection = await resolveRemoteSelection({
17082
+ mode: "cloud",
17083
+ ...io.authDir ? { authDir: io.authDir } : {},
17084
+ ...io.fetch ? { fetch: io.fetch } : {}
17085
+ }, env);
17086
+ if (selection.kind !== "hosted_cloud") throw new CapletsError("REQUEST_INVALID", "--remote Vault target did not resolve to Cloud.");
17087
+ return {
17088
+ kind: "cloud",
17089
+ workspace: selection.selectedWorkspace,
17090
+ client: new CapletsCloudClient({
17091
+ baseUrl: selection.remote.baseUrl,
17092
+ accessToken: selection.credentials.accessToken,
17093
+ ...selection.remote.fetch ? { fetch: selection.remote.fetch } : {}
17094
+ })
17095
+ };
17096
+ }
17097
+ async function remoteVaultSet(io, input) {
17098
+ const target = await resolveVaultRemoteTarget(io);
17099
+ if (target.kind === "self_hosted") return await target.client.request("vault_set", input);
17100
+ return await target.client.setVaultValue({
17101
+ workspace: target.workspace,
17102
+ ...input
17103
+ });
17104
+ }
17105
+ async function remoteVaultGet(io, input) {
17106
+ const target = await resolveVaultRemoteTarget(io);
17107
+ if (target.kind === "self_hosted") return await target.client.request("vault_get", {
17108
+ name: input.name,
17109
+ reveal: input.reveal
17110
+ });
17111
+ return await target.client.getVaultValue({
17112
+ workspace: target.workspace,
17113
+ name: input.name,
17114
+ reveal: input.reveal
17115
+ });
17116
+ }
17117
+ async function remoteVaultList(io) {
17118
+ const target = await resolveVaultRemoteTarget(io);
17119
+ if (target.kind === "self_hosted") return await target.client.request("vault_list", {});
17120
+ return await target.client.listVaultValues({ workspace: target.workspace });
17121
+ }
17122
+ async function remoteVaultDelete(io, name) {
17123
+ const target = await resolveVaultRemoteTarget(io);
17124
+ if (target.kind === "self_hosted") return await target.client.request("vault_delete", { name });
17125
+ return await target.client.deleteVaultValue({
17126
+ workspace: target.workspace,
17127
+ name
17128
+ });
17129
+ }
17130
+ async function remoteVaultAccessGrant(io, input) {
17131
+ const target = await resolveVaultRemoteTarget(io);
17132
+ if (target.kind === "self_hosted") return await target.client.request("vault_access_grant", input);
17133
+ return await target.client.grantVaultAccess({
17134
+ workspace: target.workspace,
17135
+ ...input
17136
+ });
17137
+ }
17138
+ async function remoteVaultAccessList(io, input) {
17139
+ const target = await resolveVaultRemoteTarget(io);
17140
+ if (target.kind === "self_hosted") return await target.client.request("vault_access_list", input);
17141
+ return await target.client.listVaultAccess({
17142
+ workspace: target.workspace,
17143
+ ...input
17144
+ });
17145
+ }
17146
+ async function remoteVaultAccessRevoke(io, input) {
17147
+ const target = await resolveVaultRemoteTarget(io);
17148
+ if (target.kind === "self_hosted") return await target.client.request("vault_access_revoke", input);
17149
+ return await target.client.revokeVaultAccess({
17150
+ workspace: target.workspace,
17151
+ ...input
17152
+ });
17153
+ }
17154
+ async function readVaultValue(io) {
17155
+ let value;
17156
+ if (io.readStdin) value = stripOneTrailingNewline(await io.readStdin());
17157
+ else if (!process.stdin.isTTY && !io.writeOut && !io.writeErr) value = stripOneTrailingNewline(await readAllStdin());
17158
+ else if (io.writeOut || io.writeErr || !process.stdin.isTTY || !process.stdout.isTTY) throw new CapletsError("REQUEST_INVALID", "Vault value input is required. Run interactively or provide stdin.");
17159
+ else value = await readHiddenInput(HIDDEN_INPUT_PROMPT_LABELS.vaultValue);
17160
+ if (value.length === 0) throw new CapletsError("REQUEST_INVALID", "Vault value input is required.");
17161
+ return value;
17162
+ }
17163
+ function stripOneTrailingNewline(value) {
17164
+ return value.replace(/\r?\n$/u, "");
17165
+ }
17166
+ function assertVaultTransportValueSize(value) {
17167
+ if (Buffer$1.byteLength(value, "utf8") > 65536) throw new CapletsError("REQUEST_INVALID", `Vault values must be ${VAULT_MAX_VALUE_BYTES} bytes or smaller.`);
17168
+ }
17169
+ function resolveVaultAccessOrigin(capletId, io) {
17170
+ const env = io.env ?? process.env;
17171
+ const config = loadConfigWithSources(envConfigPath(env), envProjectConfigPath(env), { vaultResolver: vaultBootstrapResolver });
17172
+ if (config.shadows[capletId]?.length) throw new CapletsError("REQUEST_INVALID", `Caplet ${capletId} is shadowed in multiple config sources; resolve the active config before granting Vault access.`);
17173
+ const origin = config.sources[capletId];
17174
+ if (!origin) throw new CapletsError("SERVER_NOT_FOUND", `Caplet ${capletId} is not configured.`);
17175
+ return origin;
17176
+ }
17177
+ function vaultAccessFilter(storedKey, capletId, referenceName) {
17178
+ return {
17179
+ ...storedKey ? { storedKey: validateVaultKeyName(storedKey) } : {},
17180
+ ...capletId ? { capletId } : {},
17181
+ ...referenceName ? { referenceName: validateVaultKeyName(referenceName) } : {}
17182
+ };
17183
+ }
16120
17184
  function localMutationTargetLabel(target, io) {
16121
17185
  return remoteClientForCli(io) ? `${target} ` : "";
16122
17186
  }
@@ -16387,7 +17451,8 @@ function mergePartialLocalOverlays(globalOverlay, projectOverlay) {
16387
17451
  config,
16388
17452
  sources,
16389
17453
  shadows,
16390
- warnings: [...globalOverlay.warnings, ...projectOverlay.warnings]
17454
+ warnings: [...globalOverlay.warnings, ...projectOverlay.warnings],
17455
+ sourceFound: globalOverlay.sourceFound || projectOverlay.sourceFound
16391
17456
  };
16392
17457
  }
16393
17458
  const capletConfigKinds = [