@caplets/core 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as resolveExposure, $t as CallToolRequestSchema, A as nativeCapletPromptGuidance, An as getLiteralValue, At as startOAuthFlow, B as CodeModeSessionManager, Bt as defaultStateBaseDir, C as resolveHostedCloudRemote, Cn as SetLevelRequestSchema, Ct as hasRenderableStructuredContent, D as isLoopbackHost, Dn as isJSONRPCErrorResponse, Dt as runGenericOAuthFlow, E as controlUrlForBase, En as isInitializeRequest, Et as refreshOAuthTokenBundle, Fn as isZ4Schema, G as CodeModeJournalStore, Gt as ReadBuffer, H as diagnoseCodeModeTypeScript, Ht as resolveConfigPath, I as codeModeRunInputSchema, In as normalizeObjectSchema, It as DEFAULT_OBSERVED_OUTPUT_SHAPE_CACHE_DIR, J as codeModeDeclarationHash, Jt as assertToolsCallTaskCapability, K as CodeModeLogStore, Kt as serializeMessage, L as codeModeRunParamsSchema, Ln as objectFromShape, Lt as defaultCacheBaseDir, M as nativeCapletToolName, Mn as getParseErrorMessage, Mt as isTokenBundleExpired, Nn as getSchemaDescription, Nt as readTokenBundle, O as parseServerBaseUrl, On as isJSONRPCRequest, Ot as runOAuthFlow, Pn as isSchemaOptional, Pt as DEFAULT_AUTH_DIR, Q as CapletsEngine, Qt as toJsonSchemaCompat, R as emptyCodeModeRunMeta, Rn as safeParse, Rt as defaultConfigBaseDir, S as resolveCapletsRemote, Sn as SUPPORTED_PROTOCOL_VERSIONS, St as loadCapletFilesFromMap, T as appendBasePath, Tn as assertCompleteRequestResourceTemplate, Tt as markdownStructuredContent, U as createCodeModeCapletsApi, Ut as resolveProjectCapletsRoot, V as QuickJsCodeModeSandbox, Vt as resolveCapletsRoot, W as listCodeModeCallableCaplets, Wt as resolveProjectConfigPath, X as generateCodeModeRunToolDescription, Xt as Protocol, Y as generateCodeModeDeclarations, Yt as AjvJsonSchemaValidator, Z as minifyCodeModeDeclarationText, Zt as mergeCapabilities, _ as CapletsCloudClient, _n as ListRootsResultSchema, _t as FileVaultStore, a as CloudAuthStore, at as ServerRegistry, b as isCapletsCloudUrl, bn as McpError, bt as discoverCapletFiles, c as redactedCloudAuthStatus, cn as ErrorCode, ct as loadConfig, d as projectBindingError, dn as InitializedNotificationSchema, dt as loadLocalOverlayConfigWithSources, en as CallToolResultSchema, et as decodeDirectResourceUri, f as projectBindingRecovery, fn as JSONRPCMessageSchema, ft as loadProjectConfig, g as buildProjectSyncManifest, gn as ListResourcesRequestSchema, gt as vaultStoreForAuthDir, hn as ListResourceTemplatesRequestSchema, ht as vaultResolverForAuthDir, i as createRemoteProfileStore, in as CreateTaskResultSchema, it as handleServerTool, j as nativeCapletToolDescription, jn as getObjectShape, jt as deleteTokenBundle, k as resolveCapletsServer, kn as isJSONRPCResultResponse, kt as startGenericOAuthFlow, l as PROJECT_BINDING_ERROR_CODES, ln as GetPromptRequestSchema, lt as loadConfigWithSources, mn as ListPromptsRequestSchema, mt as vaultBootstrapResolver, n as resolveRemoteSelection, nn as CreateMessageResultSchema, nt as findProjectRoot, o as cloudAuthPath, on as ElicitResultSchema, ot as capabilityDescription, p as CloudAuthClient, pn as LATEST_PROTOCOL_VERSION, pt as parseConfig, q as redactCodeModeLogText, qt as assertClientRequestTaskCapability, r as cloudCredentialsFromRemoteProfile, rn as CreateMessageResultWithToolsSchema, rt as fingerprintProjectRoot, s as migrateCredentials, sn as EmptyResultSchema, st as GoogleDiscoveryManager, t as createNativeCapletsService, tn as CompleteRequestSchema, tt as directResourceUriMatchesTemplate, u as ProjectBindingError, un as InitializeRequestSchema, ut as loadGlobalConfig, vn as ListToolsRequestSchema, vt as VAULT_MAX_VALUE_BYTES, w as resolveRemoteMode, wn as assertCompleteRequestPrompt, wt as markdownCallToolResultContent, x as normalizeRemoteProfileHostUrl, xn as ReadResourceRequestSchema, xt as validateCapletFile, y as hostedCloudWorkspaceFromRemoteUrl, yn as LoggingLevelSchema, yt as validateVaultKeyName, z as runCodeMode, zn as safeParseAsync, zt as defaultConfigPath } from "./service-rvZ7z6FI.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-BGGiZLHa.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 { a as SERVER_ID_PATTERN, d as CapletsError, m as toSafeError, p as redactSecrets$1, u as CAPLETS_ERROR_CODES } from "./validation-CWzd2gtn.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-DaYL-XQN.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-1wDjwHkC.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";
@@ -1553,7 +1553,7 @@ const EMPTY_COMPLETION_RESULT = { completion: {
1553
1553
  } };
1554
1554
  //#endregion
1555
1555
  //#region package.json
1556
- var version = "0.26.0";
1556
+ var version = "0.27.0";
1557
1557
  //#endregion
1558
1558
  //#region src/serve/session.ts
