@caplets/core 0.26.0 → 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/cli.d.ts +5 -0
- package/dist/{completion-DaYL-XQN.js → completion-CFOJucl5.js} +2 -2
- package/dist/config-runtime.js +1 -1
- package/dist/daemon/host-path.d.ts +8 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.js +820 -172
- package/dist/native.js +1 -1
- package/dist/project-binding/errors.d.ts +1 -1
- package/dist/remote/profile-store.d.ts +2 -0
- package/dist/remote/profiles.d.ts +2 -0
- package/dist/remote/server-credential-store.d.ts +100 -1
- package/dist/remote/server-credentials.d.ts +18 -0
- package/dist/serve/http.d.ts +5 -0
- package/dist/{service-rvZ7z6FI.js → service-aBIn4nrw.js} +56 -12
- package/dist/{validation-C4tYXw6G.js → validation-GD2x5HW1.js} +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { $ as resolveExposure, $t as
|
|
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-
|
|
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-
|
|
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";
|
|
@@ -1553,7 +1553,7 @@ const EMPTY_COMPLETION_RESULT = { completion: {
|
|
|
1553
1553
|
} };
|
|
1554
1554
|
//#endregion
|
|
1555
1555
|
//#region package.json
|
|
1556
|
-
var version = "0.26.
|
|
1556
|
+
var version = "0.26.1";
|
|
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(
|
|
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
|
-
|
|
5931
|
+
const hostPath = daemonHostPath(path);
|
|
5932
|
+
mkdirSync(dirname(hostPath), {
|
|
5916
5933
|
recursive: true,
|
|
5917
5934
|
mode: 448
|
|
5918
5935
|
});
|
|
5919
|
-
writeFileSync(
|
|
5920
|
-
chmodSync(
|
|
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(
|
|
5957
|
-
return watch(
|
|
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
|
-
|
|
5981
|
-
|
|
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
|
-
|
|
6026
|
+
const hostPath = daemonHostPath(path);
|
|
6027
|
+
if (!existsSync(hostPath)) return {
|
|
6008
6028
|
content: "",
|
|
6009
6029
|
nextOffset: 0
|
|
6010
6030
|
};
|
|
6011
|
-
const fd = openSync(
|
|
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
|
-
|
|
6053
|
-
|
|
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(
|
|
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
|
-
|
|
6585
|
+
const descriptorPath = daemonHostPath(descriptor.path);
|
|
6586
|
+
mkdirSync(dirname(descriptorPath), {
|
|
6565
6587
|
recursive: true,
|
|
6566
6588
|
mode: 448
|
|
6567
6589
|
});
|
|
6568
|
-
writeFileSync(
|
|
6569
|
-
chmodSync(
|
|
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
|
-
|
|
6593
|
+
const wrapperPath = daemonHostPath(descriptor.wrapper.path);
|
|
6594
|
+
mkdirSync(dirname(wrapperPath), {
|
|
6572
6595
|
recursive: true,
|
|
6573
6596
|
mode: 448
|
|
6574
6597
|
});
|
|
6575
|
-
writeFileSync(
|
|
6576
|
-
chmodSync(
|
|
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
|
|
6619
|
+
const hostPath = daemonHostPath(path);
|
|
6620
|
+
const existed = existsSync(hostPath);
|
|
6597
6621
|
return {
|
|
6598
6622
|
path,
|
|
6599
6623
|
existed,
|
|
6600
|
-
...existed ? { contents: readFileSync(
|
|
6601
|
-
...existed ? { mode: statSync(
|
|
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)
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
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("
|
|
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("
|
|
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) =>
|
|
11765
|
-
name:
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
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.
|
|
12144
|
+
app.post(paths.remoteLoginStart, attachHostProtection, async (c) => {
|
|
11775
12145
|
try {
|
|
11776
|
-
const parsed = await parseJsonObject(c.req.json(), "
|
|
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
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
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
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
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
|
|
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
|
|
12865
|
-
|
|
12866
|
-
|
|
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)
|
|
12871
|
-
|
|
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
|
-
|
|
13321
|
+
force: true
|
|
12874
13322
|
});
|
|
12875
|
-
|
|
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
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
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
|
|
15048
|
-
output,
|
|
15476
|
+
input,
|
|
15477
|
+
output: new HiddenPromptOutput(output, { echoFirstChunk: false }),
|
|
15049
15478
|
terminal: true
|
|
15050
15479
|
});
|
|
15051
15480
|
try {
|
|
15052
|
-
|
|
15053
|
-
if (code) return code;
|
|
15481
|
+
return await readline.question("");
|
|
15054
15482
|
} finally {
|
|
15055
15483
|
readline.close();
|
|
15056
|
-
|
|
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) =>
|
|
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").
|
|
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").
|
|
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
|
-
|
|
15475
|
-
const
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15480
|
-
|
|
15481
|
-
...
|
|
15482
|
-
|
|
15483
|
-
|
|
15484
|
-
|
|
15485
|
-
|
|
15486
|
-
|
|
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
|
-
})
|
|
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("
|
|
15558
|
-
const
|
|
15559
|
-
|
|
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(
|
|
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(
|
|
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
|
}
|