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