1559
1559
  var CapletsMcpSession = class {
@@ -5864,6 +5864,21 @@ function assertPathSegment(value, label) {
5864
5864
  if (!value || value.includes("/") || value.includes("\\") || value.includes("\0") || value === "." || value === "..") throw new Error(`Invalid ${label}: ${value}`);
5865
5865
  }
5866
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
5867
5882
  //#region src/daemon/config.ts
5868
5883
  function readDaemonConfig(paths) {
5869
5884
  return readJson(paths.configFile);
@@ -5877,10 +5892,10 @@ function writeDaemonState(paths, state) {
5877
5892
  return state;
5878
5893
  }
5879
5894
  function removeDaemonConfig(paths) {
5880
- rmSync(paths.configFile, { force: true });
5895
+ rmSync(daemonHostPath(paths.configFile), { force: true });
5881
5896
  }
5882
5897
  function removeDaemonState(paths) {
5883
- rmSync(paths.stateFile, { force: true });
5898
+ rmSync(daemonHostPath(paths.stateFile), { force: true });
5884
5899
  }
5885
5900
  function mergeDaemonEnv(existing, install) {
5886
5901
  const values = { ...existing?.values };
@@ -5904,20 +5919,22 @@ function validateEnvName(value) {
5904
5919
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value)) throw new CapletsError("REQUEST_INVALID", `Invalid environment variable name: ${value}`);
5905
5920
  }
5906
5921
  function readJson(path) {
5922
+ const hostPath = daemonHostPath(path);
5907
5923
  try {
5908
- return JSON.parse(readFileSync(path, "utf8"));
5924
+ return JSON.parse(readFileSync(hostPath, "utf8"));
5909
5925
  } catch (error) {
5910
5926
  if (error.code === "ENOENT") return void 0;
5911
5927
  throw error;
5912
5928
  }
5913
5929
  }
5914
5930
  function writeJson(path, value) {
5915
- mkdirSync(dirname(path), {
5931
+ const hostPath = daemonHostPath(path);
5932
+ mkdirSync(dirname(hostPath), {
5916
5933
  recursive: true,
5917
5934
  mode: 448
5918
5935
  });
5919
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
5920
- chmodSync(path, 384);
5936
+ writeFileSync(hostPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
5937
+ chmodSync(hostPath, 384);
5921
5938
  }
5922
5939
  //#endregion
5923
5940
  //#region src/daemon/logs.ts
@@ -5952,9 +5969,10 @@ async function followDaemonLogs(paths, options) {
5952
5969
  }).entries) options.write(entry);
5953
5970
  const watchers = selectedStreams(options.stream ?? "all").map((stream) => {
5954
5971
  const file = paths[stream === "stdout" ? "stdoutLog" : "stderrLog"];
5972
+ const hostFile = daemonHostPath(file);
5955
5973
  ensureLogFile(file);
5956
- let offset = existsSync(file) ? statSync(file).size : 0;
5957
- return watch(file, { persistent: true }, () => {
5974
+ let offset = existsSync(hostFile) ? statSync(hostFile).size : 0;
5975
+ return watch(hostFile, { persistent: true }, () => {
5958
5976
  const { content, nextOffset } = readFromOffset(file, offset);
5959
5977
  offset = nextOffset;
5960
5978
  for (const line of content.split(/\r?\n/u).filter(Boolean)) options.write({
@@ -5977,13 +5995,14 @@ function selectedStreams(stream) {
5977
5995
  return stream === "all" ? ["stdout", "stderr"] : [stream];
5978
5996
  }
5979
5997
  function tailLines(path, count) {
5980
- if (count === 0 || !existsSync(path)) return [];
5981
- 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);
5982
6001
  if (lines.at(-1) === "") lines.pop();
5983
6002
  return count < 0 ? lines : lines.slice(-count);
5984
6003
  }
5985
6004
  function readTailContent(path, count) {
5986
- const fd = openSync(path, "r");
6005
+ const fd = openSync(daemonHostPath(path), "r");
5987
6006
  try {
5988
6007
  const size = fstatSync(fd).size;
5989
6008
  const chunks = [];
@@ -6004,11 +6023,12 @@ function readTailContent(path, count) {
6004
6023
  }
6005
6024
  }
6006
6025
  function readFromOffset(path, offset) {
6007
- if (!existsSync(path)) return {
6026
+ const hostPath = daemonHostPath(path);
6027
+ if (!existsSync(hostPath)) return {
6008
6028
  content: "",
6009
6029
  nextOffset: 0
6010
6030
  };
6011
- const fd = openSync(path, "r");
6031
+ const fd = openSync(hostPath, "r");
6012
6032
  try {
6013
6033
  const size = fstatSync(fd).size;
6014
6034
  if (size <= offset) return {
@@ -6049,12 +6069,13 @@ function logTimestamp(line) {
6049
6069
  return Number.isNaN(timestamp) ? void 0 : timestamp;
6050
6070
  }
6051
6071
  function ensureLogFile(path) {
6052
- if (existsSync(path)) return;
6053
- mkdirSync(dirname(path), {
6072
+ const hostPath = daemonHostPath(path);
6073
+ if (existsSync(hostPath)) return;
6074
+ mkdirSync(dirname(hostPath), {
6054
6075
  recursive: true,
6055
6076
  mode: 448
6056
6077
  });
6057
- writeFileSync(path, "", { mode: 384 });
6078
+ writeFileSync(hostPath, "", { mode: 384 });
6058
6079
  }
6059
6080
  //#endregion
6060
6081
  //#region src/daemon/xml.ts
@@ -6245,9 +6266,9 @@ function launchdManager(runner, uid = typeof process.getuid === "function" ? pro
6245
6266
  return {
6246
6267
  descriptor: buildLaunchdDescriptor,
6247
6268
  status: async (config, paths) => {
6248
- if (!existsSync(paths.descriptorFile) && !config) return notInstalled();
6269
+ if (!existsSync(daemonHostPath(paths.descriptorFile)) && !config) return notInstalled();
6249
6270
  const result = await runner.exec("launchctl", ["print", target]);
6250
- 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 });
6251
6272
  const pid = parseNumberMatch(result.stdout, /\bpid\s*=\s*(\d+)/u);
6252
6273
  if (pid !== void 0) return runningOrStopped(pid, {
6253
6274
  stdout: result.stdout,
@@ -6273,7 +6294,7 @@ function launchdManager(runner, uid = typeof process.getuid === "function" ? pro
6273
6294
  };
6274
6295
  },
6275
6296
  uninstall: async (config, paths) => {
6276
- const hasDescriptor = existsSync(paths.descriptorFile);
6297
+ const hasDescriptor = existsSync(daemonHostPath(paths.descriptorFile));
6277
6298
  if (!hasDescriptor && !config) return {
6278
6299
  action: "uninstall",
6279
6300
  native: notInstalled(),
@@ -6291,7 +6312,7 @@ function launchdManager(runner, uid = typeof process.getuid === "function" ? pro
6291
6312
  ]];
6292
6313
  const result = await runner.exec(commands[0][0], commands[0].slice(1));
6293
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}`);
6294
- rmSync(paths.descriptorFile, { force: true });
6315
+ rmSync(daemonHostPath(paths.descriptorFile), { force: true });
6295
6316
  return {
6296
6317
  action: "uninstall",
6297
6318
  native: notInstalled({
@@ -6304,7 +6325,7 @@ function launchdManager(runner, uid = typeof process.getuid === "function" ? pro
6304
6325
  start: async (config) => launchdStartLifecycle(runner, domain, target, config.paths.descriptorFile),
6305
6326
  restart: async (config) => launchdRestartLifecycle(runner, domain, target, config.paths.descriptorFile),
6306
6327
  stop: async (config) => {
6307
- const command = config && existsSync(config.paths.descriptorFile) ? [
6328
+ const command = config && existsSync(daemonHostPath(config.paths.descriptorFile)) ? [
6308
6329
  "launchctl",
6309
6330
  "bootout",
6310
6331
  domain,
@@ -6329,7 +6350,7 @@ function systemdManager(runner, serviceAvailable = true) {
6329
6350
  descriptor: buildSystemdDescriptor,
6330
6351
  status: async (config, paths) => {
6331
6352
  if (!serviceAvailable) return unavailable("systemd --user is not available.");
6332
- if (!existsSync(paths.descriptorFile) && !config) return notInstalled();
6353
+ if (!existsSync(daemonHostPath(paths.descriptorFile)) && !config) return notInstalled();
6333
6354
  const show = await runner.exec("systemctl", [
6334
6355
  "--user",
6335
6356
  "show",
@@ -6338,13 +6359,13 @@ function systemdManager(runner, serviceAvailable = true) {
6338
6359
  if (show.code !== 0) {
6339
6360
  const message = show.stderr || show.stdout || String(show.code);
6340
6361
  if (isSystemdUnavailable(message)) return unavailable(`systemd --user is not available: ${message}`);
6341
- return existsSync(paths.descriptorFile) ? stopped({
6362
+ return existsSync(daemonHostPath(paths.descriptorFile)) ? stopped({
6342
6363
  stderr: show.stderr,
6343
6364
  stdout: show.stdout
6344
6365
  }) : notInstalled({ stderr: show.stderr });
6345
6366
  }
6346
6367
  const raw = parseSystemdShow(show.stdout);
6347
- if (!existsSync(paths.descriptorFile) && raw.LoadState === "not-found") return notInstalled({
6368
+ if (!existsSync(daemonHostPath(paths.descriptorFile)) && raw.LoadState === "not-found") return notInstalled({
6348
6369
  raw,
6349
6370
  stderr: show.stderr
6350
6371
  });
@@ -6410,7 +6431,7 @@ function systemdManager(runner, serviceAvailable = true) {
6410
6431
  ]];
6411
6432
  await assertExecUnless(runner, commands[0], "systemd unregister failed", /not loaded|not found|No such file|does not exist/iu);
6412
6433
  const descriptorBackup = backupPath(paths.descriptorFile);
6413
- rmSync(paths.descriptorFile, { force: true });
6434
+ rmSync(daemonHostPath(paths.descriptorFile), { force: true });
6414
6435
  try {
6415
6436
  await assertExec(runner, commands[1], "systemd unregister failed");
6416
6437
  } catch (error) {
@@ -6433,7 +6454,7 @@ function windowsTaskManager(runner) {
6433
6454
  return {
6434
6455
  descriptor: buildWindowsTaskDescriptor,
6435
6456
  status: async (config, paths) => {
6436
- if (!existsSync(paths.descriptorFile) && !config) return notInstalled();
6457
+ if (!existsSync(daemonHostPath(paths.descriptorFile)) && !config) return notInstalled();
6437
6458
  const result = await runner.exec("schtasks", [
6438
6459
  "/Query",
6439
6460
  "/TN",
@@ -6488,8 +6509,8 @@ function windowsTaskManager(runner) {
6488
6509
  "/F"
6489
6510
  ];
6490
6511
  await assertExecUnless(runner, command, "Scheduled Task unregister failed", /cannot find|does not exist|not found/iu);
6491
- rmSync(paths.descriptorFile, { force: true });
6492
- rmSync(paths.wrapperFile, { force: true });
6512
+ rmSync(daemonHostPath(paths.descriptorFile), { force: true });
6513
+ rmSync(daemonHostPath(paths.wrapperFile), { force: true });
6493
6514
  return {
6494
6515
  action: "uninstall",
6495
6516
  native: notInstalled(),
@@ -6561,19 +6582,21 @@ function unsupportedManager(platform) {
6561
6582
  };
6562
6583
  }
6563
6584
  function writeDescriptor(descriptor) {
6564
- mkdirSync(dirname(descriptor.path), {
6585
+ const descriptorPath = daemonHostPath(descriptor.path);
6586
+ mkdirSync(dirname(descriptorPath), {
6565
6587
  recursive: true,
6566
6588
  mode: 448
6567
6589
  });
6568
- writeFileSync(descriptor.path, descriptor.kind === "windows-scheduled-task" ? descriptor.xml : descriptor.contents, { mode: 384 });
6569
- chmodSync(descriptor.path, 384);
6590
+ writeFileSync(descriptorPath, descriptor.kind === "windows-scheduled-task" ? descriptor.xml : descriptor.contents, { mode: 384 });
6591
+ chmodSync(descriptorPath, 384);
6570
6592
  if (descriptor.kind === "windows-scheduled-task") {
6571
- mkdirSync(dirname(descriptor.wrapper.path), {
6593
+ const wrapperPath = daemonHostPath(descriptor.wrapper.path);
6594
+ mkdirSync(dirname(wrapperPath), {
6572
6595
  recursive: true,
6573
6596
  mode: 448
6574
6597
  });
6575
- writeFileSync(descriptor.wrapper.path, descriptor.wrapper.contents, { mode: 448 });
6576
- chmodSync(descriptor.wrapper.path, 448);
6598
+ writeFileSync(wrapperPath, descriptor.wrapper.contents, { mode: 448 });
6599
+ chmodSync(wrapperPath, 448);
6577
6600
  }
6578
6601
  }
6579
6602
  async function writeDescriptorForInstall(descriptor, register, afterRestore) {
@@ -6593,23 +6616,27 @@ function backupDescriptorFiles(descriptor) {
6593
6616
  return (descriptor.kind === "windows-scheduled-task" ? [descriptor.path, descriptor.wrapper.path] : [descriptor.path]).map(backupPath);
6594
6617
  }
6595
6618
  function backupPath(path) {
6596
- const existed = existsSync(path);
6619
+ const hostPath = daemonHostPath(path);
6620
+ const existed = existsSync(hostPath);
6597
6621
  return {
6598
6622
  path,
6599
6623
  existed,
6600
- ...existed ? { contents: readFileSync(path) } : {},
6601
- ...existed ? { mode: statSync(path).mode & 511 } : {}
6624
+ ...existed ? { contents: readFileSync(hostPath) } : {},
6625
+ ...existed ? { mode: statSync(hostPath).mode & 511 } : {}
6602
6626
  };
6603
6627
  }
6604
6628
  function restoreDescriptorFiles(backups) {
6605
- for (const backup of backups) if (backup.existed && backup.contents) {
6606
- mkdirSync(dirname(backup.path), {
6607
- recursive: true,
6608
- mode: 448
6609
- });
6610
- writeFileSync(backup.path, backup.contents, { mode: backup.mode ?? 384 });
6611
- chmodSync(backup.path, backup.mode ?? 384);
6612
- } 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
+ }
6613
6640
  }
6614
6641
  async function assertExec(runner, command, message) {
6615
6642
  const result = await runner.exec(command[0], command.slice(1));
@@ -11470,6 +11497,12 @@ function randomPairingPart(bytes) {
11470
11497
  const DEFAULT_PAIRING_CODE_TTL_MS = 10 * 6e4;
11471
11498
  const DEFAULT_PAIRING_CODE_MAX_ATTEMPTS = 5;
11472
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;
11473
11506
  const STALE_REFRESH_REVOKE_GRACE_MS = 3e4;
11474
11507
  const SUPERSEDED_REFRESH_TOKEN_RETENTION_MS = 1440 * 6e4;
11475
11508
  const STATE_FILE = "remote-server-credentials.json";
@@ -11481,6 +11514,199 @@ var RemoteServerCredentialStore = class {
11481
11514
  constructor(options) {
11482
11515
  this.dir = options.dir;
11483
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
+ }
11484
11710
  createPairingCode(input) {
11485
11711
  return this.withStateLock(() => {
11486
11712
  const now = input.now ?? /* @__PURE__ */ new Date();
@@ -11545,6 +11771,13 @@ var RemoteServerCredentialStore = class {
11545
11771
  listClients() {
11546
11772
  return this.loadState().clients.map(clientStatus).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
11547
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
+ }
11548
11781
  revokeClient(clientId, now = /* @__PURE__ */ new Date()) {
11549
11782
  return this.withStateLock(() => {
11550
11783
  const state = this.loadState();
@@ -11580,7 +11813,7 @@ var RemoteServerCredentialStore = class {
11580
11813
  const supersededAt = superseded ? Date.parse(superseded.supersededAt) : NaN;
11581
11814
  if (Number.isFinite(supersededAt) && now.getTime() - supersededAt >= STALE_REFRESH_REVOKE_GRACE_MS) replayedClient.revokedAt = now.toISOString();
11582
11815
  this.saveState(state);
11583
- throw new CapletsError("AUTH_FAILED", "Remote refresh credential is stale.");
11816
+ throw new CapletsError("REMOTE_CREDENTIALS_REVOKED", "Remote refresh credential is stale.");
11584
11817
  }
11585
11818
  throw new CapletsError("AUTH_FAILED", "Remote refresh credential is invalid.");
11586
11819
  }
@@ -11608,12 +11841,17 @@ var RemoteServerCredentialStore = class {
11608
11841
  if (!existsSync(path)) return {
11609
11842
  version: 1,
11610
11843
  pairingCodes: [],
11844
+ pendingLogins: [],
11611
11845
  clients: []
11612
11846
  };
11613
11847
  const parsed = JSON.parse(readFileSync(path, "utf8"));
11614
11848
  return {
11615
11849
  version: 1,
11616
11850
  pairingCodes: parsed.pairingCodes ?? [],
11851
+ pendingLogins: (parsed.pendingLogins ?? []).map((pending) => ({
11852
+ ...pending,
11853
+ supersededPendingRefreshHashes: parseSupersededRefreshTokens(pending.supersededPendingRefreshHashes)
11854
+ })),
11617
11855
  clients: (parsed.clients ?? []).map((client) => ({
11618
11856
  ...client,
11619
11857
  supersededRefreshTokenHashes: parseSupersededRefreshTokens(client.supersededRefreshTokenHashes)
@@ -11683,6 +11921,13 @@ var RemoteServerCredentialStore = class {
11683
11921
  return false;
11684
11922
  }
11685
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
+ }
11686
11931
  };
11687
11932
  function sleepSync(ms) {
11688
11933
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -11691,14 +11936,92 @@ function isFileExistsError(error) {
11691
11936
  return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
11692
11937
  }
11693
11938
  function pruneSupersededRefreshTokens(entries, now) {
11694
- return entries.filter((entry) => {
11939
+ return capSupersededRefreshTokens(entries.filter((entry) => {
11695
11940
  const supersededAt = Date.parse(entry.supersededAt);
11696
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
11697
11990
  });
11698
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
+ }
11699
12022
  function validateClient(client, hostUrl, now, options = {}) {
11700
12023
  if (client.hostUrl !== hostUrl) throw new CapletsError("AUTH_FAILED", "Remote client credential is for a different host.");
11701
- 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.");
11702
12025
  if (!options.allowExpiredAccess && Date.parse(client.accessExpiresAt) <= now.getTime()) throw new CapletsError("AUTH_FAILED", "Remote client credential has expired.");
11703
12026
  }
11704
12027
  function credentialsFromClient(client, accessToken, refreshToken) {
@@ -11723,9 +12046,48 @@ function clientStatus(client) {
11723
12046
  ...client.revokedAt ? { revokedAt: client.revokedAt } : {}
11724
12047
  };
11725
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
+ }
11726
12077
  function hashSecret(secret) {
11727
12078
  return createHash("sha256").update(secret).digest("base64url");
11728
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
+ }
11729
12091
  function safeHashEqual(left, right) {
11730
12092
  const leftBuffer = Buffer$1.from(left);
11731
12093
  const rightBuffer = Buffer$1.from(right);
@@ -11758,41 +12120,95 @@ function createHttpServeApp(options, engine, io = {}) {
11758
12120
  const remoteCredentialStore = remoteCredentialStoreForOptions(options, io.remoteCredentialStore);
11759
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.");
11760
12122
  const protectedRouteAuth = routeAuth(options, remoteCredentialStore, paths.base);
12123
+ const attachHostProtection = dnsRebindingProtection(options);
11761
12124
  app.use("*", logger((message, ...rest) => {
11762
12125
  writeErr(`${[message, ...rest].join(" ")}\n`);
11763
12126
  }));
11764
- app.get(paths.base, (c) => c.json({
11765
- name: "caplets",
11766
- transport: "http",
11767
- base: paths.base,
11768
- versions: [versionDiscovery(paths, exposeAttach)],
11769
- auth: { type: options.auth.type }
11770
- }));
11771
- 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
+ });
11772
12142
  app.get(paths.health, (c) => c.json({ status: "ok" }));
11773
12143
  if (remoteCredentialStore) {
11774
- app.post(paths.pairingExchange, async (c) => {
12144
+ app.post(paths.remoteLoginStart, attachHostProtection, async (c) => {
11775
12145
  try {
11776
- const parsed = await parseJsonObject(c.req.json(), "Pairing exchange request");
11777
- const code = stringField(parsed, "code");
12146
+ const parsed = await parseJsonObject(c.req.json(), "Pending remote login start request");
11778
12147
  const clientLabel = optionalStringField(parsed, "clientLabel");
11779
- const credentials = remoteCredentialStore.exchangePairingCode({
11780
- hostUrl: remoteCredentialHostUrl(c.req.url, paths.base, options.publicOrigin, options.trustProxy, (name) => c.req.header(name)),
11781
- code,
11782
- ...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 } : {}
11783
12156
  });
11784
- return c.json({
11785
- clientId: credentials.clientId,
11786
- clientLabel: credentials.clientLabel,
11787
- accessToken: credentials.accessToken,
11788
- refreshToken: credentials.refreshToken,
11789
- tokenType: credentials.tokenType,
11790
- 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")
11791
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
+ }));
11792
12205
  } catch (error) {
11793
12206
  return remoteCredentialErrorResponse(error);
11794
12207
  }
11795
12208
  });
12209
+ app.post(paths.pairingExchange, async (_c) => {
12210
+ return remoteCredentialErrorResponse(legacyPairingCodeUnsupportedError());
12211
+ });
11796
12212
  app.post(paths.remoteRefresh, async (c) => {
11797
12213
  try {
11798
12214
  const refreshToken = stringField(await parseJsonObject(c.req.json(), "Remote refresh request"), "refreshToken");
@@ -11855,7 +12271,6 @@ function createHttpServeApp(options, engine, io = {}) {
11855
12271
  sessions.set(nextSessionId, session);
11856
12272
  return session.transport.handleRequest(c);
11857
12273
  });
11858
- const attachHostProtection = dnsRebindingProtection(options);
11859
12274
  if (exposeAttach) {
11860
12275
  app.get(paths.attachManifest, attachHostProtection, protectedRouteAuth, async (c) => {
11861
12276
  const attachProjection = await buildAttachProjection(engine);
@@ -11970,13 +12385,26 @@ function remoteCredentialHostUrl(requestUrl, basePath, publicOrigin, trustProxy,
11970
12385
  if (trustProxy && !publicOrigin) throw new CapletsError("REQUEST_INVALID", "Remote credential auth with --trust-proxy requires CAPLETS_SERVER_URL.");
11971
12386
  return publicHostUrl(requestUrl, basePath, publicOrigin, trustProxy, header);
11972
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
+ }
11973
12393
  function firstForwardedValue(value) {
11974
12394
  return value?.split(",", 1)[0]?.trim() || void 0;
11975
12395
  }
11976
- 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) {
11977
12404
  return {
11978
12405
  version: 1,
11979
12406
  path: paths.version,
12407
+ ...remote ? { remote } : {},
11980
12408
  links: {
11981
12409
  mcp: paths.mcp,
11982
12410
  admin: paths.control,
@@ -12126,6 +12554,11 @@ function servicePaths(base) {
12126
12554
  attachInvoke: routePath(attach, "invoke"),
12127
12555
  projectBindings: routePath(attach, "project-bindings"),
12128
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"),
12129
12562
  remoteRefresh: routePath(remote, "refresh"),
12130
12563
  remoteClient: routePath(remote, "client"),
12131
12564
  health: routePath(version, "healthz")
@@ -12210,6 +12643,9 @@ function remoteCredentialErrorResponse(error) {
12210
12643
  error: safe
12211
12644
  }, { status });
12212
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
+ }
12213
12649
  function dnsRebindingProtection(options) {
12214
12650
  if (!options.loopback) return async (_c, next) => {
12215
12651
  await next();
@@ -12438,7 +12874,7 @@ async function installDaemon(install = {}, options = {}) {
12438
12874
  "write-descriptor",
12439
12875
  "register-service"
12440
12876
  ];
12441
- 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;
12442
12878
  const restartDecisionRequired = existingNative?.running === true && !install.start && !install.restart && !install.noRestart;
12443
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.");
12444
12880
  if (install.dryRun) return {
@@ -12459,13 +12895,13 @@ async function installDaemon(install = {}, options = {}) {
12459
12895
  });
12460
12896
  assertDaemonHealth(validation, "Daemon install validation");
12461
12897
  }
12462
- mkdirSync(paths.logDir, {
12898
+ mkdirSync(daemonHostPath(paths.logDir), {
12463
12899
  recursive: true,
12464
12900
  mode: 448
12465
12901
  });
12466
12902
  ensureDaemonLogFiles(paths);
12467
12903
  const persistenceBackups = backupPersistenceFiles([paths.configFile, paths.stateFile]);
12468
- const hadExistingDescriptor = existsSync(paths.descriptorFile);
12904
+ const hadExistingDescriptor = existsSync(daemonHostPath(paths.descriptorFile));
12469
12905
  let native;
12470
12906
  try {
12471
12907
  writeDaemonConfig(paths, config);
@@ -12615,11 +13051,11 @@ async function uninstallDaemon(uninstall = {}, options = {}) {
12615
13051
  if (uninstall.purge) {
12616
13052
  removeDaemonConfig(paths);
12617
13053
  removeDaemonState(paths);
12618
- rmSync(paths.logDir, {
13054
+ rmSync(daemonHostPath(paths.logDir), {
12619
13055
  recursive: true,
12620
13056
  force: true
12621
13057
  });
12622
- rmSync(dirname(paths.configFile), {
13058
+ rmSync(dirname(daemonHostPath(paths.configFile)), {
12623
13059
  recursive: true,
12624
13060
  force: true
12625
13061
  });
@@ -12859,25 +13295,32 @@ function mergeServeOptions(existing, install) {
12859
13295
  };
12860
13296
  }
12861
13297
  function backupPersistenceFiles(paths) {
12862
- return paths.map((path) => ({
12863
- path,
12864
- existed: existsSync(path),
12865
- ...existsSync(path) ? { contents: readFileSync(path) } : {},
12866
- ...existsSync(path) ? { mode: statSync(path).mode & 511 } : {}
12867
- }));
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
+ });
12868
13308
  }
12869
13309
  function restorePersistenceFiles(backups) {
12870
- for (const backup of backups) if (backup.existed && backup.contents) {
12871
- 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, {
12872
13320
  recursive: true,
12873
- mode: 448
13321
+ force: true
12874
13322
  });
12875
- writeFileSync(backup.path, backup.contents, { mode: backup.mode ?? 384 });
12876
- chmodSync(backup.path, backup.mode ?? 384);
12877
- } else rmSync(backup.path, {
12878
- recursive: true,
12879
- force: true
12880
- });
13323
+ }
12881
13324
  }
12882
13325
  async function rollbackNativeInstall(manager, persisted, paths, hadExistingDescriptor) {
12883
13326
  try {
@@ -13965,11 +14408,7 @@ function remoteSetupDefinition(id, options) {
13965
14408
  type: "command",
13966
14409
  label: "Add remote-backed Caplets MCP server to Codex",
13967
14410
  command: "codex",
13968
- args: codexMcpAddArgs([
13969
- "attach",
13970
- "--remote-url",
13971
- serverUrl
13972
- ])
14411
+ args: codexMcpAddArgs(["attach", serverUrl])
13973
14412
  }],
13974
14413
  nextSteps: [`Run caplets remote login ${serverUrl} before using this MCP config.`, "In Codex, run /mcp to confirm the caplets server is connected."]
13975
14414
  };
@@ -13979,11 +14418,7 @@ function remoteSetupDefinition(id, options) {
13979
14418
  type: "command",
13980
14419
  label: "Add remote-backed Caplets MCP server to Claude Code",
13981
14420
  command: "claude",
13982
- args: claudeMcpAddArgs([
13983
- "attach",
13984
- "--remote-url",
13985
- serverUrl
13986
- ])
14421
+ args: claudeMcpAddArgs(["attach", serverUrl])
13987
14422
  }],
13988
14423
  nextSteps: [`Run caplets remote login ${serverUrl} before using this MCP config.`, "In Claude Code, run /mcp to confirm the caplets server is connected."]
13989
14424
  };
@@ -13996,11 +14431,7 @@ function remoteSetupDefinition(id, options) {
13996
14431
  path: options.output,
13997
14432
  content: `${JSON.stringify({ mcpServers: { caplets: {
13998
14433
  command: "caplets",
13999
- args: [
14000
- "attach",
14001
- "--remote-url",
14002
- serverUrl
14003
- ]
14434
+ args: ["attach", serverUrl]
14004
14435
  } } }, null, 2)}\n`
14005
14436
  }],
14006
14437
  nextSteps: [`Run caplets remote login ${serverUrl} before using this MCP config.`, "Import the written MCP config into your MCP client."]
@@ -14938,12 +15369,17 @@ function addServeMigrationCommand(parent, name, replacement) {
14938
15369
  function collectValues(value, previous) {
14939
15370
  return [...previous, value];
14940
15371
  }
15372
+ const HIDDEN_INPUT_PROMPT_LABELS = { vaultValue: "Value: " };
14941
15373
  function remoteProfileStore(authDir, env) {
14942
15374
  return createRemoteProfileStore({
14943
15375
  authDir,
14944
15376
  env
14945
15377
  });
14946
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
+ }
14947
15383
  function remoteServerCredentialStore(statePath, env) {
14948
15384
  return new RemoteServerCredentialStore({ dir: statePath ?? env.CAPLETS_REMOTE_SERVER_STATE_DIR ?? join(DEFAULT_AUTH_DIR, "remote-server") });
14949
15385
  }
@@ -15032,40 +15468,33 @@ async function loginCloudRemoteProfile(url, options, store, io) {
15032
15468
  }
15033
15469
  });
15034
15470
  }
15035
- async function pairingCodeFromOptions(options, readStdin, writeErr) {
15036
- if (options.code?.trim()) {
15037
- writeErr("Warning: --code may store the Pairing Code in shell history; prefer the hidden prompt or --code-stdin for automation.\n");
15038
- return options.code.trim();
15039
- }
15040
- if (options.codeStdin) {
15041
- const code = (readStdin ? await readStdin() : await readAllStdin()).trim();
15042
- if (code) return code;
15043
- throw new CapletsError("REQUEST_INVALID", "Pairing Code is required when --code-stdin is used.");
15044
- }
15045
- 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);
15046
15475
  const readline = createInterface({
15047
- input: process.stdin,
15048
- output,
15476
+ input,
15477
+ output: new HiddenPromptOutput(output, { echoFirstChunk: false }),
15049
15478
  terminal: true
15050
15479
  });
15051
15480
  try {
15052
- const code = (await readline.question("Pairing Code: ")).trim();
15053
- if (code) return code;
15481
+ return await readline.question("");
15054
15482
  } finally {
15055
15483
  readline.close();
15056
- process.stdout.write("\n");
15484
+ output.write("\n");
15057
15485
  }
15058
- throw new CapletsError("REQUEST_INVALID", "Pairing Code is required for self-hosted Remote Login.");
15059
15486
  }
15060
15487
  var HiddenPromptOutput = class extends Writable {
15061
15488
  output;
15489
+ options;
15062
15490
  wrotePrompt = false;
15063
- constructor(output) {
15491
+ constructor(output, options = { echoFirstChunk: true }) {
15064
15492
  super();
15065
15493
  this.output = output;
15494
+ this.options = options;
15066
15495
  }
15067
15496
  _write(chunk, _encoding, callback) {
15068
- if (!this.wrotePrompt) {
15497
+ if (this.options.echoFirstChunk !== false && !this.wrotePrompt) {
15069
15498
  this.output.write(chunk);
15070
15499
  this.wrotePrompt = true;
15071
15500
  }
@@ -15083,6 +15512,7 @@ async function parseRemoteLoginCredentials(response) {
15083
15512
  const record = parsed;
15084
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.");
15085
15514
  return {
15515
+ ...typeof record.hostUrl === "string" ? { hostUrl: record.hostUrl } : {},
15086
15516
  clientId: record.clientId,
15087
15517
  clientLabel: typeof record.clientLabel === "string" ? record.clientLabel : "Caplets CLI",
15088
15518
  accessToken: record.accessToken,
@@ -15091,6 +15521,170 @@ async function parseRemoteLoginCredentials(response) {
15091
15521
  ...typeof record.expiresAt === "string" ? { expiresAt: record.expiresAt } : {}
15092
15522
  };
15093
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
+ }
15094
15688
  async function revokeSelfHostedRemoteClient(remoteUrl, accessToken, fetchImpl = fetch) {
15095
15689
  await fetchImpl(appendBasePath(new URL(normalizeRemoteProfileHostUrl(remoteUrl)), "v1/remote/client"), {
15096
15690
  method: "DELETE",
@@ -15172,9 +15766,35 @@ function createSetupPromptHandle(io, writeOut) {
15172
15766
  close: () => readline.close()
15173
15767
  };
15174
15768
  }
15175
- async function sleep(ms) {
15176
- if (ms <= 0) return;
15177
- 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
+ };
15178
15798
  }
15179
15799
  function createProgram(io = {}) {
15180
15800
  const writeOut = io.writeOut ?? ((value) => process.stdout.write(value));
@@ -15403,10 +16023,12 @@ function createProgram(io = {}) {
15403
16023
  }
15404
16024
  for (const entry of result.entries) writeOut(stream === "all" ? `[${entry.stream}] ${entry.line}\n` : `${entry.line}\n`);
15405
16025
  });
15406
- 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) => {
15407
16027
  try {
16028
+ const remoteUrl = attachRemoteUrlFromArgs(url, options.remoteUrl);
15408
16029
  const attachOptions = {
15409
16030
  ...options,
16031
+ ...remoteUrl ? { remoteUrl } : {},
15410
16032
  ...io.fetch ? { fetch: io.fetch } : {},
15411
16033
  ...io.authDir ? { authDir: io.authDir } : {}
15412
16034
  };
@@ -15461,7 +16083,7 @@ function createProgram(io = {}) {
15461
16083
  }
15462
16084
  });
15463
16085
  const remote = program.command(cliCommands$1.remote).description("Manage Caplets Remote Login.");
15464
- 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) => {
15465
16087
  const store = remoteProfileStore(io.authDir, env);
15466
16088
  if (isCapletsCloudUrl(url)) {
15467
16089
  writeRemoteStatus(await loginCloudRemoteProfile(url, options, store, {
@@ -15471,20 +16093,24 @@ function createProgram(io = {}) {
15471
16093
  }), options.json === true, writeOut);
15472
16094
  return;
15473
16095
  }
15474
- const code = await pairingCodeFromOptions(options, io.readStdin, writeErr);
15475
- const exchangeUrl = appendBasePath(new URL(normalizeRemoteProfileHostUrl(url)), "v1/remote/pairing/exchange");
15476
- const response = await (io.fetch ?? fetch)(exchangeUrl, {
15477
- method: "POST",
15478
- headers: { "content-type": "application/json" },
15479
- body: JSON.stringify({
15480
- code,
15481
- ...options.clientLabel ? { clientLabel: options.clientLabel } : {}
15482
- })
15483
- });
15484
- if (!response.ok) throw new CapletsError("AUTH_FAILED", "Remote Login pairing exchange failed.");
15485
- const credentials = await parseRemoteLoginCredentials(response);
15486
- 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({
15487
16112
  hostUrl: url,
16113
+ hostIdentity: normalizeRemoteProfileHostUrl(credentials.hostUrl ?? url),
15488
16114
  clientId: credentials.clientId,
15489
16115
  clientLabel: credentials.clientLabel,
15490
16116
  credentials: {
@@ -15493,7 +16119,12 @@ function createProgram(io = {}) {
15493
16119
  tokenType: credentials.tokenType,
15494
16120
  expiresAt: credentials.expiresAt
15495
16121
  }
15496
- }), 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);
15497
16128
  });
15498
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) => {
15499
16130
  if (!url) {
@@ -15554,18 +16185,19 @@ function createProgram(io = {}) {
15554
16185
  writeOut(removed ? `Logged out of ${normalizeRemoteProfileHostUrl(url)}.\n` : `No Remote Login profile found for ${normalizeRemoteProfileHostUrl(url)}.\n`);
15555
16186
  });
15556
16187
  const remoteHost = remote.command("host").description("Manage self-hosted remote credentials.");
15557
- 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) => {
15558
- const issued = remoteServerCredentialStore(options.statePath, env).createPairingCode({
15559
- hostUrl: options.hostUrl,
15560
- ...options.clientLabel ? { clientLabel: options.clientLabel } : {}
15561
- });
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.";
15562
16191
  if (options.json) {
15563
- 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`);
15564
16198
  return;
15565
16199
  }
15566
- writeOut(`Pairing Code: ${issued.code}\n`);
15567
- writeOut(`Expires At: ${issued.expiresAt}\n`);
15568
- writeOut(`Run caplets remote login ${normalizeRemoteProfileHostUrl(options.hostUrl)} and enter the Pairing Code when prompted.\n`);
16200
+ writeOut(`${guidance}\n`);
15569
16201
  });
15570
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) => {
15571
16203
  const clients = remoteServerCredentialStore(options.statePath, env).listClients();
@@ -15579,6 +16211,35 @@ function createProgram(io = {}) {
15579
16211
  }
15580
16212
  for (const client of clients) writeOut(`${client.clientId}\t${terminalSafeText(client.clientLabel)}\t${client.hostUrl}\t${client.revokedAt ? "revoked" : "active"}\n`);
15581
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
+ });
15582
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) => {
15583
16244
  const revoked = remoteServerCredentialStore(options.statePath, env).revokeClient(clientId);
15584
16245
  if (options.json) {
@@ -16495,20 +17156,7 @@ async function readVaultValue(io) {
16495
17156
  if (io.readStdin) value = stripOneTrailingNewline(await io.readStdin());
16496
17157
  else if (!process.stdin.isTTY && !io.writeOut && !io.writeErr) value = stripOneTrailingNewline(await readAllStdin());
16497
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.");
16498
- else {
16499
- const output = new HiddenPromptOutput(process.stdout);
16500
- const readline = createInterface({
16501
- input: process.stdin,
16502
- output,
16503
- terminal: true
16504
- });
16505
- try {
16506
- value = await readline.question("Vault value: ");
16507
- } finally {
16508
- readline.close();
16509
- process.stdout.write("\n");
16510
- }
16511
- }
17159
+ else value = await readHiddenInput(HIDDEN_INPUT_PROMPT_LABELS.vaultValue);
16512
17160
  if (value.length === 0) throw new CapletsError("REQUEST_INVALID", "Vault value input is required.");
16513
17161
  return value;
16514
17162
  }
@@ -16839,8 +17487,17 @@ function mergeRemoteAndLocalRows(remoteRows, localOverlay, options) {
16839
17487
  });
16840
17488
  if (!localOverlay) return [...rows.values()].filter((row) => options.includeDisabled || !row.disabled).sort((left, right) => left.server.localeCompare(right.server));
16841
17489
  for (const row of listCaplets(localOverlay, { includeDisabled: true })) {
16842
- if (rows.get(row.server)) {
17490
+ const remote = rows.get(row.server);
17491
+ if (remote) {
16843
17492
  if (row.disabled) continue;
17493
+ if (remote.shadowing === "namespace") {
17494
+ options.writeErr(`Local Caplet '${row.server}' is exposed under a qualified ID because the remote Caplet uses namespace shadowing for that Caplet ID.\n`);
17495
+ continue;
17496
+ }
17497
+ if (remote.shadowing !== "allow") {
17498
+ options.writeErr(`Local Caplet '${row.server}' is suppressed because the remote Caplet forbids shadowing that Caplet ID.\n`);
17499
+ continue;
17500
+ }
16844
17501
  options.writeErr(`Warning: ${formatOverlaySource(row.source)} Caplet ${row.server} shadows remote Caplet\n`);
16845
17502
  }
16846
17503
  rows.set(row.server, row);